ggaction 0.0.13 → 0.0.16
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 +146 -0
- package/README.md +41 -12
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +39 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +20 -12
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +34 -9
- package/src/actions/boxPlots/materialize.js +12 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +21 -12
- package/src/actions/charts/area.js +6 -2
- package/src/actions/charts/bar.js +3 -2
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +7 -3
- package/src/actions/charts/ecdf.js +6 -3
- package/src/actions/charts/endpoints.js +29 -7
- package/src/actions/charts/heatmap.js +3 -2
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +4 -2
- package/src/actions/charts/interval-regression.js +7 -4
- package/src/actions/charts/line.js +4 -2
- package/src/actions/charts/parallel.js +9 -3
- package/src/actions/charts/pie.js +11 -3
- package/src/actions/charts/polar.js +17 -6
- package/src/actions/charts/radar.js +9 -3
- package/src/actions/charts/radial.js +12 -4
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +8 -4
- package/src/actions/charts/scatter.js +4 -2
- package/src/actions/composition/actions.js +29 -24
- package/src/actions/coordinates/actions.js +2 -2
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +63 -193
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +4 -4
- package/src/actions/data/create.js +7 -14
- package/src/actions/data/density.js +15 -23
- package/src/actions/data/derived.js +57 -31
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +551 -0
- package/src/actions/data/filter.js +28 -22
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +62 -47
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +21 -27
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +125 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +6 -3
- package/src/actions/data/window.js +3 -3
- package/src/actions/encodings/angle.js +1 -1
- package/src/actions/encodings/appearance.js +4 -4
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +3 -2
- package/src/actions/encodings/density.js +7 -4
- package/src/actions/encodings/histogram.js +4 -3
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +22 -15
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +14 -6
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +13 -9
- package/src/actions/encodings/ruleAppearance.js +2 -24
- package/src/actions/encodings/seriesLayout.js +2 -1
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +34 -20
- package/src/actions/errorBands/edit.js +34 -137
- package/src/actions/errorBars/create.js +43 -20
- package/src/actions/errorBars/edit.js +29 -125
- package/src/actions/facets/actions.js +267 -73
- package/src/actions/facets/derive.js +99 -28
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +15 -8
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +18 -9
- package/src/actions/gradientPlots/materialize.js +5 -3
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +67 -21
- package/src/actions/guides/axes/lines.js +15 -9
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +6 -5
- package/src/actions/guides/axes/ticks.js +19 -6
- package/src/actions/guides/axes/titles.js +21 -13
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +20 -4
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +138 -52
- package/src/actions/guides/legends/categorical/components.js +55 -45
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +98 -16
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +104 -52
- package/src/actions/guides/legends/continuous/common.js +76 -20
- package/src/actions/guides/legends/continuous/gradient.js +95 -158
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +21 -16
- package/src/actions/guides/legends/continuous/opacity.js +37 -83
- package/src/actions/guides/legends/continuous/stroke.js +225 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +159 -60
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +59 -24
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +17 -8
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +100 -108
- package/src/actions/guides/legends/strokeWidth.js +38 -92
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +8 -8
- package/src/actions/guides/polar/axes/labels.js +29 -10
- package/src/actions/guides/polar/axes/lines.js +19 -19
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +10 -16
- package/src/actions/guides/polar/axes/titles.js +6 -6
- package/src/actions/guides/polar/grids.js +12 -14
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +16 -3
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +80 -22
- package/src/actions/marks/area/actions.js +38 -13
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +12 -3
- package/src/actions/marks/bar/edit.js +21 -4
- package/src/actions/marks/bar/materialize.js +34 -40
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +40 -17
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +9 -3
- package/src/actions/marks/point/edit.js +19 -7
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +48 -22
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +36 -15
- package/src/actions/marks/references.js +602 -33
- package/src/actions/marks/remove.js +67 -41
- package/src/actions/marks/rule/actions.js +51 -14
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +79 -14
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +157 -17
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +72 -0
- package/src/actions/marks/tick/actions.js +25 -8
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +31 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +11 -3
- package/src/actions/primitives/semanticAction.js +90 -14
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +36 -25
- package/src/actions/regression/create.js +13 -6
- package/src/actions/regression/edit.js +24 -11
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +173 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +55 -23
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +67 -12
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +5 -4
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +179 -66
- package/src/actions/selection/index.js +3 -0
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +70 -15
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +313 -121
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +6 -3
- package/src/actions/violinPlots/edit.js +2 -1
- package/src/core/action.js +117 -35
- package/src/core/compositionState.js +6 -1
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +21 -1
- package/src/core/resourceReferences.js +341 -0
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/core/vocabulary.js +16 -3
- package/src/diagnostics.js +1 -0
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +341 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +190 -41
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +351 -7
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +9 -3
- package/src/materialization/facetGuides/preparation.js +13 -8
- package/src/materialization/facetHeaders.js +347 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +20 -5
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +74 -0
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +29 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +38 -5
- package/src/selectors/layers.js +10 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +103 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +10 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +90 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +944 -118
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { resolveBarChannels } from "../grammar/bars/policy.js";
|
|
2
|
+
import {
|
|
3
|
+
assertMarkLabelPlacementSupport,
|
|
4
|
+
normalizeMarkLabelPlacement,
|
|
5
|
+
resolveMarkLabelPlacement
|
|
6
|
+
} from "../layout/labels.js";
|
|
2
7
|
import { resolveMarkLabelValues } from "../grammar/markLabels.js";
|
|
3
8
|
import { formatTextValue } from "../grammar/text.js";
|
|
4
9
|
import { normalizePositionDatum } from "../grammar/positionDatum.js";
|
|
5
10
|
import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
|
|
6
11
|
import { findDataset } from "../selectors/datasets.js";
|
|
12
|
+
import { findCoordinate } from "../selectors/coordinates.js";
|
|
7
13
|
import { findLayer } from "../selectors/layers.js";
|
|
14
|
+
import { unionConcreteGraphicBounds } from
|
|
15
|
+
"../grammar/schemas/graphicBounds.js";
|
|
16
|
+
import { polarDirection } from "../grammar/polar.js";
|
|
8
17
|
import { resolveMarkItems } from "./selection/policies/index.js";
|
|
18
|
+
import { resolveMarkSelection } from "./selection/state.js";
|
|
9
19
|
import { mapScaleConsumerValues } from "./scales/map.js";
|
|
10
20
|
import { resolveRowEncodingValues } from "./rowEncoding.js";
|
|
11
21
|
|
|
@@ -83,6 +93,172 @@ function sourceAnchor(program, source, item) {
|
|
|
83
93
|
return { x: item.properties.x, y: item.properties.y };
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
function explicitIntervalAxis(source) {
|
|
97
|
+
const axes = ["x", "y"].filter(axis =>
|
|
98
|
+
source.encoding?.[axis] !== undefined &&
|
|
99
|
+
source.encoding?.[`${axis}2`] !== undefined
|
|
100
|
+
);
|
|
101
|
+
return axes.length === 1 ? axes[0] : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sourcePlacementContext(program, source) {
|
|
105
|
+
const coordinateType = findCoordinate(program, source.coordinate)?.type;
|
|
106
|
+
return {
|
|
107
|
+
markType: source.mark.type,
|
|
108
|
+
coordinateType,
|
|
109
|
+
intervalAxis: source.mark.type === "bar"
|
|
110
|
+
? resolveBarChannels(source).measure
|
|
111
|
+
: source.mark.type === "rect"
|
|
112
|
+
? explicitIntervalAxis(source)
|
|
113
|
+
: undefined
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function validateSourceMarkLabelPlacement(program, sourceId, value) {
|
|
118
|
+
const source = findLayer(program, sourceId);
|
|
119
|
+
if (source === undefined) {
|
|
120
|
+
throw new Error(`Unknown mark label placement source "${sourceId}".`);
|
|
121
|
+
}
|
|
122
|
+
const placement = normalizeMarkLabelPlacement(value);
|
|
123
|
+
assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
|
|
124
|
+
return placement;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function rectangleBounds(item) {
|
|
128
|
+
const { x, y, width, height } = item.properties;
|
|
129
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
130
|
+
throw new TypeError("Interval mark label placement requires finite rectangle geometry.");
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
left: Math.min(x, x + width),
|
|
134
|
+
right: Math.max(x, x + width),
|
|
135
|
+
top: Math.min(y, y + height),
|
|
136
|
+
bottom: Math.max(y, y + height)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mappedChannel(program, source, item, channel) {
|
|
141
|
+
const value = item.channels[channel];
|
|
142
|
+
const primary = channel === "x2" ? "x" : channel === "y2" ? "y" : channel;
|
|
143
|
+
const scale = program.resolvedScales[
|
|
144
|
+
source.encoding?.[channel]?.scale ?? source.encoding?.[primary]?.scale
|
|
145
|
+
];
|
|
146
|
+
return value === undefined || scale === undefined
|
|
147
|
+
? undefined
|
|
148
|
+
: mapScaleConsumerValues([value], scale, channel)[0];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scaleIncreaseDirection(program, source, axis) {
|
|
152
|
+
const scale = program.resolvedScales[source.encoding?.[axis]?.scale];
|
|
153
|
+
const domain = scale?.domain;
|
|
154
|
+
if (!Array.isArray(domain) || domain.length < 2) {
|
|
155
|
+
return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
|
|
156
|
+
}
|
|
157
|
+
const mapped = mapScaleConsumerValues(
|
|
158
|
+
[domain[0], domain.at(-1)],
|
|
159
|
+
scale,
|
|
160
|
+
axis
|
|
161
|
+
);
|
|
162
|
+
const delta = mapped[1] - mapped[0];
|
|
163
|
+
if (!Number.isFinite(delta) || delta === 0) {
|
|
164
|
+
return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
|
|
165
|
+
}
|
|
166
|
+
return axis === "x"
|
|
167
|
+
? { x: Math.sign(delta), y: 0 }
|
|
168
|
+
: { x: 0, y: Math.sign(delta) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function intervalPlacementGeometry(program, source, item, axis) {
|
|
172
|
+
const bounds = rectangleBounds(item);
|
|
173
|
+
const primary = mappedChannel(program, source, item, axis);
|
|
174
|
+
const secondary = mappedChannel(program, source, item, `${axis}2`);
|
|
175
|
+
if (![primary, secondary].every(Number.isFinite)) {
|
|
176
|
+
throw new TypeError(
|
|
177
|
+
`${source.mark.type === "bar" ? "Bar" : "Rect"} semantic label placement requires resolved ${axis}/${axis}2 endpoints.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const cross = axis === "x"
|
|
181
|
+
? (bounds.top + bounds.bottom) / 2
|
|
182
|
+
: (bounds.left + bounds.right) / 2;
|
|
183
|
+
const start = axis === "x" ? { x: primary, y: cross } : { x: cross, y: primary };
|
|
184
|
+
const end = axis === "x" ? { x: secondary, y: cross } : { x: cross, y: secondary };
|
|
185
|
+
const delta = { x: end.x - start.x, y: end.y - start.y };
|
|
186
|
+
const length = Math.hypot(delta.x, delta.y);
|
|
187
|
+
const endOutward = length > 0
|
|
188
|
+
? { x: delta.x / length, y: delta.y / length }
|
|
189
|
+
: scaleIncreaseDirection(program, source, axis);
|
|
190
|
+
return {
|
|
191
|
+
kind: "interval",
|
|
192
|
+
center: {
|
|
193
|
+
x: (bounds.left + bounds.right) / 2,
|
|
194
|
+
y: (bounds.top + bounds.bottom) / 2
|
|
195
|
+
},
|
|
196
|
+
start: {
|
|
197
|
+
point: start,
|
|
198
|
+
outward: { x: -endOutward.x, y: -endOutward.y }
|
|
199
|
+
},
|
|
200
|
+
end: { point: end, outward: endOutward },
|
|
201
|
+
bounds
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pointPlacementGeometry(program, source, item) {
|
|
206
|
+
const center = { x: item.properties.x, y: item.properties.y };
|
|
207
|
+
const coordinateType = findCoordinate(program, source.coordinate)?.type;
|
|
208
|
+
if (coordinateType !== "polar") return { kind: "point", center };
|
|
209
|
+
const thetaScale = program.resolvedScales[source.encoding?.theta?.scale];
|
|
210
|
+
const theta = item.channels.theta === undefined || thetaScale === undefined
|
|
211
|
+
? undefined
|
|
212
|
+
: mapScaleConsumerValues([item.channels.theta], thetaScale, "theta")[0];
|
|
213
|
+
const outward = Number.isFinite(theta) ? polarDirection(theta) : { x: 0, y: -1 };
|
|
214
|
+
const objectBounds = unionConcreteGraphicBounds(
|
|
215
|
+
program.graphicSpec,
|
|
216
|
+
item.graphicIds
|
|
217
|
+
, program.materializationConfigs.textMetrics);
|
|
218
|
+
const corners = objectBounds === undefined ? [] : [
|
|
219
|
+
{ x: objectBounds.left, y: objectBounds.top },
|
|
220
|
+
{ x: objectBounds.right, y: objectBounds.top },
|
|
221
|
+
{ x: objectBounds.right, y: objectBounds.bottom },
|
|
222
|
+
{ x: objectBounds.left, y: objectBounds.bottom }
|
|
223
|
+
];
|
|
224
|
+
const support = Math.max(0, ...corners.map(point =>
|
|
225
|
+
(point.x - center.x) * outward.x + (point.y - center.y) * outward.y
|
|
226
|
+
));
|
|
227
|
+
return {
|
|
228
|
+
kind: "point",
|
|
229
|
+
center,
|
|
230
|
+
end: {
|
|
231
|
+
point: {
|
|
232
|
+
x: center.x + outward.x * support,
|
|
233
|
+
y: center.y + outward.y * support
|
|
234
|
+
},
|
|
235
|
+
outward
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sourcePlacementGeometry(program, source, item) {
|
|
241
|
+
if (source.mark.type === "bar") {
|
|
242
|
+
return intervalPlacementGeometry(
|
|
243
|
+
program,
|
|
244
|
+
source,
|
|
245
|
+
item,
|
|
246
|
+
resolveBarChannels(source).measure
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (source.mark.type === "rect") {
|
|
250
|
+
const axis = explicitIntervalAxis(source);
|
|
251
|
+
return axis === undefined
|
|
252
|
+
? { kind: "point", center: sourceAnchor(program, source, item) }
|
|
253
|
+
: intervalPlacementGeometry(program, source, item, axis);
|
|
254
|
+
}
|
|
255
|
+
if (source.mark.type === "arc") return item.geometry;
|
|
256
|
+
if (source.mark.type === "point") {
|
|
257
|
+
return pointPlacementGeometry(program, source, item);
|
|
258
|
+
}
|
|
259
|
+
return { kind: "point", center: sourceAnchor(program, source, item) };
|
|
260
|
+
}
|
|
261
|
+
|
|
86
262
|
function contentValue(encoding, rowOrItem, source) {
|
|
87
263
|
return Object.hasOwn(encoding, "field")
|
|
88
264
|
? source === undefined
|
|
@@ -91,8 +267,7 @@ function contentValue(encoding, rowOrItem, source) {
|
|
|
91
267
|
: encoding.datum;
|
|
92
268
|
}
|
|
93
269
|
|
|
94
|
-
function
|
|
95
|
-
const text = formatTextValue(value, format);
|
|
270
|
+
function concreteTextItem(config, position, text, { offsets = true } = {}) {
|
|
96
271
|
const x = position.x;
|
|
97
272
|
const y = position.y;
|
|
98
273
|
if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
@@ -101,8 +276,8 @@ function concreteItem(config, position, value, format) {
|
|
|
101
276
|
return {
|
|
102
277
|
type: "text",
|
|
103
278
|
properties: {
|
|
104
|
-
x: x + config.dx,
|
|
105
|
-
y: y + config.dy,
|
|
279
|
+
x: x + (offsets ? config.dx : 0),
|
|
280
|
+
y: y + (offsets ? config.dy : 0),
|
|
106
281
|
text,
|
|
107
282
|
fill: config.fill,
|
|
108
283
|
opacity: config.opacity,
|
|
@@ -116,6 +291,10 @@ function concreteItem(config, position, value, format) {
|
|
|
116
291
|
};
|
|
117
292
|
}
|
|
118
293
|
|
|
294
|
+
function concreteItem(config, position, value, format) {
|
|
295
|
+
return concreteTextItem(config, position, formatTextValue(value, format));
|
|
296
|
+
}
|
|
297
|
+
|
|
119
298
|
function relativeLuminance(color) {
|
|
120
299
|
if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) {
|
|
121
300
|
return undefined;
|
|
@@ -147,18 +326,90 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
147
326
|
if (source === undefined) {
|
|
148
327
|
throw new Error(`Text mark "${layer.id}" requires source layer "${layer.source}".`);
|
|
149
328
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
329
|
+
const requested = program.markConfigs[layer.id]?.labelAuthoring?.selection ?? {
|
|
330
|
+
kind: "all"
|
|
331
|
+
};
|
|
332
|
+
let allItems;
|
|
333
|
+
let items;
|
|
334
|
+
if (requested.kind === "all") {
|
|
335
|
+
allItems = resolveMarkItems(program, source.id);
|
|
336
|
+
items = allItems;
|
|
337
|
+
} else if (requested.kind === "inline") {
|
|
338
|
+
const resolved = resolveMarkSelection(program, source.id, requested.selector);
|
|
339
|
+
allItems = resolved.items;
|
|
340
|
+
const selected = new Set(resolved.keys);
|
|
341
|
+
items = allItems.filter(item => selected.has(item.key));
|
|
342
|
+
} else if (requested.kind === "named") {
|
|
343
|
+
const definition = program.materializationConfigs.selections?.[requested.id];
|
|
344
|
+
if (definition === undefined) {
|
|
345
|
+
throw new Error(`Unknown label selection "${requested.id}".`);
|
|
346
|
+
}
|
|
347
|
+
if (definition.target !== source.id) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Label selection "${requested.id}" must target source mark "${source.id}".`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const resolved = resolveMarkSelection(program, source.id, definition.selector);
|
|
353
|
+
allItems = resolved.items;
|
|
354
|
+
const selected = new Set(resolved.keys);
|
|
355
|
+
items = allItems.filter(item => selected.has(item.key));
|
|
356
|
+
} else {
|
|
357
|
+
throw new Error(`Unknown label selection kind "${requested.kind}".`);
|
|
358
|
+
}
|
|
359
|
+
const resolvedValues = layer.encoding.text.content === undefined ? undefined
|
|
360
|
+
: resolveMarkLabelValues(source, allItems, layer.encoding.text);
|
|
361
|
+
const values = resolvedValues === undefined ? undefined : new Map(
|
|
362
|
+
allItems.map((item, index) => [item.key, resolvedValues[index]])
|
|
363
|
+
);
|
|
364
|
+
return items.flatMap(item => {
|
|
365
|
+
let resolvedConfig = sourceTextConfig(config, source, item);
|
|
366
|
+
const text = formatTextValue(
|
|
367
|
+
values === undefined
|
|
368
|
+
? contentValue(layer.encoding.text, item, source)
|
|
369
|
+
: values.get(item.key),
|
|
159
370
|
layer.encoding.text.format
|
|
160
371
|
);
|
|
161
|
-
|
|
372
|
+
if (text === undefined) return [];
|
|
373
|
+
const placement = program.markConfigs[layer.id]?.labelAuthoring?.placement;
|
|
374
|
+
if (placement === undefined) {
|
|
375
|
+
const anchor = sourceAnchor(program, source, item);
|
|
376
|
+
const concrete = concreteTextItem(resolvedConfig, anchor, text);
|
|
377
|
+
return concrete === undefined ? [] : [{ graphic: concrete, anchor }];
|
|
378
|
+
}
|
|
379
|
+
assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
|
|
380
|
+
const resolved = resolveMarkLabelPlacement({
|
|
381
|
+
placement,
|
|
382
|
+
geometry: sourcePlacementGeometry(program, source, item),
|
|
383
|
+
text: {
|
|
384
|
+
text,
|
|
385
|
+
fontSize: resolvedConfig.fontSize,
|
|
386
|
+
fontFamily: resolvedConfig.fontFamily,
|
|
387
|
+
fontWeight: resolvedConfig.fontWeight,
|
|
388
|
+
textAlign: resolvedConfig.align,
|
|
389
|
+
textBaseline: resolvedConfig.baseline,
|
|
390
|
+
rotation: resolvedConfig.rotation,
|
|
391
|
+
dx: resolvedConfig.dx,
|
|
392
|
+
dy: resolvedConfig.dy
|
|
393
|
+
}
|
|
394
|
+
}, program.materializationConfigs.textMetrics);
|
|
395
|
+
if (!resolved.visible) return [];
|
|
396
|
+
if (
|
|
397
|
+
resolved.anchor.startsWith("outside") &&
|
|
398
|
+
config.fillExplicit !== true
|
|
399
|
+
) {
|
|
400
|
+
resolvedConfig = config;
|
|
401
|
+
}
|
|
402
|
+
const anchor = { x: resolved.sourceX, y: resolved.sourceY };
|
|
403
|
+
return [{
|
|
404
|
+
graphic: concreteTextItem(
|
|
405
|
+
resolvedConfig,
|
|
406
|
+
{ x: resolved.x, y: resolved.y },
|
|
407
|
+
text,
|
|
408
|
+
{ offsets: false }
|
|
409
|
+
),
|
|
410
|
+
anchor,
|
|
411
|
+
placement: resolved
|
|
412
|
+
}];
|
|
162
413
|
});
|
|
163
414
|
}
|
|
164
415
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { applyMaterializationPlan, buildMaterializationPlan } from "./planner.js";
|
|
2
|
+
import { withGuideLayoutTransaction } from "./guides/layout.js";
|
|
3
|
+
|
|
4
|
+
// Font tokens and measured-width profiles invalidate the same existing owners.
|
|
5
|
+
// Incomplete text remains incomplete until its own prerequisites are authored.
|
|
6
|
+
export function rematerializeTypography(program) {
|
|
7
|
+
if (program.compositionSpec !== undefined) {
|
|
8
|
+
return program.compositionSpec.type === "facet" ? program.materializeComposition() : program;
|
|
9
|
+
}
|
|
10
|
+
const exists = (id, op) => program.graphicSpec.objects[id] !== undefined && typeof program[op] === "function";
|
|
11
|
+
const marks = program.semanticSpec.layers
|
|
12
|
+
.filter(layer => layer.mark?.type === "text" && exists(layer.id, "rematerializeTextMark"))
|
|
13
|
+
.map(layer => ({ op: "rematerializeTextMark", args: { id: layer.id } }));
|
|
14
|
+
const guides = [];
|
|
15
|
+
for (const [id, op] of [
|
|
16
|
+
["xAxisLabels", "editXAxisLabels"], ["yAxisLabels", "editYAxisLabels"],
|
|
17
|
+
["xAxisTitle", "editXAxisTitle"], ["yAxisTitle", "editYAxisTitle"],
|
|
18
|
+
["thetaAxisLabels", "editThetaAxisLabels"], ["radialAxisLabels", "editRadialAxisLabels"],
|
|
19
|
+
["thetaAxisTitle", "editThetaAxisTitle"], ["radialAxisTitle", "editRadialAxisTitle"]
|
|
20
|
+
]) if (exists(id, op)) guides.push({ op });
|
|
21
|
+
const parallel = program.guideConfigs.axis?.parallel?.axes;
|
|
22
|
+
if (parallel !== undefined && typeof program.rematerializeParallelAxes === "function") {
|
|
23
|
+
guides.push({ op: "rematerializeParallelAxes", args: { target: parallel.target } });
|
|
24
|
+
}
|
|
25
|
+
if (Object.keys(program.guideConfigs.legend ?? {}).length > 0 && typeof program.rematerializeLegend === "function") {
|
|
26
|
+
guides.push({ op: "rematerializeLegend" });
|
|
27
|
+
}
|
|
28
|
+
const layout = exists("chartTitle", "rematerializeTitle") ? [{ op: "rematerializeTitle" }] : [];
|
|
29
|
+
return withGuideLayoutTransaction(program, next =>
|
|
30
|
+
applyMaterializationPlan(next, buildMaterializationPlan({ marks, guides, layout })));
|
|
31
|
+
}
|
package/src/mcp/cli.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { runGgactionMcpServer } from "./server.js";
|
|
4
|
-
|
|
5
3
|
try {
|
|
4
|
+
const { runGgactionMcpServer } = await import("./server.js");
|
|
6
5
|
await runGgactionMcpServer();
|
|
7
6
|
} catch (error) {
|
|
8
|
-
const message = error
|
|
7
|
+
const message = error.code === "ERR_MODULE_NOT_FOUND" &&
|
|
8
|
+
error.message.includes("@modelcontextprotocol/sdk")
|
|
9
|
+
? "MCP requires @modelcontextprotocol/sdk. Install it with: npm install ggaction @modelcontextprotocol/sdk"
|
|
10
|
+
: error instanceof Error ? error.message : String(error);
|
|
9
11
|
process.stderr.write(`ggaction MCP failed: ${message}\n`);
|
|
10
12
|
process.exitCode = 1;
|
|
11
13
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
3
|
+
|
|
4
|
+
export function invalidSnapshot(message, optionPath) {
|
|
5
|
+
return annotateError(new TypeError(message), { code: "unsupported-format", optionPath });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function encodeValue(value, path = "payload", ancestors = new WeakSet()) {
|
|
9
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
10
|
+
if (value === undefined) return ["undefined"];
|
|
11
|
+
if (typeof value === "bigint") return ["bigint", value.toString()];
|
|
12
|
+
if (typeof value === "number") {
|
|
13
|
+
return Number.isFinite(value) && !Object.is(value, -0) ? value
|
|
14
|
+
: ["number", Object.is(value, -0) ? "-0" : String(value)];
|
|
15
|
+
}
|
|
16
|
+
if (typeof value !== "object") throw invalidSnapshot(`Cannot serialize ${typeof value} at ${path}.`, path);
|
|
17
|
+
if (ancestors.has(value)) throw invalidSnapshot(`Cannot serialize a cycle at ${path}.`, path);
|
|
18
|
+
if (!Array.isArray(value) && !isPlainObject(value)) throw invalidSnapshot(`Cannot serialize a class instance at ${path}.`, path);
|
|
19
|
+
if (Object.getOwnPropertySymbols(value).length) throw invalidSnapshot(`Cannot serialize symbol keys at ${path}.`, path);
|
|
20
|
+
const keys = Object.getOwnPropertyNames(value);
|
|
21
|
+
if (Array.isArray(value) ? keys.some(key => key !== "length" && !(/^(0|[1-9]\d*)$/.test(key) && Number(key) < value.length)) :
|
|
22
|
+
keys.some(key => !Object.getOwnPropertyDescriptor(value, key).enumerable)) {
|
|
23
|
+
throw invalidSnapshot(`Cannot serialize hidden or named array properties at ${path}.`, path);
|
|
24
|
+
}
|
|
25
|
+
ancestors.add(value);
|
|
26
|
+
const result = Array.isArray(value)
|
|
27
|
+
? ["array", Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)
|
|
28
|
+
? encodeValue(value[index], `${path}[${index}]`, ancestors) : ["hole"])]
|
|
29
|
+
: ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors)])];
|
|
30
|
+
ancestors.delete(value);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function decodeValue(value, path = "payload") {
|
|
35
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
36
|
+
if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
|
|
37
|
+
const invalid = () => { throw invalidSnapshot(`Invalid encoded value at ${path}.`, path); };
|
|
38
|
+
if (!Array.isArray(value)) return invalid();
|
|
39
|
+
const [tag, payload] = value;
|
|
40
|
+
if (tag === "undefined" && value.length === 1) return undefined;
|
|
41
|
+
if (value.length !== 2) return invalid();
|
|
42
|
+
if (tag === "bigint" && typeof payload === "string" && /^(?:0|-?[1-9]\d*)$/.test(payload)) return BigInt(payload);
|
|
43
|
+
if (tag === "number" && typeof payload === "string" && ["NaN", "Infinity", "-Infinity", "-0"].includes(payload)) {
|
|
44
|
+
return payload === "-0" ? -0 : Number(payload);
|
|
45
|
+
}
|
|
46
|
+
if (tag === "array" && Array.isArray(payload)) {
|
|
47
|
+
const result = new Array(payload.length);
|
|
48
|
+
payload.forEach((child, index) => {
|
|
49
|
+
if (Array.isArray(child) && child.length === 1 && child[0] === "hole") return;
|
|
50
|
+
result[index] = decodeValue(child, `${path}[${index}]`);
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
if (tag === "object" && Array.isArray(payload)) {
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const entry of payload) {
|
|
57
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || Object.hasOwn(result, entry[0])) return invalid();
|
|
58
|
+
Object.defineProperty(result, entry[0], {
|
|
59
|
+
value: decodeValue(entry[1], `${path}.${entry[0]}`), enumerable: true, writable: true, configurable: true
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
return invalid();
|
|
65
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
|
|
2
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
3
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
4
|
+
import { collectAllResourceReferences } from "../core/resourceReferences.js";
|
|
5
|
+
import { requiredTraceExtension } from "../core/extensionRegistry.js";
|
|
6
|
+
import { resolveDatasetReference } from "../selectors/datasets.js";
|
|
7
|
+
import { normalizeMarkSelector } from "../grammar/markSelection.js";
|
|
8
|
+
import { parseSemanticPath } from "../grammar/schemas/semanticPath.js";
|
|
9
|
+
import { validateStoredSemanticValue } from "../actions/primitives/semantic.js";
|
|
10
|
+
import { validateGraphicType, isGraphicContainerType } from "../grammar/schemas/graphic.js";
|
|
11
|
+
import { validateConcreteGraphicProperties } from "../grammar/schemas/concreteGraphic.js";
|
|
12
|
+
import { walkGraphicTreeEvents } from "../grammar/schemas/graphicTree.js";
|
|
13
|
+
import { invalidSnapshot } from "./codec.js";
|
|
14
|
+
|
|
15
|
+
export const STATE_KEYS = Object.freeze([
|
|
16
|
+
"semanticSpec", "graphicSpec", "resolvedScales", "materializationConfigs",
|
|
17
|
+
"children", "compositionSpec", "context", "trace", "actionStack"
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function requireObject(value, path) {
|
|
21
|
+
if (!isPlainObject(value)) throw invalidSnapshot(`${path} must be a plain object.`, path);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function exactKeys(value, keys, path) {
|
|
25
|
+
requireObject(value, path);
|
|
26
|
+
if (Object.keys(value).length !== keys.length || keys.some(key => !Object.hasOwn(value, key))) {
|
|
27
|
+
throw invalidSnapshot(`${path} must contain exactly: ${keys.join(", ")}.`, path);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requireArray(value, path) {
|
|
32
|
+
if (!Array.isArray(value) || Array.from({ length: value.length }, (_, i) => i).some(i => !Object.hasOwn(value, i))) {
|
|
33
|
+
throw invalidSnapshot(`${path} must be a dense array.`, path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateGraphic(graphic, path = "payload.graphicSpec") {
|
|
38
|
+
exactKeys(graphic, ["objects", "order"], path);
|
|
39
|
+
requireObject(graphic.objects, `${path}.objects`);
|
|
40
|
+
requireArray(graphic.order, `${path}.order`);
|
|
41
|
+
const ids = new Set(Object.keys(graphic.objects));
|
|
42
|
+
function attachment(id) {
|
|
43
|
+
if (typeof id !== "string" || !Object.hasOwn(graphic.objects, id)) {
|
|
44
|
+
throw invalidSnapshot(`Unknown graphic attachment "${String(id)}".`, path);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [id, object] of Object.entries(graphic.objects)) {
|
|
48
|
+
if (!id.length) throw invalidSnapshot("Graphic IDs must be non-empty.", path);
|
|
49
|
+
requireObject(object, `${path}.objects.${id}`);
|
|
50
|
+
validateGraphicType(object.type);
|
|
51
|
+
if (Object.keys(object).some(key => !["type", "properties", "items", "children"].includes(key))) {
|
|
52
|
+
throw invalidSnapshot(`Unknown graphic member at ${id}.`, path);
|
|
53
|
+
}
|
|
54
|
+
if (object.properties !== undefined) validateConcreteGraphicProperties(object.type, object.properties);
|
|
55
|
+
if (object.items !== undefined) {
|
|
56
|
+
if (object.type === "canvas" || object.properties !== undefined) throw invalidSnapshot(`Invalid item owner "${id}".`, path);
|
|
57
|
+
requireArray(object.items, `${path}.objects.${id}.items`);
|
|
58
|
+
for (const item of object.items) {
|
|
59
|
+
exactKeys(item, object.type === "collection" ? ["id", "type", "properties"] : ["id", "properties"], path);
|
|
60
|
+
if (typeof item.id !== "string" || !item.id.length || ids.has(item.id)) throw invalidSnapshot(`Duplicate or invalid graphic item ID "${item.id}".`, path);
|
|
61
|
+
ids.add(item.id);
|
|
62
|
+
const type = item.type ?? object.type;
|
|
63
|
+
validateGraphicType(type);
|
|
64
|
+
if (isGraphicContainerType(type)) throw invalidSnapshot("Graphic items must be primitive drawables.", path);
|
|
65
|
+
validateConcreteGraphicProperties(type, item.properties);
|
|
66
|
+
}
|
|
67
|
+
} else if (object.properties === undefined) {
|
|
68
|
+
throw invalidSnapshot(`Graphic "${id}" requires properties or items.`, path);
|
|
69
|
+
}
|
|
70
|
+
if (object.children !== undefined) {
|
|
71
|
+
if (!isGraphicContainerType(object.type)) throw invalidSnapshot(`Graphic "${id}" cannot own children.`, path);
|
|
72
|
+
requireArray(object.children, `${path}.objects.${id}.children`);
|
|
73
|
+
object.children.forEach(attachment);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
graphic.order.forEach(attachment);
|
|
77
|
+
walkGraphicTreeEvents(graphic, {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateTrace(trace, ProgramClass, extensions, path = "payload.trace") {
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
function visit(node, root) {
|
|
83
|
+
exactKeys(node, ["id", "op", "description", "args", "children"], path);
|
|
84
|
+
if (typeof node.description !== "string" || typeof node.op !== "string" ||
|
|
85
|
+
(root ? node.id !== "program" || node.op !== "program" : !/^a[1-9]\d*$/.test(node.id) || node.op === "program") || seen.has(node.id)) {
|
|
86
|
+
throw invalidSnapshot(`Invalid or duplicate trace node "${node.id}".`, path);
|
|
87
|
+
}
|
|
88
|
+
seen.add(node.id);
|
|
89
|
+
requireObject(node.args, `${path}.args`);
|
|
90
|
+
requireArray(node.children, `${path}.children`);
|
|
91
|
+
const extension = requiredTraceExtension(ProgramClass, node.op);
|
|
92
|
+
if (extension !== undefined) extensions.add(extension);
|
|
93
|
+
node.children.forEach(child => visit(child, false));
|
|
94
|
+
}
|
|
95
|
+
visit(trace, true);
|
|
96
|
+
// The constructor derives the next action number from the node count.
|
|
97
|
+
for (let index = 1; index < seen.size; index++) {
|
|
98
|
+
if (!seen.has(`a${index}`)) throw invalidSnapshot("Trace action IDs must be consecutive.", path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateSemanticProperties(program, value, property) {
|
|
103
|
+
let parsed;
|
|
104
|
+
try { parsed = parseSemanticPath(property); } catch (error) {
|
|
105
|
+
if (!isPlainObject(value) || !Object.keys(value).length) throw error;
|
|
106
|
+
for (const [key, child] of Object.entries(value)) validateSemanticProperties(program, child, `${property}.${key}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const leaf = parsed.path.at(-1);
|
|
110
|
+
if (["data", "coordinate", "scale"].includes(leaf)) validateUserId(value, property);
|
|
111
|
+
if (leaf === "field" && (typeof value !== "string" || value.length === 0)) throw invalidSnapshot(`Invalid field at ${property}.`, property);
|
|
112
|
+
validateStoredSemanticValue(program, parsed, value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function validateProgramState(program, path) {
|
|
116
|
+
const semantic = program.semanticSpec;
|
|
117
|
+
exactKeys(semantic, ["datasets", "layers", "scales", "coordinates", "guides", "title"], `${path}.semanticSpec`);
|
|
118
|
+
const resources = new Map();
|
|
119
|
+
for (const [collection, kind, referenceKind] of [
|
|
120
|
+
["datasets", "dataset", "data"], ["layers", "layer", "mark"],
|
|
121
|
+
["scales", "scale", "scale"], ["coordinates", "coordinate", "coordinate"]
|
|
122
|
+
]) {
|
|
123
|
+
requireArray(semantic[collection], `${path}.semanticSpec.${collection}`);
|
|
124
|
+
const entries = new Map();
|
|
125
|
+
for (const resource of semantic[collection]) {
|
|
126
|
+
requireObject(resource, `${path}.semanticSpec.${collection}`);
|
|
127
|
+
validateUserId(resource.id, `${kind} id`);
|
|
128
|
+
if (entries.has(resource.id)) throw invalidSnapshot(`Duplicate ${kind} "${resource.id}".`, path);
|
|
129
|
+
entries.set(resource.id, resource);
|
|
130
|
+
}
|
|
131
|
+
resources.set(referenceKind, entries);
|
|
132
|
+
for (const resource of entries.values()) {
|
|
133
|
+
for (const [key, value] of Object.entries(resource)) {
|
|
134
|
+
if (key !== "id") validateSemanticProperties(program, value, `${kind}[${resource.id}].${key}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const [key, prefix] of [["guides", "guide"], ["title", "title"]]) {
|
|
139
|
+
requireObject(semantic[key], `${path}.semanticSpec.${key}`);
|
|
140
|
+
for (const [name, value] of Object.entries(semantic[key])) validateSemanticProperties(program, value, `${prefix}.${name}`);
|
|
141
|
+
}
|
|
142
|
+
for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
|
|
143
|
+
for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
|
|
144
|
+
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
145
|
+
const selections = program.materializationConfigs.selections ?? {};
|
|
146
|
+
requireObject(selections, `${path}.materializationConfigs.selections`);
|
|
147
|
+
for (const [id, selection] of Object.entries(selections)) {
|
|
148
|
+
validateUserId(id, "Selection id");
|
|
149
|
+
exactKeys(selection, ["target", "selector"], `${path}.materializationConfigs.selections.${id}`);
|
|
150
|
+
validateUserId(selection.target, "Selection target");
|
|
151
|
+
normalizeMarkSelector(selection.selector);
|
|
152
|
+
}
|
|
153
|
+
resources.set("selection", new Map(Object.entries(selections)));
|
|
154
|
+
for (const reference of collectAllResourceReferences(program)) {
|
|
155
|
+
if (reference.kind === "data") resolveDatasetReference(program, reference.id);
|
|
156
|
+
else if (!resources.get(reference.kind)?.has(reference.id)) {
|
|
157
|
+
throw invalidSnapshot(`Unknown ${reference.kind} "${reference.id}" referenced by ${reference.ownerKind} "${reference.ownerId}".`, path);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [id, scale] of Object.entries(program.resolvedScales)) {
|
|
161
|
+
requireObject(scale, `${path}.resolvedScales.${id}`);
|
|
162
|
+
requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
|
|
163
|
+
requireArray(scale.range, `${path}.resolvedScales.${id}.range`);
|
|
164
|
+
if (!resources.get("scale").has(id) || scale.type !== resources.get("scale").get(id).type) {
|
|
165
|
+
throw invalidSnapshot(`Resolved scale "${id}" has no matching semantic owner/type.`, path);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const id of Object.keys(program.markConfigs)) {
|
|
169
|
+
if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
|
|
170
|
+
}
|
|
171
|
+
for (const kind of ["data", "mark"]) {
|
|
172
|
+
const visited = new Set();
|
|
173
|
+
const active = new Set();
|
|
174
|
+
function visit(resource) {
|
|
175
|
+
if (active.has(resource.id)) throw invalidSnapshot(`Cyclic ${kind} source "${resource.id}".`, path);
|
|
176
|
+
if (visited.has(resource.id)) return;
|
|
177
|
+
active.add(resource.id);
|
|
178
|
+
if (resource.source !== undefined) visit(kind === "data" ? resolveDatasetReference(program, resource.source) : resources.get(kind).get(resource.source));
|
|
179
|
+
active.delete(resource.id);
|
|
180
|
+
visited.add(resource.id);
|
|
181
|
+
}
|
|
182
|
+
for (const resource of resources.get(kind).values()) visit(resource);
|
|
183
|
+
}
|
|
184
|
+
validateGraphic(program.graphicSpec, `${path}.graphicSpec`);
|
|
185
|
+
}
|