ggaction 0.0.15 → 0.0.17
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 +88 -0
- package/README.md +23 -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 +40 -7
- 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 +3 -3
- 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 +23 -17
- package/src/actions/data/density.js +4 -4
- package/src/actions/data/derived.js +25 -15
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +54 -29
- package/src/actions/data/filter.js +29 -24
- 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 +58 -54
- package/src/actions/data/interval.js +4 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +2 -2
- package/src/actions/data/regression.js +9 -7
- package/src/actions/data/revise.js +80 -0
- package/src/actions/data/revisionBindings.js +13 -33
- package/src/actions/data/shared.js +30 -2
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +2 -2
- package/src/actions/data/window.js +2 -2
- package/src/actions/encodings/angle.js +5 -2
- package/src/actions/encodings/appearance.js +10 -6
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +2 -2
- package/src/actions/encodings/color/continuous.js +4 -1
- package/src/actions/encodings/color/index.js +5 -2
- 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 +6 -3
- 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/position/resolve.js +6 -1
- package/src/actions/encodings/ranged.js +11 -6
- package/src/actions/encodings/remove.js +4 -5
- package/src/actions/encodings/ruleAppearance.js +5 -2
- package/src/actions/encodings/seriesLayout.js +1 -1
- package/src/actions/encodings/stroke.js +5 -2
- package/src/actions/encodings/strokeDash.js +8 -3
- package/src/actions/encodings/text.js +7 -2
- 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 +9 -2
- package/src/actions/marks/bar/edit.js +13 -6
- 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 +9 -2
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +20 -26
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/references.js +8 -10
- package/src/actions/marks/remove.js +5 -49
- package/src/actions/marks/rule/actions.js +28 -12
- package/src/actions/marks/text/actions.js +30 -11
- 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 +26 -11
- 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 +44 -7
- package/src/actions/primitives/semanticValidation/dataset.js +14 -4
- package/src/actions/primitives/semanticValidation/layer.js +22 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/components.js +10 -14
- package/src/actions/regression/create.js +28 -8
- package/src/actions/regression/edit.js +50 -16
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/resources/remove.js +12 -11
- package/src/actions/scales/channels.js +11 -11
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/create.js +3 -2
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/offset.js +2 -2
- package/src/actions/scales/parallel.js +2 -2
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +28 -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 +67 -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 +109 -36
- 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 +6 -2
- 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/bin.js +39 -10
- package/src/grammar/datasetSchema.js +386 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/impute.js +3 -2
- package/src/grammar/interval.js +32 -1
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/regression/derive.js +57 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +51 -22
- package/src/grammar/regression/parameters.js +56 -4
- package/src/grammar/scales/definition.js +7 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/graphicBounds.js +11 -11
- package/src/grammar/schemas/semanticPath.js +3 -2
- package/src/grammar/sort.js +88 -0
- package/src/grammar/summary.js +117 -19
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +21 -11
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- 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/bars/resolve.js +4 -2
- 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/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/resolve.js +67 -4
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/text.js +6 -4
- 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 +147 -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/inspection.d.ts +17 -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 +181 -28
|
@@ -3,7 +3,7 @@ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
|
|
|
3
3
|
|
|
4
4
|
// Measure item content before choosing its edge. Family owners supply the
|
|
5
5
|
// sample dimensions and formatted labels; this module does not inspect scales.
|
|
6
|
-
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
6
|
+
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, profile) {
|
|
7
7
|
const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
|
|
8
8
|
? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
|
|
9
9
|
const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
|
|
@@ -19,17 +19,17 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
19
19
|
...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
|
|
20
20
|
const side = ["left", "right"].includes(config.position);
|
|
21
21
|
const titleVisible = config.titleVisible !== false;
|
|
22
|
-
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
|
|
22
|
+
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle, profile) : 0;
|
|
23
23
|
const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
|
|
24
24
|
const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
|
|
25
|
-
text => measureTextWidth(text, config.labels)
|
|
25
|
+
text => measureTextWidth(text, config.labels, profile)
|
|
26
26
|
));
|
|
27
27
|
let symbolX;
|
|
28
28
|
let labelX;
|
|
29
29
|
let itemY;
|
|
30
30
|
let title;
|
|
31
31
|
if (config.layout === "legacy-bottom") {
|
|
32
|
-
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
|
|
32
|
+
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
|
|
33
33
|
const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
|
|
34
34
|
const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
|
|
35
35
|
let cursor = x;
|
|
@@ -54,7 +54,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
54
54
|
itemY = labels.map((_, index) => firstY + index * pitch);
|
|
55
55
|
title = { x, y: plot.y + 20, align: "left" };
|
|
56
56
|
} else {
|
|
57
|
-
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
|
|
57
|
+
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
|
|
58
58
|
const inline = config.titlePosition === "left" && titleVisible;
|
|
59
59
|
const prefix = inline ? titleWidth + 20 : 0;
|
|
60
60
|
const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
|
|
@@ -79,7 +79,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
79
79
|
}
|
|
80
80
|
const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
|
|
81
81
|
x, y, text, ...style, textAlign: align, textBaseline: "middle"
|
|
82
|
-
});
|
|
82
|
+
}, profile);
|
|
83
83
|
const bounds = [
|
|
84
84
|
...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
|
|
85
85
|
...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
|
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) {
|
|
@@ -4,6 +4,7 @@ import { findLayer } from "../../selectors/layers.js";
|
|
|
4
4
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
5
5
|
import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
|
|
6
6
|
import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
|
|
7
|
+
import { applyItemMissingPolicy } from "../../grammar/itemMissing.js";
|
|
7
8
|
|
|
8
9
|
export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
|
|
9
10
|
export const DEFAULT_BAR_STROKE = "white";
|
|
@@ -37,10 +38,11 @@ export function requireCompleteBar(program, id) {
|
|
|
37
38
|
if (layer?.mark?.type !== "bar") {
|
|
38
39
|
throw new Error(`Unknown bar mark "${id}".`);
|
|
39
40
|
}
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
41
|
+
const sourceDataset = findDataset(program, layer.data);
|
|
42
|
+
if (sourceDataset === undefined) {
|
|
42
43
|
throw new Error(`Bar mark "${id}" requires an existing dataset.`);
|
|
43
44
|
}
|
|
45
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
44
46
|
const graphic = program.graphicSpec.objects[id];
|
|
45
47
|
const roundedCollection = graphic?.type === "collection" &&
|
|
46
48
|
Object.hasOwn(program.markConfigs[id]?.barAppearance ?? {}, "cornerRadius") &&
|
|
@@ -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
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
let activeReplacements;
|
|
2
|
+
|
|
3
|
+
export function withLogicalDataReplacements(replacements, operation) {
|
|
4
|
+
const previous = activeReplacements;
|
|
5
|
+
activeReplacements = replacements;
|
|
6
|
+
try {
|
|
7
|
+
return operation();
|
|
8
|
+
} finally {
|
|
9
|
+
activeReplacements = previous;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function comparableDataIdentity(id) {
|
|
14
|
+
if (activeReplacements === undefined) return id;
|
|
15
|
+
for (const [before, after] of activeReplacements) {
|
|
16
|
+
if (after === id) return before;
|
|
17
|
+
}
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
@@ -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) && (
|
|
@@ -35,6 +35,36 @@ import { resolveOffsetScalePolicy } from "./policies/offset.js";
|
|
|
35
35
|
import { resolveBinnedPositionDomain } from "./policies/binnedPosition.js";
|
|
36
36
|
import { resolveSeriesLayoutDomain } from "./policies/series.js";
|
|
37
37
|
import { OFFSET_POSITION_CHANNELS } from "../../core/vocabulary.js";
|
|
38
|
+
import { annotateError } from "../../core/diagnostics.js";
|
|
39
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
40
|
+
import { comparableDataIdentity } from "../revisionIdentity.js";
|
|
41
|
+
|
|
42
|
+
const bindingSignatures = new WeakMap();
|
|
43
|
+
|
|
44
|
+
function scaleBindingSignature(channel, consumers, { comparable = false } = {}) {
|
|
45
|
+
return consumers.map(consumer => ({
|
|
46
|
+
channel: consumer.channel ?? channel,
|
|
47
|
+
coordinate: consumer.layer.coordinate,
|
|
48
|
+
data: comparable
|
|
49
|
+
? comparableDataIdentity(consumer.layer.data)
|
|
50
|
+
: consumer.layer.data,
|
|
51
|
+
field: consumer.encoding.field,
|
|
52
|
+
datum: consumer.encoding.datum,
|
|
53
|
+
fieldType: consumer.encoding.fieldType,
|
|
54
|
+
temporalUnit: consumer.encoding.temporalUnit,
|
|
55
|
+
aggregate: consumer.encoding.aggregate,
|
|
56
|
+
bin: consumer.encoding.bin
|
|
57
|
+
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function rememberResolvedScaleBinding(resolvedScale, channel, consumers) {
|
|
61
|
+
if (resolvedScale !== undefined) {
|
|
62
|
+
bindingSignatures.set(
|
|
63
|
+
resolvedScale,
|
|
64
|
+
cloneAndFreeze(scaleBindingSignature(channel, consumers))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
|
|
39
69
|
function resolveDefaultDomain({
|
|
40
70
|
scale,
|
|
@@ -206,12 +236,22 @@ export function resolveScaleMaterialization(options) {
|
|
|
206
236
|
.flatMap(item => item.values)
|
|
207
237
|
.filter(value => value !== undefined);
|
|
208
238
|
const preserved = resolvedScales[id]?.domain;
|
|
239
|
+
const bindingSignature = scaleBindingSignature(channel, consumers);
|
|
240
|
+
const comparableBindingSignature = scaleBindingSignature(channel, consumers, {
|
|
241
|
+
comparable: true
|
|
242
|
+
});
|
|
243
|
+
const priorSignature = resolvedScales[id] === undefined
|
|
244
|
+
? undefined
|
|
245
|
+
: bindingSignatures.get(resolvedScales[id]);
|
|
246
|
+
const samePreservedMeaning = priorSignature !== undefined &&
|
|
247
|
+
JSON.stringify(priorSignature) === JSON.stringify(comparableBindingSignature);
|
|
209
248
|
if (
|
|
210
249
|
scale.domain === "auto" &&
|
|
211
250
|
Array.isArray(preserved) &&
|
|
212
|
-
|
|
251
|
+
resolvedScales[id]?.type === scale.type &&
|
|
252
|
+
(consumers.some(({ layer }) =>
|
|
213
253
|
markConfigs?.[layer.id]?.markFilter?.empty === true
|
|
214
|
-
)
|
|
254
|
+
) || (scale.emptyDomain === "preserve" && allValues.length === 0 && samePreservedMeaning))
|
|
215
255
|
) {
|
|
216
256
|
return resolveScaleMaterialization({
|
|
217
257
|
...options,
|
|
@@ -222,6 +262,27 @@ export function resolveScaleMaterialization(options) {
|
|
|
222
262
|
}))
|
|
223
263
|
});
|
|
224
264
|
}
|
|
265
|
+
if (
|
|
266
|
+
scale.domain === "auto" && allValues.length === 0 &&
|
|
267
|
+
scale.emptyDomain === "preserve" && Array.isArray(preserved) && !samePreservedMeaning
|
|
268
|
+
) {
|
|
269
|
+
throw annotateError(new Error(`Scale "${id}" requires an explicit domain because its binding changed.`), {
|
|
270
|
+
code: "incompatible-resource",
|
|
271
|
+
operation: "rematerializeScale",
|
|
272
|
+
optionPath: "domain",
|
|
273
|
+
resourceId: id,
|
|
274
|
+
reason: "domain-required"
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (scale.domain === "auto" && allValues.length === 0) {
|
|
278
|
+
throw annotateError(new Error(`Scale "${id}" requires an explicit domain for empty data.`), {
|
|
279
|
+
code: "incompatible-resource",
|
|
280
|
+
operation: "rematerializeScale",
|
|
281
|
+
optionPath: "domain",
|
|
282
|
+
resourceId: id,
|
|
283
|
+
reason: "domain-required"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
225
286
|
if (channel === "size") {
|
|
226
287
|
return resolveSizeScale({ ...scale, values: allValues });
|
|
227
288
|
}
|
|
@@ -389,7 +450,9 @@ export function resolveScaleMaterialization(options) {
|
|
|
389
450
|
};
|
|
390
451
|
}
|
|
391
452
|
}
|
|
392
|
-
|
|
453
|
+
const finalScale = cloneAndFreeze(scale.reverse === true
|
|
393
454
|
? reverseResolvedScale(resolvedScale)
|
|
394
|
-
: resolvedScale;
|
|
455
|
+
: resolvedScale);
|
|
456
|
+
bindingSignatures.set(finalScale, cloneAndFreeze(bindingSignature));
|
|
457
|
+
return finalScale;
|
|
395
458
|
}
|
|
@@ -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 };
|
|
@@ -9,6 +9,7 @@ import { formatTextValue } from "../grammar/text.js";
|
|
|
9
9
|
import { normalizePositionDatum } from "../grammar/positionDatum.js";
|
|
10
10
|
import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
|
|
11
11
|
import { findDataset } from "../selectors/datasets.js";
|
|
12
|
+
import { applyItemMissingPolicy } from "../grammar/itemMissing.js";
|
|
12
13
|
import { findCoordinate } from "../selectors/coordinates.js";
|
|
13
14
|
import { findLayer } from "../selectors/layers.js";
|
|
14
15
|
import { unionConcreteGraphicBounds } from
|
|
@@ -214,7 +215,7 @@ function pointPlacementGeometry(program, source, item) {
|
|
|
214
215
|
const objectBounds = unionConcreteGraphicBounds(
|
|
215
216
|
program.graphicSpec,
|
|
216
217
|
item.graphicIds
|
|
217
|
-
);
|
|
218
|
+
, program.materializationConfigs.textMetrics);
|
|
218
219
|
const corners = objectBounds === undefined ? [] : [
|
|
219
220
|
{ x: objectBounds.left, y: objectBounds.top },
|
|
220
221
|
{ x: objectBounds.right, y: objectBounds.top },
|
|
@@ -391,7 +392,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
391
392
|
dx: resolvedConfig.dx,
|
|
392
393
|
dy: resolvedConfig.dy
|
|
393
394
|
}
|
|
394
|
-
});
|
|
395
|
+
}, program.materializationConfigs.textMetrics);
|
|
395
396
|
if (!resolved.visible) return [];
|
|
396
397
|
if (
|
|
397
398
|
resolved.anchor.startsWith("outside") &&
|
|
@@ -414,10 +415,11 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
function resolveRowTextItems(program, layer, config) {
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
418
|
+
const sourceDataset = findDataset(program, layer.data);
|
|
419
|
+
if (sourceDataset === undefined) {
|
|
419
420
|
throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
|
|
420
421
|
}
|
|
422
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
421
423
|
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
|
|
422
424
|
const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
|
|
423
425
|
? dataset.values.length
|
|
@@ -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
|
+
}
|