ggaction 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +5 -3
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +18 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +41 -45
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/apply.js +30 -0
- package/src/actions/encodings/position/policies/arc.js +23 -2
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +36 -7
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +212 -30
- package/src/actions/errorBars/edit.js +91 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +38 -16
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +79 -54
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +170 -32
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/positionCompatibility.js +1 -1
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/rules.js +2 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +139 -60
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +55 -30
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +9 -2
- package/src/materialization/facetGuides/placement.js +49 -19
- package/src/materialization/facetGuides/preparation.js +35 -6
- package/src/materialization/facets.js +42 -28
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +15 -4
- package/src/materialization/marks/index.js +29 -6
- package/src/materialization/rowEncoding.js +57 -1
- package/src/materialization/scales/policies/bar.js +61 -102
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +6 -9
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +523 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
|
@@ -21,9 +21,9 @@ function assertTitleScope(program, operation) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function hasRotation(graphic) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return (graphic.items ?? [graphic]).some(
|
|
25
|
+
item => Object.hasOwn(item.properties, "rotation")
|
|
26
|
+
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function ensureTextShape(program, id, component) {
|
|
@@ -45,14 +45,7 @@ function ensureTextShape(program, id, component) {
|
|
|
45
45
|
...placement
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
if (
|
|
49
|
-
return program.editGraphics({
|
|
50
|
-
target: id,
|
|
51
|
-
property: "length",
|
|
52
|
-
value: component.lines.length
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (collection && graphic.items.length !== component.lines.length) {
|
|
48
|
+
if (needsCollection && graphic.items?.length !== component.lines.length) {
|
|
56
49
|
return program.editGraphics({
|
|
57
50
|
target: id,
|
|
58
51
|
property: "length",
|
|
@@ -68,20 +61,20 @@ function distributed(value, count) {
|
|
|
68
61
|
|
|
69
62
|
function editTextGraphic(program, id, component, style) {
|
|
70
63
|
const count = component.lines.length;
|
|
71
|
-
let next = ensureTextShape(program, id, component)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.editGraphics({ target: id, property
|
|
84
|
-
|
|
64
|
+
let next = ensureTextShape(program, id, component);
|
|
65
|
+
for (const [property, value] of Object.entries({
|
|
66
|
+
x: distributed(component.x, count),
|
|
67
|
+
y: distributed(component.y, count),
|
|
68
|
+
text: distributed(component.lines, count),
|
|
69
|
+
fill: style.color,
|
|
70
|
+
fontSize: style.fontSize,
|
|
71
|
+
fontFamily: style.fontFamily,
|
|
72
|
+
fontWeight: style.fontWeight,
|
|
73
|
+
textAlign: component.textAlign,
|
|
74
|
+
textBaseline: "middle"
|
|
75
|
+
})) {
|
|
76
|
+
next = next.editGraphics({ target: id, property, value });
|
|
77
|
+
}
|
|
85
78
|
if (component.explicitRotation) {
|
|
86
79
|
next = next.editGraphics({
|
|
87
80
|
target: id,
|
|
@@ -92,78 +85,39 @@ function editTextGraphic(program, id, component, style) {
|
|
|
92
85
|
return next;
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
function titleTextAction(kind, create) {
|
|
89
|
+
const suffix = `${kind[0].toUpperCase()}${kind.slice(1)}Text`;
|
|
90
|
+
const op = `${create ? "create" : "edit"}${suffix}`;
|
|
91
|
+
const id = `chart${kind[0].toUpperCase()}${kind.slice(1)}`;
|
|
92
|
+
return action({
|
|
93
|
+
op,
|
|
94
|
+
description: `${create ? "Create" : "Rematerialize"} chart ${kind} text.`
|
|
95
|
+
}, function (args = {}) {
|
|
96
|
+
noOptions(args, op);
|
|
97
|
+
const config = requireTitleConfig(this);
|
|
98
|
+
if (create && this.graphicSpec.objects[id] !== undefined) {
|
|
99
|
+
throw new Error(`${op} requires a missing chart ${kind} graphic.`);
|
|
100
|
+
}
|
|
101
|
+
const component = resolveTitleLayout(this, config)[kind];
|
|
102
|
+
if (component === undefined) {
|
|
103
|
+
throw new Error(`${op} requires semantic subtitle text.`);
|
|
104
|
+
}
|
|
105
|
+
if (!create) {
|
|
106
|
+
return editTextGraphic(this, id, component, config[`${kind}Style`]);
|
|
107
|
+
}
|
|
108
|
+
return this.createGraphics({
|
|
109
|
+
id,
|
|
110
|
+
type: "text",
|
|
111
|
+
...(component.lines.length > 1 ? { length: component.lines.length } : {}),
|
|
112
|
+
...resolveCanvasGraphicPlacement(this)
|
|
113
|
+
})[`edit${suffix}`]();
|
|
101
114
|
});
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
export const editTitleText =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const config = requireTitleConfig(this);
|
|
109
|
-
const layout = resolveTitleLayout(this, config);
|
|
110
|
-
return editTextGraphic(
|
|
111
|
-
this,
|
|
112
|
-
"chartTitle",
|
|
113
|
-
layout.title,
|
|
114
|
-
config.titleStyle
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
export const createTitleText = action(
|
|
120
|
-
{ op: "createTitleText", description: "Create chart title text." },
|
|
121
|
-
function (args = {}) {
|
|
122
|
-
noOptions(args, "createTitleText");
|
|
123
|
-
const config = requireTitleConfig(this);
|
|
124
|
-
if (this.graphicSpec.objects.chartTitle !== undefined) {
|
|
125
|
-
throw new Error("createTitleText requires a missing chart title graphic.");
|
|
126
|
-
}
|
|
127
|
-
const layout = resolveTitleLayout(this, config);
|
|
128
|
-
return createTextGraphic(this, "chartTitle", layout.title)
|
|
129
|
-
.editTitleText();
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
export const editSubtitleText = action(
|
|
134
|
-
{ op: "editSubtitleText", description: "Rematerialize chart subtitle text." },
|
|
135
|
-
function (args = {}) {
|
|
136
|
-
noOptions(args, "editSubtitleText");
|
|
137
|
-
const config = requireTitleConfig(this);
|
|
138
|
-
const layout = resolveTitleLayout(this, config);
|
|
139
|
-
if (layout.subtitle === undefined) {
|
|
140
|
-
throw new Error("editSubtitleText requires semantic subtitle text.");
|
|
141
|
-
}
|
|
142
|
-
return editTextGraphic(
|
|
143
|
-
this,
|
|
144
|
-
"chartSubtitle",
|
|
145
|
-
layout.subtitle,
|
|
146
|
-
config.subtitleStyle
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
export const createSubtitleText = action(
|
|
152
|
-
{ op: "createSubtitleText", description: "Create chart subtitle text." },
|
|
153
|
-
function (args = {}) {
|
|
154
|
-
noOptions(args, "createSubtitleText");
|
|
155
|
-
const config = requireTitleConfig(this);
|
|
156
|
-
if (this.graphicSpec.objects.chartSubtitle !== undefined) {
|
|
157
|
-
throw new Error("createSubtitleText requires a missing chart subtitle graphic.");
|
|
158
|
-
}
|
|
159
|
-
const layout = resolveTitleLayout(this, config);
|
|
160
|
-
if (layout.subtitle === undefined) {
|
|
161
|
-
throw new Error("createSubtitleText requires semantic subtitle text.");
|
|
162
|
-
}
|
|
163
|
-
return createTextGraphic(this, "chartSubtitle", layout.subtitle)
|
|
164
|
-
.editSubtitleText();
|
|
165
|
-
}
|
|
166
|
-
);
|
|
117
|
+
export const editTitleText = titleTextAction("title", false);
|
|
118
|
+
export const createTitleText = titleTextAction("title", true);
|
|
119
|
+
export const editSubtitleText = titleTextAction("subtitle", false);
|
|
120
|
+
export const createSubtitleText = titleTextAction("subtitle", true);
|
|
167
121
|
|
|
168
122
|
export const rematerializeTitle = action(
|
|
169
123
|
{ op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
|
|
@@ -252,29 +252,20 @@ export function resolveTitleLayout(program, config) {
|
|
|
252
252
|
)
|
|
253
253
|
}, config);
|
|
254
254
|
const horizontal = ["top", "bottom"].includes(config.position);
|
|
255
|
-
let
|
|
256
|
-
let subtitle;
|
|
255
|
+
let component;
|
|
257
256
|
if (horizontal) {
|
|
258
257
|
const x = alignedTextAnchor(plot.x, plot.width, config.align);
|
|
259
258
|
const blockTop = config.position === "top"
|
|
260
259
|
? 16 + config.offset
|
|
261
260
|
: plot.y + plot.height + config.offset;
|
|
262
|
-
|
|
263
|
-
lines
|
|
261
|
+
component = (lines, centers) => ({
|
|
262
|
+
lines,
|
|
264
263
|
x,
|
|
265
|
-
y:
|
|
264
|
+
y: centers.map(value => blockTop + value),
|
|
266
265
|
textAlign: config.align,
|
|
267
266
|
rotation: 0,
|
|
268
|
-
explicitRotation: config.position !== "top" ||
|
|
269
|
-
};
|
|
270
|
-
subtitle = block.subtitleLines.length === 0 ? undefined : {
|
|
271
|
-
lines: block.subtitleLines,
|
|
272
|
-
x,
|
|
273
|
-
y: block.subtitleCenters.map(value => blockTop + value),
|
|
274
|
-
textAlign: config.align,
|
|
275
|
-
rotation: 0,
|
|
276
|
-
explicitRotation: config.position !== "top" || block.subtitleLines.length > 1
|
|
277
|
-
};
|
|
267
|
+
explicitRotation: config.position !== "top" || lines.length > 1
|
|
268
|
+
});
|
|
278
269
|
} else {
|
|
279
270
|
const rotation = config.position === "left" ? -Math.PI / 2 : Math.PI / 2;
|
|
280
271
|
const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align);
|
|
@@ -282,23 +273,19 @@ export function resolveTitleLayout(program, config) {
|
|
|
282
273
|
? 16 + config.offset
|
|
283
274
|
: canvas.properties.width - 16 + config.offset;
|
|
284
275
|
const mapX = value => config.position === "left" ? edge + value : edge - value;
|
|
285
|
-
|
|
286
|
-
lines
|
|
287
|
-
x:
|
|
288
|
-
y,
|
|
289
|
-
textAlign: "center",
|
|
290
|
-
rotation,
|
|
291
|
-
explicitRotation: true
|
|
292
|
-
};
|
|
293
|
-
subtitle = block.subtitleLines.length === 0 ? undefined : {
|
|
294
|
-
lines: block.subtitleLines,
|
|
295
|
-
x: block.subtitleCenters.map(mapX),
|
|
276
|
+
component = (lines, centers) => ({
|
|
277
|
+
lines,
|
|
278
|
+
x: centers.map(mapX),
|
|
296
279
|
y,
|
|
297
280
|
textAlign: "center",
|
|
298
281
|
rotation,
|
|
299
282
|
explicitRotation: true
|
|
300
|
-
};
|
|
283
|
+
});
|
|
301
284
|
}
|
|
285
|
+
const title = component(block.titleLines, block.titleCenters);
|
|
286
|
+
const subtitle = block.subtitleLines.length === 0
|
|
287
|
+
? undefined
|
|
288
|
+
: component(block.subtitleLines, block.subtitleCenters);
|
|
302
289
|
const titleBounds = unionTitleBounds([
|
|
303
290
|
resolveTitleComponentBounds(title, config.titleStyle),
|
|
304
291
|
...(subtitle === undefined
|
|
@@ -27,6 +27,8 @@ const AREA_OPTIONS = Object.freeze([
|
|
|
27
27
|
"fill", "opacity", "stroke", "strokeWidth", "curve"
|
|
28
28
|
]);
|
|
29
29
|
const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
|
|
30
|
+
const OPERATION = "createViolinPlot";
|
|
31
|
+
const ROLE_ERROR = `${OPERATION} requires one categorical axis and one quantitative axis.`;
|
|
30
32
|
|
|
31
33
|
function inferFieldType(dataset, encoding, label) {
|
|
32
34
|
if (encoding.fieldType !== undefined) return encoding.fieldType;
|
|
@@ -51,29 +53,6 @@ function normalizePosition(dataset, value, label) {
|
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
function normalizeDensity(value) {
|
|
55
|
-
if (value === undefined) return { options: {}, width: undefined };
|
|
56
|
-
validateOptionObject(value, DENSITY_OPTIONS, "createViolinPlot density");
|
|
57
|
-
if (value.width !== undefined) {
|
|
58
|
-
validateOptionObject(
|
|
59
|
-
value.width,
|
|
60
|
-
WIDTH_OPTIONS,
|
|
61
|
-
"createViolinPlot density.width"
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
const { width, ...options } = value;
|
|
65
|
-
return { options, width };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeSplit(value) {
|
|
69
|
-
if (value === undefined) return undefined;
|
|
70
|
-
validateOptionObject(value, SPLIT_OPTIONS, "createViolinPlot split");
|
|
71
|
-
if (typeof value.field !== "string" || value.field.length === 0) {
|
|
72
|
-
throw new TypeError("createViolinPlot split.field must be a non-empty string.");
|
|
73
|
-
}
|
|
74
|
-
return { ...value };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
56
|
function axisGuideOptions(value, field) {
|
|
78
57
|
if (value === false) return false;
|
|
79
58
|
return {
|
|
@@ -109,55 +88,69 @@ export const createViolinPlot = action(
|
|
|
109
88
|
description: "Create a categorical kernel-density violin plot."
|
|
110
89
|
},
|
|
111
90
|
function (args = {}) {
|
|
112
|
-
validateFacadeOptions(args, OPTIONS,
|
|
91
|
+
validateFacadeOptions(args, OPTIONS, OPERATION);
|
|
113
92
|
const id = resolveFacadeId(this, args.id, {
|
|
114
93
|
defaultId: "violinPlot",
|
|
115
|
-
operation:
|
|
94
|
+
operation: OPERATION
|
|
116
95
|
});
|
|
117
|
-
const data = resolveFacadeData(this, args.data,
|
|
96
|
+
const data = resolveFacadeData(this, args.data, OPERATION);
|
|
118
97
|
const dataset = findDataset(this, data);
|
|
119
|
-
const x = normalizePosition(dataset, args.x,
|
|
120
|
-
const y = normalizePosition(dataset, args.y,
|
|
98
|
+
const x = normalizePosition(dataset, args.x, `${OPERATION} x`);
|
|
99
|
+
const y = normalizePosition(dataset, args.y, `${OPERATION} y`);
|
|
121
100
|
const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
|
|
122
101
|
const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
|
|
123
102
|
if (xCategorical === yCategorical || (
|
|
124
103
|
x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
|
|
125
104
|
)) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
"createViolinPlot requires one categorical axis and one quantitative axis."
|
|
128
|
-
);
|
|
105
|
+
throw new Error(ROLE_ERROR);
|
|
129
106
|
}
|
|
130
107
|
const category = xCategorical ? x : y;
|
|
131
108
|
const value = xCategorical ? y : x;
|
|
132
109
|
if (value.fieldType !== "quantitative") {
|
|
133
|
-
throw new Error(
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
throw new Error(ROLE_ERROR);
|
|
111
|
+
}
|
|
112
|
+
let split = args.split;
|
|
113
|
+
if (split !== undefined) {
|
|
114
|
+
validateOptionObject(split, SPLIT_OPTIONS, `${OPERATION} split`);
|
|
115
|
+
if (typeof split.field !== "string" || split.field.length === 0) {
|
|
116
|
+
throw new TypeError(`${OPERATION} split.field must be a non-empty string.`);
|
|
117
|
+
}
|
|
118
|
+
split = { ...split };
|
|
136
119
|
}
|
|
137
|
-
const split = normalizeSplit(args.split);
|
|
138
120
|
if (split?.field === category.field) {
|
|
139
|
-
throw new Error(
|
|
121
|
+
throw new Error(`${OPERATION} split field must differ from its category field.`);
|
|
140
122
|
}
|
|
141
|
-
const color = normalizeEncoding(args.color,
|
|
123
|
+
const color = normalizeEncoding(args.color, `${OPERATION} color`);
|
|
142
124
|
if (
|
|
143
125
|
color !== undefined &&
|
|
144
126
|
![category.field, split?.field].includes(color.field)
|
|
145
127
|
) {
|
|
146
128
|
throw new Error(
|
|
147
|
-
|
|
129
|
+
`${OPERATION} color must encode its category or split field.`
|
|
148
130
|
);
|
|
149
131
|
}
|
|
150
|
-
const density =
|
|
132
|
+
const density = args.density === undefined ? {} : args.density;
|
|
133
|
+
if (args.density !== undefined) {
|
|
134
|
+
validateOptionObject(density, DENSITY_OPTIONS, `${OPERATION} density`);
|
|
135
|
+
if (density.width !== undefined) {
|
|
136
|
+
validateOptionObject(
|
|
137
|
+
density.width,
|
|
138
|
+
WIDTH_OPTIONS,
|
|
139
|
+
`${OPERATION} density.width`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const { width: densityWidth, ...densityOptions } = density;
|
|
151
144
|
const area = normalizeAppearance(
|
|
152
145
|
args.area,
|
|
153
146
|
AREA_OPTIONS,
|
|
154
|
-
|
|
147
|
+
`${OPERATION} area`
|
|
155
148
|
);
|
|
156
149
|
if (color !== undefined && Object.hasOwn(area, "fill")) {
|
|
157
|
-
throw new Error(
|
|
150
|
+
throw new Error(`${OPERATION} area.fill cannot be combined with color.`);
|
|
158
151
|
}
|
|
159
152
|
const guides = guideOptions(
|
|
160
|
-
normalizeGuides(args.guides,
|
|
153
|
+
normalizeGuides(args.guides, OPERATION),
|
|
161
154
|
x,
|
|
162
155
|
y,
|
|
163
156
|
category,
|
|
@@ -184,10 +177,10 @@ export const createViolinPlot = action(
|
|
|
184
177
|
densityChannel: xCategorical ? "x" : "y",
|
|
185
178
|
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
186
179
|
...(value.scale === undefined ? {} : { valueScale: value.scale }),
|
|
187
|
-
...
|
|
180
|
+
...densityOptions,
|
|
188
181
|
placement: {
|
|
189
182
|
type: "category",
|
|
190
|
-
...(
|
|
183
|
+
...(densityWidth === undefined ? {} : { width: densityWidth }),
|
|
191
184
|
...(split === undefined ? {} : { split }),
|
|
192
185
|
...(category.scale === undefined ? {} : { scale: category.scale })
|
|
193
186
|
}
|
package/src/core/textMetrics.js
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
1
1
|
function codePointWidth(codePoint) {
|
|
2
2
|
if (/\s/u.test(codePoint)) return 0.28;
|
|
3
|
-
if (/[
|
|
3
|
+
if (/[iIl.,:;!'|]/u.test(codePoint)) return 0.27;
|
|
4
4
|
if (/[mwMW@#%&]/u.test(codePoint)) return 0.82;
|
|
5
|
-
if (/[A-
|
|
6
|
-
if (/[0-9]/u.test(codePoint)) return 0.53;
|
|
5
|
+
if (/[A-Z0-9]/u.test(codePoint)) return 0.61;
|
|
7
6
|
if (/[-_]/u.test(codePoint)) return 0.34;
|
|
8
7
|
if (codePoint.codePointAt(0) > 0x7f) return 1;
|
|
9
8
|
return 0.47;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
export function
|
|
11
|
+
export function formatVisibleText(value) {
|
|
12
|
+
return String(value) || "(empty)";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function textBoundsIntersect(first, second) {
|
|
16
|
+
return first.left < second.right && first.right > second.left &&
|
|
17
|
+
first.top < second.bottom && first.bottom > second.top;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function textBoundsFitCanvas(bounds, canvas) {
|
|
21
|
+
const epsilon = 1e-9;
|
|
22
|
+
return bounds.left >= -epsilon && bounds.right <= canvas.width + epsilon &&
|
|
23
|
+
bounds.top >= -epsilon && bounds.bottom <= canvas.height + epsilon;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function measureTextWidth(
|
|
27
|
+
text,
|
|
28
|
+
{ fontSize, fontFamily, fontWeight } = {}
|
|
29
|
+
) {
|
|
13
30
|
if (typeof text !== "string") {
|
|
14
31
|
throw new TypeError("Text measurement requires a string.");
|
|
15
32
|
}
|
|
16
33
|
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
17
34
|
throw new RangeError("Text measurement requires a positive fontSize.");
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
let joined = false;
|
|
37
|
+
let width = 0;
|
|
38
|
+
for (const codePoint of text) {
|
|
39
|
+
if (codePoint === "\u200d") {
|
|
40
|
+
joined = true;
|
|
41
|
+
} else if (!/\p{Mark}|\p{Format}|\p{Emoji_Modifier}/u.test(codePoint)) {
|
|
42
|
+
if (joined) joined = false;
|
|
43
|
+
else width += codePointWidth(codePoint) * fontSize;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const family = typeof fontFamily === "string" ? fontFamily.toLowerCase() : "";
|
|
47
|
+
const familyFactor = /mono/u.test(family)
|
|
48
|
+
? 1.08
|
|
49
|
+
: /serif/u.test(family) && !/sans-serif/u.test(family) ? 1.03 : 1;
|
|
50
|
+
const weight = Number(fontWeight);
|
|
51
|
+
width *= familyFactor * ((
|
|
52
|
+
Number.isFinite(weight)
|
|
53
|
+
? weight >= 600
|
|
54
|
+
: /bold|black|heavy/iu.test(fontWeight ?? "")
|
|
55
|
+
) ? 1.04 : 1);
|
|
56
|
+
if (!Number.isFinite(width)) {
|
|
57
|
+
throw new RangeError("Text measurement exceeds the finite numeric range.");
|
|
58
|
+
}
|
|
59
|
+
return width;
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
62
|
+
export function requireFiniteBounds(bounds, label = "Graphic") {
|
|
63
|
+
if (bounds !== undefined && !Object.values(bounds).every(Number.isFinite)) {
|
|
64
|
+
throw new RangeError(`${label} bounds exceed the finite numeric range.`);
|
|
65
|
+
}
|
|
66
|
+
return bounds;
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
export function resolveTextBounds({
|
|
@@ -40,6 +71,8 @@ export function resolveTextBounds({
|
|
|
40
71
|
y,
|
|
41
72
|
text,
|
|
42
73
|
fontSize,
|
|
74
|
+
fontFamily,
|
|
75
|
+
fontWeight,
|
|
43
76
|
textAlign = "left",
|
|
44
77
|
textBaseline = "alphabetic",
|
|
45
78
|
rotation = 0
|
|
@@ -47,24 +80,28 @@ export function resolveTextBounds({
|
|
|
47
80
|
if (![x, y, rotation].every(Number.isFinite)) {
|
|
48
81
|
throw new TypeError("Text bounds require finite x, y, and rotation values.");
|
|
49
82
|
}
|
|
50
|
-
const width = measureTextWidth(text, { fontSize });
|
|
51
|
-
const [left, right] =
|
|
52
|
-
|
|
83
|
+
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight });
|
|
84
|
+
const [left, right] = textAlign === "center"
|
|
85
|
+
? [-width / 2, width / 2]
|
|
86
|
+
: ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
|
|
87
|
+
const [top, bottom] = textBaseline === "middle"
|
|
88
|
+
? [-fontSize / 2, fontSize / 2]
|
|
89
|
+
: ["top", "hanging"].includes(textBaseline)
|
|
90
|
+
? [0, fontSize]
|
|
91
|
+
: ["bottom", "ideographic"].includes(textBaseline)
|
|
92
|
+
? [-fontSize, 0]
|
|
93
|
+
: [-fontSize * 0.8, fontSize * 0.2];
|
|
53
94
|
const cosine = Math.cos(rotation);
|
|
54
95
|
const sine = Math.sin(rotation);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
right: Math.max(...corners.map(point => point.x)),
|
|
67
|
-
top: Math.min(...corners.map(point => point.y)),
|
|
68
|
-
bottom: Math.max(...corners.map(point => point.y))
|
|
69
|
-
});
|
|
96
|
+
const bounds = {
|
|
97
|
+
left: x + Math.min(left * cosine, right * cosine) -
|
|
98
|
+
Math.max(top * sine, bottom * sine),
|
|
99
|
+
right: x + Math.max(left * cosine, right * cosine) -
|
|
100
|
+
Math.min(top * sine, bottom * sine),
|
|
101
|
+
top: y + Math.min(left * sine, right * sine) +
|
|
102
|
+
Math.min(top * cosine, bottom * cosine),
|
|
103
|
+
bottom: y + Math.max(left * sine, right * sine) +
|
|
104
|
+
Math.max(top * cosine, bottom * cosine)
|
|
105
|
+
};
|
|
106
|
+
return Object.freeze(requireFiniteBounds(bounds, "Text"));
|
|
70
107
|
}
|
package/src/core/validation.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { isPlainObject } from "./immutable.js";
|
|
2
2
|
|
|
3
|
+
export const MAX_GENERATED_ITEMS = 10_000;
|
|
4
|
+
export const MAX_WORK_ITEMS = 10_000_000;
|
|
5
|
+
|
|
6
|
+
export function validateGeneratedItemLimit(
|
|
7
|
+
value,
|
|
8
|
+
label,
|
|
9
|
+
maximum = MAX_GENERATED_ITEMS
|
|
10
|
+
) {
|
|
11
|
+
if (value > maximum) {
|
|
12
|
+
throw new RangeError(`${label} must not exceed ${maximum}.`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateWorkLimit(value, label) {
|
|
18
|
+
return validateGeneratedItemLimit(value, label, MAX_WORK_ITEMS);
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export function validateKeys(value, supported, label) {
|
|
4
22
|
for (const key of Object.keys(value)) {
|
|
5
23
|
if (!supported.includes(key)) {
|
package/src/core/vocabulary.js
CHANGED
|
@@ -86,14 +86,14 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
|
|
|
86
86
|
role: "offset",
|
|
87
87
|
scaleChannel: "xOffset",
|
|
88
88
|
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
89
|
-
markTypes: Object.freeze(["bar"])
|
|
89
|
+
markTypes: Object.freeze(["bar", "point", "rule"])
|
|
90
90
|
}),
|
|
91
91
|
yOffset: Object.freeze({
|
|
92
92
|
family: "cartesian",
|
|
93
93
|
role: "offset",
|
|
94
94
|
scaleChannel: "yOffset",
|
|
95
95
|
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
96
|
-
markTypes: Object.freeze(["bar"])
|
|
96
|
+
markTypes: Object.freeze(["bar", "point", "rule"])
|
|
97
97
|
}),
|
|
98
98
|
theta: Object.freeze({
|
|
99
99
|
family: "polar",
|