ggaction 0.0.15 → 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 +68 -0
- package/README.md +22 -2
- 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 +33 -6
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/boxPlots/components.js +6 -8
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +3 -4
- package/src/actions/boxPlots/materialize.js +1 -1
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +6 -8
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +1 -1
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +2 -2
- package/src/actions/charts/endpoints.js +4 -4
- 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/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +1 -1
- package/src/actions/charts/polar.js +2 -2
- package/src/actions/charts/radar.js +1 -1
- package/src/actions/charts/radial.js +2 -2
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +1 -1
- package/src/actions/composition/actions.js +20 -24
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +1 -1
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +2 -2
- package/src/actions/data/bin2d.js +6 -8
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +2 -2
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +5 -12
- package/src/actions/data/density.js +3 -3
- package/src/actions/data/derived.js +10 -14
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +35 -23
- package/src/actions/data/filter.js +14 -20
- 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 +2 -2
- package/src/actions/data/index.js +55 -54
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +2 -2
- package/src/actions/data/regression.js +5 -6
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +13 -33
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +2 -2
- package/src/actions/data/timeUnit.js +2 -2
- package/src/actions/data/window.js +2 -2
- 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 +2 -2
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/histogram.js +1 -1
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/offset.js +2 -2
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/index.js +4 -4
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +4 -5
- package/src/actions/encodings/ruleAppearance.js +1 -1
- package/src/actions/encodings/seriesLayout.js +1 -1
- package/src/actions/encodings/stroke.js +1 -1
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +11 -17
- package/src/actions/errorBands/edit.js +13 -131
- package/src/actions/errorBars/create.js +8 -13
- package/src/actions/errorBars/edit.js +10 -121
- package/src/actions/facets/actions.js +30 -16
- package/src/actions/facets/derive.js +8 -2
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +5 -6
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +4 -5
- package/src/actions/gradientPlots/materialize.js +2 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- 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 +9 -9
- package/src/actions/guides/axes/lines.js +9 -7
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- 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 +4 -4
- package/src/actions/guides/axes/ticks.js +13 -4
- package/src/actions/guides/axes/titles.js +10 -10
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/guides.js +1 -1
- package/src/actions/guides/legends/blocks.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +4 -4
- package/src/actions/guides/legends/categorical/components.js +38 -33
- package/src/actions/guides/legends/categorical/layout.js +1 -1
- package/src/actions/guides/legends/categorical/symbols.js +27 -30
- package/src/actions/guides/legends/continuous/common.js +46 -8
- package/src/actions/guides/legends/continuous/gradient.js +89 -155
- package/src/actions/guides/legends/continuous/interval.js +13 -13
- package/src/actions/guides/legends/continuous/opacity.js +30 -79
- package/src/actions/guides/legends/continuous/stroke.js +22 -322
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/lane.js +6 -6
- package/src/actions/guides/legends/remove.js +4 -5
- package/src/actions/guides/legends/size.js +21 -82
- package/src/actions/guides/legends/strokeWidth.js +17 -77
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +5 -5
- package/src/actions/guides/polar/axes/lines.js +9 -15
- package/src/actions/guides/polar/axes/ticks.js +9 -15
- package/src/actions/guides/polar/axes/titles.js +5 -5
- package/src/actions/guides/polar/grids.js +11 -13
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +3 -3
- package/src/actions/marks/area/actions.js +4 -4
- package/src/actions/marks/bar/create.js +1 -1
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/bar/materialize.js +12 -38
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/point/create.js +1 -1
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +16 -24
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/references.js +8 -10
- package/src/actions/marks/remove.js +5 -49
- package/src/actions/marks/rule/actions.js +4 -4
- package/src/actions/marks/text/actions.js +5 -5
- package/src/actions/marks/text/layout.js +12 -20
- package/src/actions/marks/text/removal.js +1 -1
- package/src/actions/marks/text/selection.js +4 -5
- package/src/actions/marks/tick/actions.js +3 -3
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +5 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +10 -3
- package/src/actions/primitives/semanticAction.js +7 -4
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/regression/components.js +10 -14
- package/src/actions/regression/create.js +3 -4
- package/src/actions/regression/edit.js +3 -4
- package/src/actions/resources/remove.js +12 -11
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/offset.js +2 -2
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +25 -36
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +2 -2
- package/src/actions/theme/reconcile.js +65 -121
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +1 -1
- package/src/actions/violinPlots/edit.js +1 -1
- package/src/core/action.js +107 -35
- 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 +1 -1
- package/src/core/resourceReferences.js +15 -6
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/diagnostics.js +1 -0
- package/src/grammar/impute.js +3 -2
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/graphicBounds.js +11 -11
- package/src/layout/labels.js +11 -10
- 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/facetGuides/placement.js +1 -1
- package/src/materialization/facetGuides/preparation.js +1 -1
- package/src/materialization/facetHeaders.js +11 -10
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/text.js +2 -2
- 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/index.js +5 -4
- 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 +10 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +8 -6
- package/src/selectors/layers.js +6 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +2 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +4 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +5 -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 +34 -1
package/src/layout/text.js
CHANGED
|
@@ -5,12 +5,12 @@ export {
|
|
|
5
5
|
resolveTextBounds
|
|
6
6
|
} from "../core/textMetrics.js";
|
|
7
7
|
|
|
8
|
-
function wrapCodePoints(text, maxWidth, style) {
|
|
8
|
+
function wrapCodePoints(text, maxWidth, style, profile) {
|
|
9
9
|
const lines = [];
|
|
10
10
|
let line = "";
|
|
11
11
|
for (const codePoint of [...text]) {
|
|
12
12
|
const candidate = line + codePoint;
|
|
13
|
-
if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
|
|
13
|
+
if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
|
|
14
14
|
lines.push(line);
|
|
15
15
|
line = codePoint;
|
|
16
16
|
} else {
|
|
@@ -21,22 +21,22 @@ function wrapCodePoints(text, maxWidth, style) {
|
|
|
21
21
|
return lines;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function wrapWords(text, maxWidth, style) {
|
|
24
|
+
function wrapWords(text, maxWidth, style, profile) {
|
|
25
25
|
const lines = [];
|
|
26
26
|
let line = "";
|
|
27
27
|
for (const word of text.trim().split(/\s+/u)) {
|
|
28
|
-
if (measureTextWidth(word, style) > maxWidth) {
|
|
28
|
+
if (measureTextWidth(word, style, profile) > maxWidth) {
|
|
29
29
|
if (line !== "") {
|
|
30
30
|
lines.push(line);
|
|
31
31
|
line = "";
|
|
32
32
|
}
|
|
33
|
-
const fragments = wrapCodePoints(word, maxWidth, style);
|
|
33
|
+
const fragments = wrapCodePoints(word, maxWidth, style, profile);
|
|
34
34
|
lines.push(...fragments.slice(0, -1));
|
|
35
35
|
line = fragments.at(-1);
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
const candidate = line === "" ? word : `${line} ${word}`;
|
|
39
|
-
if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
|
|
39
|
+
if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
|
|
40
40
|
lines.push(line);
|
|
41
41
|
line = word;
|
|
42
42
|
} else {
|
|
@@ -50,7 +50,8 @@ function wrapWords(text, maxWidth, style) {
|
|
|
50
50
|
export function wrapText(text, {
|
|
51
51
|
maxWidth,
|
|
52
52
|
mode = "word",
|
|
53
|
-
style
|
|
53
|
+
style,
|
|
54
|
+
profile
|
|
54
55
|
} = {}) {
|
|
55
56
|
if (maxWidth === undefined) return [text];
|
|
56
57
|
if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
|
|
@@ -60,6 +61,6 @@ export function wrapText(text, {
|
|
|
60
61
|
throw new Error(`Unsupported text wrap mode "${mode}".`);
|
|
61
62
|
}
|
|
62
63
|
return mode === "word"
|
|
63
|
-
? wrapWords(text, maxWidth, style)
|
|
64
|
-
: wrapCodePoints(text, maxWidth, style);
|
|
64
|
+
? wrapWords(text, maxWidth, style, profile)
|
|
65
|
+
: wrapCodePoints(text, maxWidth, style, profile);
|
|
65
66
|
}
|
package/src/layout/title.js
CHANGED
|
@@ -4,16 +4,16 @@ function lineCenters(lines, style, lineHeight, start) {
|
|
|
4
4
|
return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function buildTitleReadingBlock({ text, subtitle }, config) {
|
|
7
|
+
export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
|
|
8
8
|
const titleLines = wrapText(text, {
|
|
9
9
|
maxWidth: config.maxWidth,
|
|
10
10
|
mode: config.wrap,
|
|
11
|
-
style: config.titleStyle
|
|
11
|
+
style: config.titleStyle, profile
|
|
12
12
|
});
|
|
13
13
|
const subtitleLines = subtitle === undefined ? [] : wrapText(subtitle, {
|
|
14
14
|
maxWidth: config.maxWidth,
|
|
15
15
|
mode: config.wrap,
|
|
16
|
-
style: config.subtitleStyle
|
|
16
|
+
style: config.subtitleStyle, profile
|
|
17
17
|
});
|
|
18
18
|
const titleLineHeight = config.lineHeight ?? config.titleStyle.fontSize * 1.2;
|
|
19
19
|
const subtitleLineHeight = config.lineHeight ??
|
|
@@ -36,8 +36,8 @@ export function buildTitleReadingBlock({ text, subtitle }, config) {
|
|
|
36
36
|
? titleBottom
|
|
37
37
|
: subtitleCenters.at(-1) + config.subtitleStyle.fontSize / 2;
|
|
38
38
|
const widths = [
|
|
39
|
-
...titleLines.map(line => measureTextWidth(line, config.titleStyle)),
|
|
40
|
-
...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle))
|
|
39
|
+
...titleLines.map(line => measureTextWidth(line, config.titleStyle, profile)),
|
|
40
|
+
...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle, profile))
|
|
41
41
|
];
|
|
42
42
|
return {
|
|
43
43
|
titleLines,
|
|
@@ -61,7 +61,7 @@ export function alignedTextAnchor(start, length, align) {
|
|
|
61
61
|
return start + length;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
|
|
64
|
+
function axisAlignedTextBounds({ x, y, text, style, align, rotation }, profile) {
|
|
65
65
|
return resolveTextBounds({
|
|
66
66
|
x,
|
|
67
67
|
y,
|
|
@@ -70,7 +70,7 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
|
|
|
70
70
|
textAlign: align,
|
|
71
71
|
textBaseline: "middle",
|
|
72
72
|
rotation
|
|
73
|
-
});
|
|
73
|
+
}, profile);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function unionBounds(bounds) {
|
|
@@ -82,7 +82,7 @@ function unionBounds(bounds) {
|
|
|
82
82
|
}), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function resolveTitleComponentBounds(component, style) {
|
|
85
|
+
export function resolveTitleComponentBounds(component, style, profile) {
|
|
86
86
|
return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
|
|
87
87
|
x: Array.isArray(component.x) ? component.x[index] : component.x,
|
|
88
88
|
y: Array.isArray(component.y) ? component.y[index] : component.y,
|
|
@@ -90,7 +90,7 @@ export function resolveTitleComponentBounds(component, style) {
|
|
|
90
90
|
style,
|
|
91
91
|
align: component.textAlign,
|
|
92
92
|
rotation: component.rotation
|
|
93
|
-
})));
|
|
93
|
+
}, profile)));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function unionTitleBounds(bounds) {
|
|
@@ -92,7 +92,7 @@ function attachParentLegend(program, prepared, plot, layout) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function assertSharedLegendFits(program, id, layout, plot) {
|
|
95
|
-
const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id);
|
|
95
|
+
const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
|
|
96
96
|
if (bounds === undefined) {
|
|
97
97
|
throw new Error("Facet shared legend requires measurable concrete bounds.");
|
|
98
98
|
}
|
|
@@ -197,7 +197,7 @@ export function prepareSharedFacetLegend(program) {
|
|
|
197
197
|
if (roots.length === 0) {
|
|
198
198
|
throw new Error("Shared facet legend has no concrete guide graphics.");
|
|
199
199
|
}
|
|
200
|
-
const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots);
|
|
200
|
+
const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots, source.materializationConfigs.textMetrics);
|
|
201
201
|
if (bounds === undefined) {
|
|
202
202
|
throw new Error("Shared facet legend has no measurable concrete bounds.");
|
|
203
203
|
}
|
|
@@ -139,13 +139,13 @@ function roleDescriptors(program, headers) {
|
|
|
139
139
|
return [...columns, ...rows];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
function laneThickness(descriptors, role) {
|
|
142
|
+
function laneThickness(descriptors, role, profile) {
|
|
143
143
|
const visible = descriptors.filter(item => item.role === role && item.text !== "");
|
|
144
144
|
if (visible.length === 0) return 0;
|
|
145
145
|
if (role === "column") {
|
|
146
146
|
return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
|
|
147
147
|
}
|
|
148
|
-
return Math.max(...visible.map(item => measureTextWidth(item.text, item.config) +
|
|
148
|
+
return Math.max(...visible.map(item => measureTextWidth(item.text, item.config, profile) +
|
|
149
149
|
item.config.offset));
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -163,7 +163,7 @@ export function prepareFacetHeaders(program, headersConfig) {
|
|
|
163
163
|
};
|
|
164
164
|
if (headers.mode === "roles") {
|
|
165
165
|
const column = resolveFacetHeaderConfig(headers, "column");
|
|
166
|
-
const columnThickness = laneThickness(descriptors, "column");
|
|
166
|
+
const columnThickness = laneThickness(descriptors, "column", program.materializationConfigs.textMetrics);
|
|
167
167
|
if (program.compositionSpec.facet.grid === undefined) {
|
|
168
168
|
for (const descriptor of descriptors) {
|
|
169
169
|
if (descriptor.text !== "") {
|
|
@@ -176,7 +176,7 @@ export function prepareFacetHeaders(program, headersConfig) {
|
|
|
176
176
|
} else {
|
|
177
177
|
headerLayout.outer[column.side] = columnThickness;
|
|
178
178
|
const row = resolveFacetHeaderConfig(headers, "row");
|
|
179
|
-
headerLayout.outer[row.side] = laneThickness(descriptors, "row");
|
|
179
|
+
headerLayout.outer[row.side] = laneThickness(descriptors, "row", program.materializationConfigs.textMetrics);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
return { headers, descriptors, headerLayout };
|
|
@@ -287,10 +287,10 @@ function styledItem(item) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
function assertRoleHeadersFit(records, items, layout) {
|
|
290
|
+
function assertRoleHeadersFit(records, items, layout, profile) {
|
|
291
291
|
const bounds = items.map((item, index) => records[index].descriptor.text === ""
|
|
292
292
|
? undefined
|
|
293
|
-
: resolveTextBounds(item.properties));
|
|
293
|
+
: resolveTextBounds(item.properties, profile));
|
|
294
294
|
for (const [index, current] of bounds.entries()) {
|
|
295
295
|
if (current === undefined) continue;
|
|
296
296
|
if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
|
|
@@ -305,11 +305,11 @@ function assertRoleHeadersFit(records, items, layout) {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
function assertLegacyHeadersFit(items, layout, plotById) {
|
|
308
|
+
function assertLegacyHeadersFit(items, layout, plotById, profile) {
|
|
309
309
|
const previous = [];
|
|
310
310
|
for (let index = 0; index < items.length; index += 1) {
|
|
311
311
|
if (items[index].properties.text === "") continue;
|
|
312
|
-
const bounds = resolveTextBounds(items[index].properties);
|
|
312
|
+
const bounds = resolveTextBounds(items[index].properties, profile);
|
|
313
313
|
const cell = layout.children[index];
|
|
314
314
|
const plot = plotById.get(cell.id);
|
|
315
315
|
if (bounds.left < 0 || bounds.right > layout.width ||
|
|
@@ -325,6 +325,7 @@ function assertLegacyHeadersFit(items, layout, plotById) {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
export function materializeFacetHeaders(program, layout, plots, prepared) {
|
|
328
|
+
const profile = program.materializationConfigs.textMetrics;
|
|
328
329
|
const cells = new Map(layout.children.map(cell => [cell.id, cell]));
|
|
329
330
|
const plotById = new Map(plots.map(plot => [plot.id, plot]));
|
|
330
331
|
const records = prepared.descriptors.map(descriptor => {
|
|
@@ -335,9 +336,9 @@ export function materializeFacetHeaders(program, layout, plots, prepared) {
|
|
|
335
336
|
});
|
|
336
337
|
const items = records.map(styledItem);
|
|
337
338
|
if (prepared.headers.mode === "legacy") {
|
|
338
|
-
assertLegacyHeadersFit(items, layout, plotById);
|
|
339
|
+
assertLegacyHeadersFit(items, layout, plotById, profile);
|
|
339
340
|
} else {
|
|
340
|
-
assertRoleHeadersFit(records, items, layout);
|
|
341
|
+
assertRoleHeadersFit(records, items, layout, profile);
|
|
341
342
|
}
|
|
342
343
|
const id = `${program.compositionSpec.id}-headers`;
|
|
343
344
|
return program
|
|
@@ -4,12 +4,12 @@ import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
|
|
|
4
4
|
|
|
5
5
|
// Only domain-owned guides participate. Extension primitives may deliberately
|
|
6
6
|
// overlap, and a combined categorical/size legend is one occupied group.
|
|
7
|
-
export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
|
|
7
|
+
export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig, profile) {
|
|
8
8
|
const blocks = [];
|
|
9
9
|
const append = (id, kind, position, ids) => {
|
|
10
10
|
if (position === undefined) return;
|
|
11
11
|
const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
|
|
12
|
-
const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
|
|
12
|
+
const bounds = unionConcreteGraphicBounds(graphicSpec, existing, profile);
|
|
13
13
|
if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
|
|
14
14
|
};
|
|
15
15
|
append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
|
|
@@ -46,7 +46,7 @@ export function withGuideLayoutTransaction(program, apply) {
|
|
|
46
46
|
context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
|
|
47
47
|
}
|
|
48
48
|
const next = result._clone({ context });
|
|
49
|
-
assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
|
|
49
|
+
assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig, next.materializationConfigs.textMetrics));
|
|
50
50
|
return next;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -18,6 +18,16 @@ const GRIDS = Object.freeze([
|
|
|
18
18
|
Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
+
export function planAxisTickGridRematerialization(program, channel) {
|
|
22
|
+
const ticks = program.guideConfigs.axis?.[channel]?.ticks;
|
|
23
|
+
const role = channel === "x" ? "vertical" : "horizontal";
|
|
24
|
+
const config = program.guideConfigs.grid?.[role];
|
|
25
|
+
return buildMaterializationPlan({ guides:
|
|
26
|
+
config?.inferredValues === true && config.scale === ticks?.scale
|
|
27
|
+
? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : []
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
function usesPositionalScale(program, id) {
|
|
22
32
|
return program.semanticSpec.layers.some(layer =>
|
|
23
33
|
!isSourceOwnedText(layer) && (
|
|
@@ -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
|
-
);
|
|
105
|
+
, program.materializationConfigs.textMetrics);
|
|
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 };
|
|
@@ -214,7 +214,7 @@ function pointPlacementGeometry(program, source, item) {
|
|
|
214
214
|
const objectBounds = unionConcreteGraphicBounds(
|
|
215
215
|
program.graphicSpec,
|
|
216
216
|
item.graphicIds
|
|
217
|
-
);
|
|
217
|
+
, program.materializationConfigs.textMetrics);
|
|
218
218
|
const corners = objectBounds === undefined ? [] : [
|
|
219
219
|
{ x: objectBounds.left, y: objectBounds.top },
|
|
220
220
|
{ x: objectBounds.right, y: objectBounds.top },
|
|
@@ -391,7 +391,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
391
391
|
dx: resolvedConfig.dx,
|
|
392
392
|
dy: resolvedConfig.dy
|
|
393
393
|
}
|
|
394
|
-
});
|
|
394
|
+
}, program.materializationConfigs.textMetrics);
|
|
395
395
|
if (!resolved.visible) return [];
|
|
396
396
|
if (
|
|
397
397
|
resolved.anchor.startsWith("outside") &&
|
|
@@ -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
|
+
}
|