ggaction 0.0.21 → 0.0.23
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 +90 -0
- package/README.md +83 -90
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +11 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +1 -0
- package/src/actions/boxPlots/components.js +1 -96
- package/src/actions/boxPlots/create.js +6 -1
- package/src/actions/boxPlots/edit.js +8 -0
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +1 -1
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +2 -1
- package/src/actions/canvas/plotOrder.js +14 -0
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +4 -4
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +1 -1
- package/src/actions/charts/endpoints.js +54 -5
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +1 -1
- package/src/actions/charts/index.js +2 -1
- package/src/actions/charts/interval-regression.js +3 -3
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/radar.js +63 -3
- package/src/actions/charts/raincloud.js +37 -6
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +3 -3
- package/src/actions/charts/text.js +1 -1
- package/src/actions/composition/actions.js +10 -3
- package/src/actions/coordinates/actions.js +2 -0
- package/src/actions/data/basic.js +6 -0
- package/src/actions/data/derived.js +6 -2
- package/src/actions/data/edit.js +22 -36
- package/src/actions/data/filter.js +1 -376
- package/src/actions/data/flatten.js +19 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/lookup.js +50 -0
- package/src/actions/data/raincloudFilter.js +56 -0
- package/src/actions/data/shared.js +10 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/distributions/revision.js +3 -1
- package/src/actions/encodings/angle.js +2 -1
- package/src/actions/encodings/appearance.js +38 -13
- package/src/actions/encodings/channels.js +14 -3
- package/src/actions/encodings/color/continuous.js +26 -10
- package/src/actions/encodings/color/index.js +50 -20
- package/src/actions/encodings/color/policy.js +57 -4
- package/src/actions/encodings/parallel.js +4 -0
- package/src/actions/encodings/position/policies/bar.js +10 -6
- package/src/actions/encodings/position/policies/line.js +1 -152
- package/src/actions/encodings/position/resolve.js +4 -1
- package/src/actions/encodings/ranged.js +46 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/encodings/ruleAppearance.js +7 -6
- package/src/actions/encodings/shared.js +14 -2
- package/src/actions/encodings/stroke.js +20 -14
- package/src/actions/encodings/strokeDash.js +9 -11
- package/src/actions/encodings/text.js +30 -2
- package/src/actions/errorBars/edit.js +24 -4
- package/src/actions/facets/actions.js +1 -793
- package/src/actions/facets/derive.js +34 -5
- package/src/actions/facets/replay.js +1 -38
- package/src/actions/gradientPlots/components.js +21 -9
- package/src/actions/gradientPlots/create.js +4 -2
- package/src/actions/gradientPlots/edit.js +18 -1
- package/src/actions/gradientPlots/materialize.js +35 -36
- package/src/actions/gradientPlots/options.js +1 -1
- package/src/actions/gradientPlots/paint.js +13 -7
- package/src/actions/guides/applicability.js +2 -0
- package/src/actions/guides/axes/axes.js +39 -0
- package/src/actions/guides/axes/axis.js +9 -5
- package/src/actions/guides/axes/edit.js +11 -8
- package/src/actions/guides/axes/labels.js +24 -8
- package/src/actions/guides/axes/parallel/policy.js +3 -5
- package/src/actions/guides/axes/parallel/resolve.js +4 -3
- package/src/actions/guides/axes/policy.js +22 -0
- package/src/actions/guides/axes/tickGroups.js +4 -2
- package/src/actions/guides/axes/ticks.js +22 -4
- package/src/actions/guides/axes/titles.js +19 -6
- package/src/actions/guides/facadeAxes.js +3 -0
- package/src/actions/guides/grids/grid.js +3 -1
- package/src/actions/guides/grids/resolve.js +7 -11
- package/src/actions/guides/guides.js +21 -1
- package/src/actions/guides/legends/categorical/actions.js +1 -515
- package/src/actions/guides/legends/categorical/components.js +1 -4
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +4 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -3
- package/src/actions/guides/legends/categorical/symbols.js +3 -3
- package/src/actions/guides/legends/colorSize.js +2 -0
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -360
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +15 -1
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/sampling.js +1 -140
- package/src/actions/guides/legends/strokeWidth.js +3 -2
- package/src/actions/guides/legends/transition.js +19 -2
- package/src/actions/guides/polar/axes/facade.js +5 -4
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/resolve.js +2 -1
- package/src/actions/guides/tickValues.js +7 -7
- package/src/actions/marks/arc/actions.js +5 -7
- package/src/actions/marks/area/actions.js +9 -3
- package/src/actions/marks/bar/create.js +4 -2
- package/src/actions/marks/bar/edit.js +9 -6
- package/src/actions/marks/bar/materialize.js +5 -3
- package/src/actions/marks/dependentReferences.js +87 -0
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/lifecycle.js +2 -1
- package/src/actions/marks/line/actions.js +24 -6
- package/src/actions/marks/line/materialize.js +7 -3
- package/src/actions/marks/point/create.js +5 -4
- package/src/actions/marks/point/edit.js +6 -3
- package/src/actions/marks/point/jitter.js +6 -6
- package/src/actions/marks/point/materialize.js +36 -39
- package/src/actions/marks/rect/actions.js +16 -4
- package/src/actions/marks/references.js +183 -35
- package/src/actions/marks/rule/actions.js +20 -9
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +10 -3
- package/src/actions/marks/tick/actions.js +11 -2
- package/src/actions/primitives/editGraphics.js +28 -3
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semanticAction.js +13 -7
- package/src/actions/primitives/semanticValidation/layer.js +20 -0
- package/src/actions/primitives/semanticValidation/scale.js +8 -1
- package/src/actions/regression/create.js +6 -1
- package/src/actions/regression/edit.js +10 -1
- package/src/actions/scales/channels.js +1 -1
- package/src/actions/scales/consumers/common.js +19 -1
- package/src/actions/scales/consumers/families.js +2 -2
- package/src/actions/scales/consumers/seriesLayout.js +4 -3
- package/src/actions/scales/edit.js +11 -0
- package/src/actions/scales/editPolicy.js +5 -3
- package/src/actions/scales/preview.js +2 -1
- package/src/actions/scales/quantitativeColor.js +2 -4
- package/src/actions/selection/actions.js +10 -1
- package/src/actions/theme/reconcile.js +37 -9
- package/src/core/compositionState.js +26 -7
- package/src/core/datasetReferences.js +17 -0
- package/src/core/facetAxisPolicy.js +30 -0
- package/src/core/immutable.js +1 -0
- package/src/core/resourceReferences.js +5 -3
- package/src/core/validation.js +4 -0
- package/src/core/vocabulary.js +24 -2
- package/src/grammar/aggregate.js +1 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bars/policy.js +2 -2
- package/src/grammar/curveCommands.js +35 -2
- package/src/grammar/datasetDependencies.js +97 -0
- package/src/grammar/datasetSchema.js +32 -2
- package/src/grammar/facetHeaders.js +1 -0
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +7 -5
- package/src/grammar/facets/legendIntent.js +6 -2
- package/src/grammar/facets/scales.js +27 -7
- package/src/grammar/flatten.js +85 -0
- package/src/grammar/histogram.js +13 -1
- package/src/grammar/horizon.js +1 -494
- package/src/grammar/jitter.js +2 -2
- package/src/grammar/legendSpacing.js +45 -0
- package/src/grammar/lineMissing.js +90 -0
- package/src/grammar/lineSeries.js +1 -376
- package/src/grammar/lookup.js +97 -0
- package/src/grammar/markFilter.js +4 -2
- package/src/grammar/markLabels.js +8 -3
- package/src/grammar/numeric.js +1 -1
- package/src/grammar/pathCommands.js +1 -1
- package/src/grammar/pathSeries.js +1 -109
- package/src/grammar/pointPolygon.js +81 -0
- package/src/grammar/pointShapes.js +15 -15
- package/src/grammar/rects.js +1 -1
- package/src/grammar/regression/derive.js +1 -195
- package/src/grammar/regression/parameters.js +7 -0
- package/src/grammar/scales/appearance.js +1 -1
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -63
- package/src/grammar/scales/continuous.js +58 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +3 -3
- package/src/grammar/schemas/graphicBounds.js +40 -12
- package/src/grammar/schemas/graphicClip.js +23 -0
- package/src/grammar/schemas/graphicTree.js +16 -0
- package/src/grammar/statisticalReference.js +22 -0
- package/src/grammar/text.js +65 -1
- package/src/grammar/ticks.js +92 -1
- package/src/grammar/timeUnit.js +9 -3
- package/src/grammar/transformTopology.js +3 -1
- package/src/grammar/transforms.js +24 -0
- package/src/inspection.js +1 -495
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -1
- package/src/layout/facetContentFlow.js +101 -0
- package/src/layout/facetHeaderTitles.js +1 -0
- package/src/layout/facetTopLane.js +29 -0
- package/src/layout/facets.js +1 -1
- package/src/layout/guideCollisions.js +9 -3
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +26 -10
- package/src/layout/legendSpacing.js +66 -0
- package/src/layout/plotDrawOrder.js +17 -0
- package/src/materialization/bars/histogram.js +8 -2
- package/src/materialization/bars/resolve.js +1 -1
- package/src/materialization/composition.js +1 -0
- package/src/materialization/compositionSnapshot.js +22 -2
- package/src/materialization/dataProvenance.js +1 -1
- package/src/materialization/facetContentBounds.js +96 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +39 -11
- package/src/materialization/facetGuides/preparation.js +43 -24
- package/src/materialization/facetHeaders.js +1 -1
- package/src/materialization/facetTopLane.js +25 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +30 -1
- package/src/materialization/graphicTranslation.js +2 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/markClip.js +26 -0
- package/src/materialization/marks/capabilities.js +3 -2
- package/src/materialization/marks/grouping.js +7 -5
- package/src/materialization/marks/index.js +4 -2
- package/src/materialization/marks/policies.js +4 -0
- package/src/materialization/plotDrawOrder.js +44 -0
- package/src/materialization/rect.js +11 -3
- package/src/materialization/scaleGuideDependencies.js +13 -5
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/policies/index.js +2 -1
- package/src/materialization/text.js +10 -5
- package/src/mcp/cli.js +0 -0
- package/src/persistence/validation.js +114 -18
- package/src/persistence.js +4 -2
- package/src/renderers/canvas/index.js +16 -0
- package/src/renderers/canvas/isolated.js +11 -1
- package/src/renderers/canvas/native.js +7 -0
- package/src/renderers/canvas/scene.js +66 -0
- package/src/renderers/canvas/text.js +3 -2
- package/src/renderers/svg.js +1 -617
- package/src/renderers/svgSerialization.js +643 -0
- package/src/renderers/transitionScene.js +67 -0
- package/src/scene.js +89 -0
- package/src/selectors/axes.js +1 -1
- package/src/selectors/datasets.js +1 -0
- package/src/selectors/grids.js +9 -0
- package/src/selectors/markClip.js +28 -0
- package/src/selectors/raincloud.js +7 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +7 -0
- package/types/index.d.ts +8 -0
- package/types/program.d.ts +286 -70
- package/types/scene.d.ts +35 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cartesianAxisOwners, axisGraphicIds, PARALLEL_AXIS_GRAPHICS } from "../selectors/axes.js";
|
|
2
|
+
import { gridNames } from "../selectors/grids.js";
|
|
3
|
+
import { resolvePlotDrawOrder } from "../layout/plotDrawOrder.js";
|
|
4
|
+
|
|
5
|
+
export function hasPlotUnderlays(program) {
|
|
6
|
+
return program.compositionSpec === undefined && (
|
|
7
|
+
program.materializationConfigs.canvas?.plotBorder?.underlay === true ||
|
|
8
|
+
cartesianAxisOwners(program).some(owner => owner.config?.frame?.underlay === true)
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function plotDrawOrderResources(program) {
|
|
13
|
+
const axes = cartesianAxisOwners(program).map(owner => ({
|
|
14
|
+
ids: axisGraphicIds(owner.channel, owner.id), underlay: owner.config?.frame?.underlay === true
|
|
15
|
+
}));
|
|
16
|
+
for (const channel of ["theta", "radius"]) if (program.semanticSpec.guides.axis?.[channel] !== undefined) {
|
|
17
|
+
axes.push({ ids: axisGraphicIds(channel), underlay: false });
|
|
18
|
+
}
|
|
19
|
+
if (program.semanticSpec.guides.axis?.parallel !== undefined) axes.push({ ids: Object.values(PARALLEL_AXIS_GRAPHICS), underlay: false });
|
|
20
|
+
return {
|
|
21
|
+
children: program.graphicSpec.objects["plot-main"]?.children ?? [],
|
|
22
|
+
grids: ["horizontal", "vertical"].flatMap((direction, index) => {
|
|
23
|
+
const config = program.guideConfigs.grid?.[direction];
|
|
24
|
+
return config === undefined ? [] : [{ id: gridNames(direction).graphic, zIndex: config.zIndex, index }];
|
|
25
|
+
}),
|
|
26
|
+
border: program.materializationConfigs.canvas?.plotBorder === undefined ? undefined : {
|
|
27
|
+
id: "plotBorder", underlay: program.materializationConfigs.canvas.plotBorder.underlay === true
|
|
28
|
+
}, axes
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveProgramPlotDrawOrder(program) {
|
|
33
|
+
return resolvePlotDrawOrder(plotDrawOrderResources(program));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function firstOverlayGuide(program) {
|
|
37
|
+
const { children, grids, border, axes } = plotDrawOrderResources(program);
|
|
38
|
+
const overlay = new Set([
|
|
39
|
+
...grids.filter(grid => grid.zIndex > 0).map(grid => grid.id),
|
|
40
|
+
...(border?.underlay === false ? [border.id] : []),
|
|
41
|
+
...axes.filter(axis => !axis.underlay).flatMap(axis => axis.ids)
|
|
42
|
+
]);
|
|
43
|
+
return children.find(id => overlay.has(id));
|
|
44
|
+
}
|
|
@@ -27,7 +27,7 @@ function requiredChannels(layer, mode) {
|
|
|
27
27
|
return [
|
|
28
28
|
...position,
|
|
29
29
|
...(layer.encoding?.color ? ["color"] : []),
|
|
30
|
-
...
|
|
30
|
+
...["stroke", "opacity", "strokeWidth"].filter(channel => layer.encoding?.[channel])
|
|
31
31
|
];
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -96,6 +96,10 @@ export function resolveRectRows(program, layer, dataset) {
|
|
|
96
96
|
const stroke = layer.encoding?.stroke === undefined
|
|
97
97
|
? undefined
|
|
98
98
|
: mappedEncoding(program, layer, dataset, "stroke").values;
|
|
99
|
+
const opacity = layer.encoding?.opacity === undefined ? undefined
|
|
100
|
+
: mappedEncoding(program, layer, dataset, "opacity").values;
|
|
101
|
+
const strokeWidth = layer.encoding?.strokeWidth === undefined ? undefined
|
|
102
|
+
: mappedEncoding(program, layer, dataset, "strokeWidth").values;
|
|
99
103
|
const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
|
|
100
104
|
|
|
101
105
|
const rows = rectUsesFields(layer) ? dataset.values : [{}];
|
|
@@ -105,7 +109,9 @@ export function resolveRectRows(program, layer, dataset) {
|
|
|
105
109
|
(x !== undefined && !Number.isFinite(x.values[index])) ||
|
|
106
110
|
(y !== undefined && !Number.isFinite(y.values[index])) ||
|
|
107
111
|
typeof fill !== "string" ||
|
|
108
|
-
(stroke !== undefined && typeof stroke[index] !== "string")
|
|
112
|
+
(stroke !== undefined && typeof stroke[index] !== "string") ||
|
|
113
|
+
(opacity !== undefined && !Number.isFinite(opacity[index])) ||
|
|
114
|
+
(strokeWidth !== undefined && !Number.isFinite(strokeWidth[index]))
|
|
109
115
|
) return [];
|
|
110
116
|
|
|
111
117
|
let geometry;
|
|
@@ -138,7 +144,9 @@ export function resolveRectRows(program, layer, dataset) {
|
|
|
138
144
|
return [{
|
|
139
145
|
row,
|
|
140
146
|
sourceIndex: index,
|
|
141
|
-
properties: { ...geometry, ...appearance(config, fill, stroke?.[index])
|
|
147
|
+
properties: { ...geometry, ...appearance(config, fill, stroke?.[index]),
|
|
148
|
+
...(opacity === undefined ? {} : { opacity: opacity[index] }),
|
|
149
|
+
...(strokeWidth === undefined ? {} : { strokeWidth: strokeWidth[index] }) }
|
|
142
150
|
}];
|
|
143
151
|
});
|
|
144
152
|
}
|
|
@@ -16,14 +16,22 @@ const GRIDS = Object.freeze([
|
|
|
16
16
|
Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
-
export function planAxisTickGridRematerialization(program, channel, axisId = channel) {
|
|
19
|
+
export function planAxisTickGridRematerialization(program, channel, axisId = channel, { syncLabels = false } = {}) {
|
|
20
20
|
const ticks = program.guideConfigs.axis?.[axisId]?.ticks;
|
|
21
21
|
const role = channel === "x" ? "vertical" : "horizontal";
|
|
22
22
|
const config = program.guideConfigs.grid?.[role];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const labels = syncLabels && program.guideConfigs.axis?.[axisId]?.labels
|
|
24
|
+
? [{ op: `edit${channel.toUpperCase()}AxisLabels`, args: {
|
|
25
|
+
...axisTargetArgs(axisId, channel),
|
|
26
|
+
...(ticks.mode === "values" ? { values: ticks.values } : {
|
|
27
|
+
count: ticks.count,
|
|
28
|
+
...(ticks.rounding === undefined ? {} : { rounding: ticks.rounding })
|
|
29
|
+
})
|
|
30
|
+
} }] : [];
|
|
31
|
+
return buildMaterializationPlan({ guides: [...labels,
|
|
32
|
+
...(config?.inferredValues === true && config.scale === ticks?.scale
|
|
33
|
+
? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : [])
|
|
34
|
+
] });
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
function usesPositionalScale(program, id) {
|
|
@@ -102,7 +102,7 @@ function collectionBounds(program, items) {
|
|
|
102
102
|
const bounds = unionConcreteGraphicBounds(
|
|
103
103
|
program.graphicSpec,
|
|
104
104
|
items.map(item => item.id)
|
|
105
|
-
, program.materializationConfigs.textMetrics);
|
|
105
|
+
, program.materializationConfigs.textMetrics, {includeOwnerClips: false});
|
|
106
106
|
if (bounds === undefined) return {};
|
|
107
107
|
const { left, top, right, bottom } = bounds;
|
|
108
108
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
@@ -8,6 +8,7 @@ import { pointSelectionPolicy } from "./point.js";
|
|
|
8
8
|
import { ruleSelectionPolicy } from "./rule.js";
|
|
9
9
|
import { tickSelectionPolicy } from "./tick.js";
|
|
10
10
|
import { rectSelectionPolicy } from "./rect.js";
|
|
11
|
+
import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
|
|
11
12
|
|
|
12
13
|
const POLICIES = Object.freeze({
|
|
13
14
|
arc: arcSelectionPolicy,
|
|
@@ -42,5 +43,5 @@ export function resolveMarkItems(program, target, grain = "item") {
|
|
|
42
43
|
if (!policy.supportedGrains.includes(grain)) {
|
|
43
44
|
throw new Error(`Mark "${target}" does not support ${grain} selection grain.`);
|
|
44
45
|
}
|
|
45
|
-
return policy.resolveItems(program, layer, dataset, grain);
|
|
46
|
+
return policy.resolveItems(program, layer, applyItemMissingPolicy(layer, dataset), grain);
|
|
46
47
|
}
|
|
@@ -5,11 +5,10 @@ import {
|
|
|
5
5
|
resolveMarkLabelPlacement
|
|
6
6
|
} from "../layout/labels.js";
|
|
7
7
|
import { resolveMarkLabelValues } from "../grammar/markLabels.js";
|
|
8
|
-
import { formatTextValue, resolveTextLines } from "../grammar/text.js";
|
|
8
|
+
import { formatTextValue, resolveTextLines, resolveTextOffsetDataset, textOffsetValue } from "../grammar/text.js";
|
|
9
9
|
import { normalizePositionDatum } from "../grammar/positionDatum.js";
|
|
10
10
|
import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
|
|
11
11
|
import { findDataset } from "../selectors/datasets.js";
|
|
12
|
-
import { applyItemMissingPolicy } from "../grammar/itemMissing.js";
|
|
13
12
|
import { findCoordinate } from "../selectors/coordinates.js";
|
|
14
13
|
import { findLayer } from "../selectors/layers.js";
|
|
15
14
|
import { unionConcreteGraphicBounds } from
|
|
@@ -427,8 +426,9 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
427
426
|
if (sourceDataset === undefined) {
|
|
428
427
|
throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
|
|
429
428
|
}
|
|
430
|
-
const dataset =
|
|
431
|
-
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color
|
|
429
|
+
const dataset = resolveTextOffsetDataset(layer, sourceDataset);
|
|
430
|
+
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color,
|
|
431
|
+
layer.encoding?.dx, layer.encoding?.dy];
|
|
432
432
|
const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
|
|
433
433
|
? dataset.values.length
|
|
434
434
|
: 1;
|
|
@@ -465,7 +465,12 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
465
465
|
const fills = resolveRowEncodingValues(program, layer, dataset, "color");
|
|
466
466
|
return rows.flatMap((row, index) => {
|
|
467
467
|
const concrete = concreteItem(
|
|
468
|
-
|
|
468
|
+
{
|
|
469
|
+
...config,
|
|
470
|
+
...(fills === undefined ? {} : { fill: fills[index] }),
|
|
471
|
+
dx: textOffsetValue(layer.encoding?.dx, row, config.dx),
|
|
472
|
+
dy: textOffsetValue(layer.encoding?.dy, row, config.dy)
|
|
473
|
+
},
|
|
469
474
|
{ x: x[index], y: y[index] },
|
|
470
475
|
contentValue(layer.encoding.text, row),
|
|
471
476
|
layer.encoding.text.format
|
package/src/mcp/cli.js
CHANGED
|
File without changes
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import { validateFacetAxisPolicy } from "../core/facetAxisPolicy.js";
|
|
2
|
+
import { resolveFacetFamily } from "../grammar/facets/dependencies.js";
|
|
3
|
+
import { validateRegressionPlacement } from "../grammar/regression/parameters.js";
|
|
4
|
+
import { validateGraphicClip } from "../grammar/schemas/graphicClip.js";
|
|
5
|
+
import { validatePointShape } from "../grammar/pointShapes.js";
|
|
6
|
+
import {deriveLookupRows,sameLookupValue} from "../grammar/lookup.js";
|
|
7
|
+
import {deriveTransformSchema,datasetSchemaContext} from "../grammar/datasetSchema.js";
|
|
8
|
+
import {orderedDatasetAncestors} from "../grammar/datasetDependencies.js";
|
|
9
|
+
import {validateLegendSpacing} from "../grammar/legendSpacing.js";
|
|
10
|
+
import { requestedMarkClip, validateMarkClipCoordinate } from "../selectors/markClip.js";
|
|
1
11
|
import { validateAuthoredFacetLegend } from "../grammar/facets/legendIntent.js";
|
|
12
|
+
import { validateFacetHeaderRotations } from "../grammar/facetHeaders.js";
|
|
2
13
|
import { AXIS_RESOURCE_CHANNELS } from "../core/vocabulary.js";
|
|
3
14
|
import { cartesianAxisOwners, assertCartesianAxisSideAvailable } from "../selectors/axes.js";
|
|
4
15
|
import { axisGraphicIds } from "../selectors/axes.js";
|
|
@@ -20,16 +31,19 @@ import { validateGraphicItemLimit } from "../core/validation.js";
|
|
|
20
31
|
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
|
|
21
32
|
import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
|
|
22
33
|
import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
|
|
34
|
+
import { validateTickSelectionRounding, axisTickSelection } from "../grammar/ticks.js";
|
|
23
35
|
import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
|
|
24
36
|
import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
|
|
25
37
|
import { validateRegressionFitStatistics, requireRegressionStatisticsSource, regressionStatisticsRows } from "../grammar/regression/statistics.js";
|
|
26
38
|
import { resolveGraphicItemCapacity } from "../selectors/graphicCapacity.js";
|
|
27
39
|
import { isContinuousColorScaleType } from "../grammar/scales/types.js";
|
|
40
|
+
import { validateTickColorChannels } from "../grammar/scales/colorConsumers.js";
|
|
28
41
|
import { normalizeColorInterpolationOptions } from "../grammar/scales/color.js";
|
|
29
42
|
import { normalizeTransformParameters } from "../grammar/scales/transformed.js";
|
|
30
43
|
import { validateLegendGroupState } from "../grammar/legendGroups.js";
|
|
31
44
|
import { normalizeGradientDirection, validateLegendGradient } from "../grammar/legendGradient.js";
|
|
32
45
|
import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
|
|
46
|
+
import { resolveTextOffsetDataset } from "../grammar/text.js";
|
|
33
47
|
import { isPlainObject } from "../core/immutable.js";
|
|
34
48
|
import { validateUserId } from "../core/identifiers.js";
|
|
35
49
|
import { collectAllResourceReferences } from "../core/resourceReferences.js";
|
|
@@ -48,6 +62,21 @@ export const STATE_KEYS = Object.freeze([
|
|
|
48
62
|
"children", "compositionSpec", "context", "trace", "actionStack"
|
|
49
63
|
]);
|
|
50
64
|
|
|
65
|
+
function validateStoredColorLegendSampling(program, config, path) {
|
|
66
|
+
requireObject(config.sampling, path);
|
|
67
|
+
const sample = config.sampling;
|
|
68
|
+
exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
|
|
69
|
+
if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
|
|
70
|
+
if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
|
|
71
|
+
validateGeneratedItemLimit(sample.count, "Color legend sample count");
|
|
72
|
+
const scale = program.resolvedScales[config.scale];
|
|
73
|
+
if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
|
|
74
|
+
if (sample.mode === "values") {
|
|
75
|
+
requireArray(sample.values, path);
|
|
76
|
+
if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
51
80
|
export function requireObject(value, path) {
|
|
52
81
|
if (!isPlainObject(value)) throw invalidSnapshot(`${path} must be a plain object.`, path);
|
|
53
82
|
}
|
|
@@ -79,9 +108,10 @@ export function validateGraphic(graphic, path = "payload.graphicSpec") {
|
|
|
79
108
|
if (!id.length) throw invalidSnapshot("Graphic IDs must be non-empty.", path);
|
|
80
109
|
requireObject(object, `${path}.objects.${id}`);
|
|
81
110
|
validateGraphicType(object.type);
|
|
82
|
-
if (Object.keys(object).some(key => !["type", "properties", "items", "children"].includes(key))) {
|
|
111
|
+
if (Object.keys(object).some(key => !["type", "properties", "items", "children", "clip"].includes(key))) {
|
|
83
112
|
throw invalidSnapshot(`Unknown graphic member at ${id}.`, path);
|
|
84
113
|
}
|
|
114
|
+
if (object.clip !== undefined) validateGraphicClip(object.clip, object.type);
|
|
85
115
|
if (object.properties !== undefined) validateConcreteGraphicProperties(object.type, object.properties);
|
|
86
116
|
if (object.items !== undefined) {
|
|
87
117
|
if (object.type === "canvas" || object.properties !== undefined) throw invalidSnapshot(`Invalid item owner "${id}".`, path);
|
|
@@ -143,7 +173,7 @@ function validateSemanticProperties(program, value, property) {
|
|
|
143
173
|
validateStoredSemanticValue(program, parsed, value);
|
|
144
174
|
}
|
|
145
175
|
|
|
146
|
-
export function validateProgramState(program, path) {
|
|
176
|
+
export function validateProgramState(program, path, { allowMissingLookupSchemas = false } = {}) {
|
|
147
177
|
const semantic = program.semanticSpec;
|
|
148
178
|
exactKeys(semantic, ["datasets", "layers", "scales", "coordinates", "guides", "title"], `${path}.semanticSpec`);
|
|
149
179
|
const resources = new Map();
|
|
@@ -161,6 +191,12 @@ export function validateProgramState(program, path) {
|
|
|
161
191
|
}
|
|
162
192
|
resources.set(referenceKind, entries);
|
|
163
193
|
for (const resource of entries.values()) {
|
|
194
|
+
if (kind === "layer") validateTickColorChannels(resource);
|
|
195
|
+
if (kind === "layer" && ["dx", "dy"].some(channel => resource.encoding?.[channel] !== undefined)) {
|
|
196
|
+
const dataset = semantic.datasets.find(item => item.id === resource.data);
|
|
197
|
+
if (dataset === undefined) throw new Error("Numeric text offsets require an existing dataset.");
|
|
198
|
+
resolveTextOffsetDataset(resource, dataset);
|
|
199
|
+
}
|
|
164
200
|
if (kind === "scale") {
|
|
165
201
|
normalizeColorInterpolationOptions(resource.interpolate, resource);
|
|
166
202
|
if (resource.type === "pow") normalizeTransformParameters("pow", {
|
|
@@ -181,6 +217,15 @@ export function validateProgramState(program, path) {
|
|
|
181
217
|
if (program.compositionSpec?.type === "facet" && isPlainObject(program.compositionSpec.facet.guides.legend)) {
|
|
182
218
|
validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
|
|
183
219
|
}
|
|
220
|
+
if (program.compositionSpec?.type === "facet") {
|
|
221
|
+
validateFacetAxisPolicy(program.compositionSpec.facet.guides.axes,
|
|
222
|
+
resolveFacetFamily(program.semanticSpec).family, program.compositionSpec.facet.repeat !== undefined);
|
|
223
|
+
const headers = program.materializationConfigs.facets?.[program.compositionSpec.id]?.headers;
|
|
224
|
+
if (headers !== undefined) validateFacetHeaderRotations(headers);
|
|
225
|
+
if (headers?.row?.title !== undefined && program.compositionSpec.facet.grid === undefined) {
|
|
226
|
+
throw new TypeError("Facet row field title requires a row-column facet grid.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
184
229
|
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
185
230
|
for (const config of [
|
|
186
231
|
...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
|
|
@@ -189,17 +234,54 @@ export function validateProgramState(program, path) {
|
|
|
189
234
|
if (config?.strokeDash !== undefined) validateGuideDash(config.strokeDash);
|
|
190
235
|
if (config?.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
|
|
191
236
|
}
|
|
192
|
-
|
|
237
|
+
const cartesianOwners = cartesianAxisOwners(program);
|
|
238
|
+
for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
|
|
239
|
+
for (const config of [axis?.ticks, axis?.labels]) {
|
|
240
|
+
if (config === undefined || !Object.hasOwn(config, "rounding")) continue;
|
|
241
|
+
if (!cartesianOwners.some(owner => owner.id === id)) {
|
|
242
|
+
throw invalidSnapshot("Tick rounding requires a Cartesian axis owner.", path);
|
|
243
|
+
}
|
|
244
|
+
validateTickSelectionRounding(config, program.resolvedScales[config.scale]?.type);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const owner of cartesianOwners) {
|
|
193
248
|
const channel = owner.id;
|
|
194
249
|
const axis = owner.config;
|
|
250
|
+
if (axis?.ticks && axis?.labels &&
|
|
251
|
+
(Object.hasOwn(axis.ticks, "rounding") || Object.hasOwn(axis.labels, "rounding")) &&
|
|
252
|
+
JSON.stringify(axisTickSelection(axis.ticks)) !== JSON.stringify(axisTickSelection(axis.labels))) {
|
|
253
|
+
throw invalidSnapshot("Stored tick rounding conflicts with axis labels.", path);
|
|
254
|
+
}
|
|
195
255
|
if (axis?.frame !== undefined) {
|
|
196
|
-
|
|
197
|
-
validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
|
|
256
|
+
validateOptionObject(axis.frame, ["offset", "underlay"], `Stored axis "${channel}" frame`, { allowEmpty: false });
|
|
257
|
+
if (Object.hasOwn(axis.frame, "offset")) validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
|
|
258
|
+
if (Object.hasOwn(axis.frame, "underlay") && axis.frame.underlay !== true) {
|
|
259
|
+
throw new Error("Stored axis underlay must be canonical true intent.");
|
|
260
|
+
}
|
|
198
261
|
}
|
|
199
262
|
if (axis?.labels !== undefined) validateAxisTextPlacement(axis.labels);
|
|
200
263
|
if (axis?.title !== undefined) validateAxisTextPlacement(axis.title, { title: true });
|
|
201
264
|
}
|
|
265
|
+
for (const config of Object.values(program.guideConfigs.grid ?? {})) {
|
|
266
|
+
if (!Object.hasOwn(config, "rounding")) continue;
|
|
267
|
+
validateTickSelectionRounding(config, program.resolvedScales[config.scale]?.type);
|
|
268
|
+
const owners = cartesianOwners.map(owner => owner.config?.ticks)
|
|
269
|
+
.filter(ticks => ticks?.scale === config.scale);
|
|
270
|
+
if (config.inferredValues !== true || config.rounding !== "nearest" || owners.length === 0 ||
|
|
271
|
+
owners.some(ticks => JSON.stringify(axisTickSelection(ticks)) !== JSON.stringify(axisTickSelection(config)))) {
|
|
272
|
+
throw invalidSnapshot("Stored grid rounding requires its matching inferred tick policy.", path);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const border = program.materializationConfigs.canvas?.plotBorder;
|
|
276
|
+
if (isPlainObject(border) && Object.hasOwn(border, "underlay") && border.underlay !== true) {
|
|
277
|
+
throw new Error("Stored plot border underlay must be canonical true intent.");
|
|
278
|
+
}
|
|
202
279
|
for (const id of Object.keys(program.guideConfigs.axis ?? {})) {
|
|
280
|
+
const config = program.guideConfigs.axis[id];
|
|
281
|
+
if (config?.frame !== undefined && Object.hasOwn(config.frame, "underlay") &&
|
|
282
|
+
!cartesianOwners.some(owner => owner.id === id)) {
|
|
283
|
+
throw new Error("Stored axis underlay requires a Cartesian axis owner.");
|
|
284
|
+
}
|
|
203
285
|
if (!AXIS_RESOURCE_CHANNELS.includes(id) && !Object.hasOwn(program.semanticSpec.guides.axis ?? {}, id)) {
|
|
204
286
|
throw invalidSnapshot("Named axis configs require their semantic owner.", path);
|
|
205
287
|
}
|
|
@@ -242,6 +324,7 @@ export function validateProgramState(program, path) {
|
|
|
242
324
|
if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
|
|
243
325
|
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
244
326
|
validateLegendClip(kind,config);
|
|
327
|
+
validateLegendSpacing(kind,config);
|
|
245
328
|
if (localLegendPosition(config) !== undefined) {
|
|
246
329
|
const descriptor = describeLegendBlock(program, kind, config);
|
|
247
330
|
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
@@ -301,22 +384,17 @@ export function validateProgramState(program, path) {
|
|
|
301
384
|
if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
|
|
302
385
|
validateLegendGradient(config.gradient, "Stored legend gradient");
|
|
303
386
|
}
|
|
387
|
+
if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols" && config.sampling !== undefined) {
|
|
388
|
+
validateStoredColorLegendSampling(program, config, path);
|
|
389
|
+
if (config.sampling.mode !== "values" || config.count !== config.sampling.count) {
|
|
390
|
+
throw invalidSnapshot("Gradient exact sampling must retain its automatic count.", path);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
304
393
|
if (config.representation === undefined) continue;
|
|
305
394
|
if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
|
|
306
395
|
if (config.representation === "symbols") {
|
|
307
396
|
if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
|
|
308
|
-
|
|
309
|
-
const sample = config.sampling;
|
|
310
|
-
exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
|
|
311
|
-
if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
|
|
312
|
-
if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
|
|
313
|
-
validateGeneratedItemLimit(sample.count, "Color legend sample count");
|
|
314
|
-
const scale = program.resolvedScales[config.scale];
|
|
315
|
-
if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
|
|
316
|
-
if (sample.mode === "values") {
|
|
317
|
-
requireArray(sample.values, path);
|
|
318
|
-
if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
|
|
319
|
-
}
|
|
397
|
+
validateStoredColorLegendSampling(program, config, path);
|
|
320
398
|
requireObject(config.symbol, path);
|
|
321
399
|
validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
|
|
322
400
|
if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
|
|
@@ -326,7 +404,7 @@ export function validateProgramState(program, path) {
|
|
|
326
404
|
if (!["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"].includes(config.position) || !["horizontal", "vertical"].includes(config.direction) || config.layout !== "edge" || !["left", "center", "right"].includes(config.align) || !["top", "left"].includes(config.titlePosition) || config.columns !== undefined && (!Number.isInteger(config.columns) || config.columns < 1)) throw invalidSnapshot("Invalid sampled color item layout.", path);
|
|
327
405
|
validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
|
|
328
406
|
validateNonNegativeFinite(config.offset, "Stored sampled color offset");
|
|
329
|
-
} else if (["
|
|
407
|
+
} else if (["symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
|
|
330
408
|
}
|
|
331
409
|
|
|
332
410
|
const regressionFits = program.materializationConfigs.regressionFits ?? {};
|
|
@@ -336,6 +414,18 @@ export function validateProgramState(program, path) {
|
|
|
336
414
|
validateRegressionFitStatistics(cache, dataset, resources.get("data").get(dataset?.source));
|
|
337
415
|
}
|
|
338
416
|
for (const dataset of semantic.datasets) {
|
|
417
|
+
if(dataset.transform?.[0]?.type==="lookup"){
|
|
418
|
+
orderedDatasetAncestors(semantic.datasets,dataset.id);
|
|
419
|
+
const transform=dataset.transform[0],source=resources.get("data").get(dataset.source),right=resources.get("data").get(transform.lookup.data);
|
|
420
|
+
if(source===undefined||right===undefined)throw invalidSnapshot("Lookup requires both existing input datasets.",path);
|
|
421
|
+
const missingSchemas = [source, right, dataset].some(input => input.schema === undefined);
|
|
422
|
+
if (!allowMissingLookupSchemas || !missingSchemas) {
|
|
423
|
+
const schema=deriveTransformSchema(source.schema,transform,dataset.values,
|
|
424
|
+
datasetSchemaContext(source,transform,dataset.id,id=>resources.get("data").get(id)));
|
|
425
|
+
if(!sameLookupValue(dataset.schema,schema))throw invalidSnapshot("Lookup schema does not match both inputs.",path);
|
|
426
|
+
}
|
|
427
|
+
if(dataset.values!==undefined&&!sameLookupValue(dataset.values,deriveLookupRows(source.values,right.values,transform)))throw invalidSnapshot("Lookup values do not match both input datasets.",path);
|
|
428
|
+
}
|
|
339
429
|
if (dataset.transform?.[0]?.type !== "regressionStatistics") continue;
|
|
340
430
|
const fit = resources.get("data").get(dataset.source);
|
|
341
431
|
requireRegressionStatisticsSource(fit);
|
|
@@ -378,7 +468,13 @@ export function validateProgramState(program, path) {
|
|
|
378
468
|
}
|
|
379
469
|
}
|
|
380
470
|
for (const id of Object.keys(program.markConfigs)) {
|
|
471
|
+
validateRegressionPlacement(program.markConfigs[id].regression?.placement);
|
|
472
|
+
requestedMarkClip(program.markConfigs[id]);
|
|
473
|
+
validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
|
|
381
474
|
if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
|
|
475
|
+
if (resources.get("mark").get(id).mark.type === "point" && program.markConfigs[id].shape !== undefined) {
|
|
476
|
+
validatePointShape(program.markConfigs[id].shape);
|
|
477
|
+
}
|
|
382
478
|
if (resources.get("mark").get(id).mark.type === "area") {
|
|
383
479
|
validateAreaPaintConfig(program.markConfigs[id]);
|
|
384
480
|
}
|
package/src/persistence.js
CHANGED
|
@@ -7,6 +7,7 @@ import { decodeValue, encodeValue, encodeSharedValue, invalidSnapshot } from "./
|
|
|
7
7
|
import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
|
|
8
8
|
import {
|
|
9
9
|
deriveTransformSchema,
|
|
10
|
+
datasetSchemaContext,
|
|
10
11
|
inferDatasetSchema,
|
|
11
12
|
validateDatasetSchema
|
|
12
13
|
} from "./grammar/datasetSchema.js";
|
|
@@ -63,7 +64,7 @@ function restoreState(state, extensions, path = "payload", { requireSchemas = fa
|
|
|
63
64
|
{ requireSchemas }
|
|
64
65
|
)]));
|
|
65
66
|
const program = new ChartProgram({ ...state, children });
|
|
66
|
-
validateProgramState(program, path);
|
|
67
|
+
validateProgramState(program, path, { allowMissingLookupSchemas: !requireSchemas });
|
|
67
68
|
if (requireSchemas) {
|
|
68
69
|
for (const dataset of program.semanticSpec.datasets) {
|
|
69
70
|
if (dataset.schema === undefined) {
|
|
@@ -96,7 +97,8 @@ function migrateStateSchemas(state) {
|
|
|
96
97
|
sourceSchema,
|
|
97
98
|
dataset.transform?.[0] ?? {},
|
|
98
99
|
dataset.values,
|
|
99
|
-
{
|
|
100
|
+
datasetSchemaContext({...byId.get(dataset.source),schema:sourceSchema},dataset.transform?.[0]??{},dataset.id,
|
|
101
|
+
input=>({...byId.get(input),schema:schemaFor(input)}))
|
|
100
102
|
);
|
|
101
103
|
}
|
|
102
104
|
active.delete(id);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
|
|
2
|
+
import { validateGraphicClip } from "../../grammar/schemas/graphicClip.js";
|
|
2
3
|
import { prepareIsolatedGlyphs, drawIsolatedGlyph } from "./isolated.js";
|
|
3
4
|
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
4
5
|
import { annotateError } from "../../core/diagnostics.js";
|
|
@@ -158,6 +159,16 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
|
|
|
158
159
|
enterNestedCanvas(context, id, object);
|
|
159
160
|
return;
|
|
160
161
|
}
|
|
162
|
+
if (object.clip !== undefined) {
|
|
163
|
+
validateGraphicClip(object.clip, object.type);
|
|
164
|
+
requireContextMethod(context, "rect", `Graphic "${id}" clip`);
|
|
165
|
+
requireContextMethod(context, "clip", `Graphic "${id}" clip`);
|
|
166
|
+
context.save();
|
|
167
|
+
context.beginPath();
|
|
168
|
+
const {x, y, width, height} = object.clip;
|
|
169
|
+
context.rect(x, y, width, height);
|
|
170
|
+
context.clip();
|
|
171
|
+
}
|
|
161
172
|
if (object.type === "collection") {
|
|
162
173
|
context.save();
|
|
163
174
|
return;
|
|
@@ -179,6 +190,7 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
|
|
|
179
190
|
) {
|
|
180
191
|
context.restore();
|
|
181
192
|
}
|
|
193
|
+
if (object.clip !== undefined) context.restore();
|
|
182
194
|
}
|
|
183
195
|
});
|
|
184
196
|
}
|
|
@@ -221,6 +233,10 @@ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, c
|
|
|
221
233
|
), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
|
|
222
234
|
}
|
|
223
235
|
preflightCanvasGraphicSpec(target, pixelRatio);
|
|
236
|
+
if (Object.values(graphicSpec.objects).some(object => object.clip !== undefined)) {
|
|
237
|
+
requireContextMethod(context, "rect", "Graphic clip");
|
|
238
|
+
requireContextMethod(context, "clip", "Graphic clip");
|
|
239
|
+
}
|
|
224
240
|
const isolated = prepareIsolatedGlyphs(target, context, {pixelRatio, createCanvas});
|
|
225
241
|
|
|
226
242
|
context.canvas.width = physicalWidth;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolvePrimitiveGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
2
|
+
import { graphicClipBounds, intersectClipBounds, validateGraphicClip } from "../../grammar/schemas/graphicClip.js";
|
|
2
3
|
import { walkGraphicTreeEvents } from "../../grammar/schemas/graphicTree.js";
|
|
3
4
|
import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
|
|
4
5
|
|
|
@@ -124,10 +125,19 @@ export function prepareIsolatedGlyphs(target, context, { pixelRatio = 1, createC
|
|
|
124
125
|
};
|
|
125
126
|
frames.push({offset, clip});
|
|
126
127
|
}
|
|
128
|
+
if (object.clip !== undefined) {
|
|
129
|
+
const parent = frames.at(-1);
|
|
130
|
+
validateGraphicClip(object.clip, object.type);
|
|
131
|
+
frames.push({offset: parent.offset, clip: intersectClipBounds(parent.clip,
|
|
132
|
+
graphicClipBounds(object.clip, ...parent.offset))});
|
|
133
|
+
}
|
|
127
134
|
if (!object.items) plan(id, object, object.type);
|
|
128
135
|
},
|
|
129
136
|
item({id, object, owner}) { plan(id, object, object.type ?? owner.type); },
|
|
130
|
-
exit({object}) {
|
|
137
|
+
exit({object}) {
|
|
138
|
+
if (object.clip !== undefined) frames.pop();
|
|
139
|
+
if (object.type === "canvas") frames.pop();
|
|
140
|
+
}
|
|
131
141
|
});
|
|
132
142
|
return {plans, surface, pixelRatio};
|
|
133
143
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveLinearGradientCoordinates } from "../../grammar/paint.js";
|
|
2
|
+
import { validateGraphicClip } from "../../grammar/schemas/graphicClip.js";
|
|
2
3
|
import { resolvePathCommandBounds } from
|
|
3
4
|
"../../grammar/schemas/graphicBounds.js";
|
|
4
5
|
import { walkGraphicTreeEvents } from
|
|
@@ -143,6 +144,12 @@ export function preflightCanvasGraphicSpec(target, scale = 1) {
|
|
|
143
144
|
offset = id === target.canvasId ? undefined : parent;
|
|
144
145
|
}
|
|
145
146
|
preflightGraphic(id, object, scale, object.type, offset);
|
|
147
|
+
if (object.clip !== undefined) {
|
|
148
|
+
const clip = validateGraphicClip(object.clip, object.type);
|
|
149
|
+
requireProperties(clip, ["x", "y", "width", "height"], `Graphic "${id}" Canvas clip`, scale, offset);
|
|
150
|
+
requireCanvasNativeValue(clip.x + clip.width, `Graphic "${id}" Canvas clip`, scale, offset?.[0]);
|
|
151
|
+
requireCanvasNativeValue(clip.y + clip.height, `Graphic "${id}" Canvas clip`, scale, offset?.[1]);
|
|
152
|
+
}
|
|
146
153
|
},
|
|
147
154
|
item({ id, object, owner }) {
|
|
148
155
|
preflightGraphic(
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isOwned } from '../../core/immutable.js';
|
|
2
|
+
import { render } from './index.js';
|
|
3
|
+
|
|
4
|
+
const compiled = new WeakMap();
|
|
5
|
+
// Translate only concrete position properties. Path/text child coordinates keep
|
|
6
|
+
// their authored relationship; gradients retain their local bounding-box units.
|
|
7
|
+
function position(properties, dx, dy) {
|
|
8
|
+
if (!dx && !dy) return properties;
|
|
9
|
+
const result = { ...properties };
|
|
10
|
+
for (const key of ['x', 'x1', 'x2']) if (key in result) result[key] += dx;
|
|
11
|
+
for (const key of ['y', 'y1', 'y2']) if (key in result) result[key] += dy;
|
|
12
|
+
if (result.commands) result.commands = result.commands.map(command => position(command, dx, dy));
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function compile(scene) {
|
|
16
|
+
if (scene?.version !== 2 || ![scene.width, scene.height].every(n => Number.isFinite(n) && n > 0) || !Array.isArray(scene.nodes)) {
|
|
17
|
+
throw new TypeError('renderTransitionScene requires a version 2 Canvas scene.');
|
|
18
|
+
}
|
|
19
|
+
const objects = { canvas: { type: 'canvas', properties: { width: scene.width, height: scene.height }, children: [] } };
|
|
20
|
+
const keys = new Set();
|
|
21
|
+
let index = 0, activeClips = [];
|
|
22
|
+
const groups = [objects.canvas], origins = [[0, 0]];
|
|
23
|
+
const validClip = clip => Array.isArray(clip) && clip.length === 4 &&
|
|
24
|
+
clip.every(Number.isFinite) && clip[2] >= 0 && clip[3] >= 0;
|
|
25
|
+
for (const node of scene.nodes) {
|
|
26
|
+
const clips = node.clips ?? [node.clip];
|
|
27
|
+
if (typeof node.key !== 'string' || keys.has(node.key) || !['circle','rect','line','path','text'].includes(node.type) ||
|
|
28
|
+
node.offset?.length !== 2 || !node.offset.every(Number.isFinite) || !validClip(node.clip) ||
|
|
29
|
+
!Array.isArray(clips) || !clips.every(validClip) || !node.properties || typeof node.properties !== 'object') {
|
|
30
|
+
throw new TypeError('Canvas scene nodes require unique keys, concrete properties, offsets and clips.');
|
|
31
|
+
}
|
|
32
|
+
keys.add(node.key);
|
|
33
|
+
let common = 0;
|
|
34
|
+
while (common < Math.min(clips.length, activeClips.length) &&
|
|
35
|
+
clips[common].every((value, i) => value === activeClips[common][i])) common++;
|
|
36
|
+
groups.length = common + 1; origins.length = common + 1;
|
|
37
|
+
// Retain shared concrete clip ancestors across adjacent nodes. Rebuilding
|
|
38
|
+
// a fractional ancestor between its children can change raster coverage.
|
|
39
|
+
for (let i = common; i < clips.length; i++) {
|
|
40
|
+
const clip = clips[i], origin = origins.at(-1), parent = groups.at(-1);
|
|
41
|
+
const id = `clip-${index++}`;
|
|
42
|
+
const nested = objects[id] = { type: 'canvas', properties: {
|
|
43
|
+
x: clip[0] - origin[0], y: clip[1] - origin[1], width: clip[2], height: clip[3]
|
|
44
|
+
}, children: [] };
|
|
45
|
+
parent.children.push(id); groups.push(nested); origins.push([clip[0], clip[1]]);
|
|
46
|
+
}
|
|
47
|
+
activeClips = clips;
|
|
48
|
+
const group = groups.at(-1), origin = origins.at(-1);
|
|
49
|
+
const id = `node-${index++}`;
|
|
50
|
+
objects[id] = { type: node.type, properties: position(node.properties,
|
|
51
|
+
node.offset[0] - origin[0], node.offset[1] - origin[1]) };
|
|
52
|
+
group.children.push(id);
|
|
53
|
+
}
|
|
54
|
+
return { graphicSpec: { objects, order: ['canvas'] } };
|
|
55
|
+
}
|
|
56
|
+
/** Paint concrete Canvas scenes with the ordinary renderer; no semantics or DOM. */
|
|
57
|
+
export function renderTransitionScene(scene, context, options) {
|
|
58
|
+
// Only immutable producer scenes are cacheable. Intermediate animation frames
|
|
59
|
+
// may be reused/mutated by a caller and must always reflect their current data.
|
|
60
|
+
let program = isOwned(scene) ? compiled.get(scene) : undefined;
|
|
61
|
+
if (!program) {
|
|
62
|
+
program = compile(scene);
|
|
63
|
+
if (isOwned(scene)) compiled.set(scene, program);
|
|
64
|
+
}
|
|
65
|
+
return render(program, context, options);
|
|
66
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
2
2
|
import { requireFiniteProperty, requireStringProperty } from "./validation.js";
|
|
3
|
-
import { validateConcreteGraphicProperties } from
|
|
3
|
+
import { validateConcreteGraphicProperties, validateConcreteGraphicValue } from
|
|
4
4
|
"../../grammar/schemas/concreteGraphic.js";
|
|
5
5
|
import { normalizeRendererFontWeight } from "../text.js";
|
|
6
6
|
|
|
@@ -13,7 +13,8 @@ function drawText(context, child, collectionId) {
|
|
|
13
13
|
const fontSize = requireFiniteProperty(properties, "fontSize", graphicId);
|
|
14
14
|
const rotation = properties.rotation ?? 0;
|
|
15
15
|
const opacity = properties.opacity ?? 1;
|
|
16
|
-
const text =
|
|
16
|
+
const text = properties.text;
|
|
17
|
+
validateConcreteGraphicValue("text", "text", text);
|
|
17
18
|
const fill = requireStringProperty(properties, "fill", graphicId);
|
|
18
19
|
const fontFamily = requireStringProperty(
|
|
19
20
|
properties,
|