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.
Files changed (236) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +22 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +33 -6
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/boxPlots/components.js +6 -8
  13. package/src/actions/boxPlots/create.js +4 -5
  14. package/src/actions/boxPlots/edit.js +3 -4
  15. package/src/actions/boxPlots/materialize.js +1 -1
  16. package/src/actions/canvas/actions.js +3 -3
  17. package/src/actions/canvas/fitting.js +1 -1
  18. package/src/actions/categoryOrder/index.js +6 -8
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +1 -1
  21. package/src/actions/charts/beeswarm.js +1 -1
  22. package/src/actions/charts/density.js +1 -1
  23. package/src/actions/charts/ecdf.js +2 -2
  24. package/src/actions/charts/endpoints.js +4 -4
  25. package/src/actions/charts/heatmap.js +1 -1
  26. package/src/actions/charts/histogram.js +1 -1
  27. package/src/actions/charts/horizon.js +1 -1
  28. package/src/actions/charts/interval-regression.js +2 -2
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/parallel.js +1 -1
  31. package/src/actions/charts/pie.js +1 -1
  32. package/src/actions/charts/polar.js +2 -2
  33. package/src/actions/charts/radar.js +1 -1
  34. package/src/actions/charts/radial.js +2 -2
  35. package/src/actions/charts/raincloud.js +2 -2
  36. package/src/actions/charts/rug-strip.js +2 -2
  37. package/src/actions/charts/scatter.js +1 -1
  38. package/src/actions/composition/actions.js +20 -24
  39. package/src/actions/coordinates/actions.js +1 -1
  40. package/src/actions/coordinates/edit.js +1 -1
  41. package/src/actions/data/basicBin2d.js +4 -5
  42. package/src/actions/data/bin.js +2 -2
  43. package/src/actions/data/bin2d.js +6 -8
  44. package/src/actions/data/bin2dMaterialize.js +4 -5
  45. package/src/actions/data/box.js +4 -4
  46. package/src/actions/data/complete.js +2 -2
  47. package/src/actions/data/computed.js +2 -2
  48. package/src/actions/data/create.js +5 -12
  49. package/src/actions/data/density.js +3 -3
  50. package/src/actions/data/derived.js +10 -14
  51. package/src/actions/data/ecdf.js +2 -2
  52. package/src/actions/data/edit.js +35 -23
  53. package/src/actions/data/filter.js +14 -20
  54. package/src/actions/data/fold.js +2 -2
  55. package/src/actions/data/gradientProfile.js +2 -2
  56. package/src/actions/data/horizon.js +2 -2
  57. package/src/actions/data/impute.js +2 -2
  58. package/src/actions/data/index.js +55 -54
  59. package/src/actions/data/interval.js +2 -2
  60. package/src/actions/data/intervalEdit.js +26 -18
  61. package/src/actions/data/normalize.js +2 -2
  62. package/src/actions/data/regression.js +5 -6
  63. package/src/actions/data/revise.js +48 -0
  64. package/src/actions/data/revisionBindings.js +13 -33
  65. package/src/actions/data/stack.js +2 -2
  66. package/src/actions/data/summary.js +2 -2
  67. package/src/actions/data/timeUnit.js +2 -2
  68. package/src/actions/data/window.js +2 -2
  69. package/src/actions/encodings/angle.js +1 -1
  70. package/src/actions/encodings/appearance.js +4 -4
  71. package/src/actions/encodings/barWidth.js +1 -1
  72. package/src/actions/encodings/channels.js +2 -2
  73. package/src/actions/encodings/color/index.js +1 -1
  74. package/src/actions/encodings/density.js +2 -2
  75. package/src/actions/encodings/histogram.js +1 -1
  76. package/src/actions/encodings/horizon.js +6 -8
  77. package/src/actions/encodings/offset.js +2 -2
  78. package/src/actions/encodings/parallel.js +4 -5
  79. package/src/actions/encodings/pathOrder.js +6 -11
  80. package/src/actions/encodings/position/index.js +4 -4
  81. package/src/actions/encodings/ranged.js +5 -5
  82. package/src/actions/encodings/remove.js +4 -5
  83. package/src/actions/encodings/ruleAppearance.js +1 -1
  84. package/src/actions/encodings/seriesLayout.js +1 -1
  85. package/src/actions/encodings/stroke.js +1 -1
  86. package/src/actions/encodings/strokeDash.js +2 -2
  87. package/src/actions/encodings/text.js +1 -1
  88. package/src/actions/errorBands/create.js +11 -17
  89. package/src/actions/errorBands/edit.js +13 -131
  90. package/src/actions/errorBars/create.js +8 -13
  91. package/src/actions/errorBars/edit.js +10 -121
  92. package/src/actions/facets/actions.js +30 -16
  93. package/src/actions/facets/derive.js +8 -2
  94. package/src/actions/facets/guides.js +4 -5
  95. package/src/actions/facets/replay.js +4 -5
  96. package/src/actions/gradientPlots/components.js +5 -6
  97. package/src/actions/gradientPlots/create.js +4 -5
  98. package/src/actions/gradientPlots/edit.js +4 -5
  99. package/src/actions/gradientPlots/materialize.js +2 -2
  100. package/src/actions/gradientPlots/rebind.js +4 -5
  101. package/src/actions/guides/axes/axes.js +23 -4
  102. package/src/actions/guides/axes/axis.js +2 -2
  103. package/src/actions/guides/axes/edit.js +2 -2
  104. package/src/actions/guides/axes/labels.js +9 -9
  105. package/src/actions/guides/axes/lines.js +9 -7
  106. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  107. package/src/actions/guides/axes/parallel.js +5 -5
  108. package/src/actions/guides/axes/remove.js +4 -4
  109. package/src/actions/guides/axes/tickGroups.js +4 -4
  110. package/src/actions/guides/axes/ticks.js +13 -4
  111. package/src/actions/guides/axes/titles.js +10 -10
  112. package/src/actions/guides/grids/grid.js +18 -29
  113. package/src/actions/guides/guides.js +1 -1
  114. package/src/actions/guides/legends/blocks.js +1 -1
  115. package/src/actions/guides/legends/categorical/actions.js +4 -4
  116. package/src/actions/guides/legends/categorical/components.js +38 -33
  117. package/src/actions/guides/legends/categorical/layout.js +1 -1
  118. package/src/actions/guides/legends/categorical/symbols.js +27 -30
  119. package/src/actions/guides/legends/continuous/common.js +46 -8
  120. package/src/actions/guides/legends/continuous/gradient.js +89 -155
  121. package/src/actions/guides/legends/continuous/interval.js +13 -13
  122. package/src/actions/guides/legends/continuous/opacity.js +30 -79
  123. package/src/actions/guides/legends/continuous/stroke.js +22 -322
  124. package/src/actions/guides/legends/edit.js +1 -1
  125. package/src/actions/guides/legends/focused.js +5 -5
  126. package/src/actions/guides/legends/lane.js +6 -6
  127. package/src/actions/guides/legends/remove.js +4 -5
  128. package/src/actions/guides/legends/size.js +21 -82
  129. package/src/actions/guides/legends/strokeWidth.js +17 -77
  130. package/src/actions/guides/polar/axes/facade.js +4 -4
  131. package/src/actions/guides/polar/axes/labels.js +5 -5
  132. package/src/actions/guides/polar/axes/lines.js +9 -15
  133. package/src/actions/guides/polar/axes/ticks.js +9 -15
  134. package/src/actions/guides/polar/axes/titles.js +5 -5
  135. package/src/actions/guides/polar/grids.js +11 -13
  136. package/src/actions/index.js +2 -0
  137. package/src/actions/intervals/revision.js +126 -0
  138. package/src/actions/marks/arc/actions.js +3 -3
  139. package/src/actions/marks/area/actions.js +4 -4
  140. package/src/actions/marks/bar/create.js +1 -1
  141. package/src/actions/marks/bar/edit.js +1 -1
  142. package/src/actions/marks/bar/materialize.js +12 -38
  143. package/src/actions/marks/line/actions.js +3 -3
  144. package/src/actions/marks/point/create.js +1 -1
  145. package/src/actions/marks/point/edit.js +1 -1
  146. package/src/actions/marks/point/jitter.js +6 -8
  147. package/src/actions/marks/point/materialize.js +16 -24
  148. package/src/actions/marks/point/packing.js +6 -8
  149. package/src/actions/marks/rect/actions.js +3 -3
  150. package/src/actions/marks/references.js +8 -10
  151. package/src/actions/marks/remove.js +5 -49
  152. package/src/actions/marks/rule/actions.js +4 -4
  153. package/src/actions/marks/text/actions.js +5 -5
  154. package/src/actions/marks/text/layout.js +12 -20
  155. package/src/actions/marks/text/removal.js +1 -1
  156. package/src/actions/marks/text/selection.js +4 -5
  157. package/src/actions/marks/tick/actions.js +3 -3
  158. package/src/actions/primitives/createGraphics.js +5 -4
  159. package/src/actions/primitives/editGraphics.js +5 -4
  160. package/src/actions/primitives/graphicProperties.js +6 -0
  161. package/src/actions/primitives/semantic.js +10 -3
  162. package/src/actions/primitives/semanticAction.js +7 -4
  163. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  164. package/src/actions/regression/components.js +10 -14
  165. package/src/actions/regression/create.js +3 -4
  166. package/src/actions/regression/edit.js +3 -4
  167. package/src/actions/resources/remove.js +12 -11
  168. package/src/actions/scales/create.js +1 -1
  169. package/src/actions/scales/edit.js +1 -1
  170. package/src/actions/scales/materialize.js +1 -1
  171. package/src/actions/scales/offset.js +2 -2
  172. package/src/actions/scales/parallel.js +1 -1
  173. package/src/actions/scales/quantitativeColor.js +4 -5
  174. package/src/actions/selection/actions.js +25 -36
  175. package/src/actions/textMetrics/index.js +56 -0
  176. package/src/actions/theme/actions.js +2 -2
  177. package/src/actions/theme/reconcile.js +65 -121
  178. package/src/actions/titles/actions.js +8 -8
  179. package/src/actions/titles/resolve.js +3 -3
  180. package/src/actions/violinPlots/create.js +1 -1
  181. package/src/actions/violinPlots/edit.js +1 -1
  182. package/src/core/action.js +107 -35
  183. package/src/core/diagnostics.js +16 -0
  184. package/src/core/extensionRegistry.js +27 -0
  185. package/src/core/font.js +18 -0
  186. package/src/core/identifiers.js +5 -4
  187. package/src/core/immutable.js +10 -3
  188. package/src/core/optionRequirements.js +8 -0
  189. package/src/core/programState.js +1 -1
  190. package/src/core/resourceReferences.js +15 -6
  191. package/src/core/textMetricProfile.js +28 -0
  192. package/src/core/textMetrics.js +24 -3
  193. package/src/core/validation.js +22 -10
  194. package/src/diagnostics.js +1 -0
  195. package/src/grammar/impute.js +3 -2
  196. package/src/grammar/scales/types.js +8 -0
  197. package/src/grammar/schemas/graphicBounds.js +11 -11
  198. package/src/layout/labels.js +11 -10
  199. package/src/layout/legend.js +4 -4
  200. package/src/layout/legendItems.js +6 -6
  201. package/src/layout/text.js +10 -9
  202. package/src/layout/title.js +9 -9
  203. package/src/materialization/facetGuides/placement.js +1 -1
  204. package/src/materialization/facetGuides/preparation.js +1 -1
  205. package/src/materialization/facetHeaders.js +11 -10
  206. package/src/materialization/guides/layout.js +3 -3
  207. package/src/materialization/scaleGuideDependencies.js +10 -0
  208. package/src/materialization/selection/items/common.js +1 -1
  209. package/src/materialization/text.js +2 -2
  210. package/src/materialization/typography.js +31 -0
  211. package/src/mcp/cli.js +5 -3
  212. package/src/persistence/codec.js +65 -0
  213. package/src/persistence/validation.js +185 -0
  214. package/src/persistence.js +74 -0
  215. package/src/renderers/canvas/index.js +5 -4
  216. package/src/renderers/nativeBackend.js +10 -0
  217. package/src/renderers/options.js +13 -0
  218. package/src/renderers/pdf.js +25 -54
  219. package/src/renderers/png.js +22 -17
  220. package/src/renderers/svg.js +10 -14
  221. package/src/renderers/text.js +1 -11
  222. package/src/selectors/coordinates.js +3 -1
  223. package/src/selectors/datasets.js +8 -6
  224. package/src/selectors/layers.js +6 -4
  225. package/src/selectors/markOwners.js +46 -0
  226. package/src/selectors/scales.js +6 -4
  227. package/src/theme/defaults.js +2 -1
  228. package/src/version.js +2 -0
  229. package/types/accessibility.d.ts +37 -0
  230. package/types/basic.d.ts +4 -2
  231. package/types/diagnostics.d.ts +15 -0
  232. package/types/index.d.ts +5 -0
  233. package/types/pdf.d.ts +9 -0
  234. package/types/persistence.d.ts +7 -0
  235. package/types/png.d.ts +9 -0
  236. package/types/program.d.ts +34 -1
@@ -0,0 +1,56 @@
1
+ import { action, closedAction } from "../../core/action.js";
2
+ import { noOptions } from "../../core/validation.js";
3
+ import { normalizeTextMetricProfile } from "../../core/textMetricProfile.js";
4
+ import { rematerializeTypography } from "../../materialization/typography.js";
5
+
6
+ function metricsProgram(parent, child) {
7
+ return typeof child.applyTextMetrics === "function" ? child : new parent.constructor({
8
+ ...child, actionSequence: child._actionSequence
9
+ });
10
+ }
11
+
12
+ export function inheritTextMetrics(parent, child) {
13
+ const profile = parent.materializationConfigs.textMetrics;
14
+ return profile === undefined ? child : metricsProgram(parent, child).applyTextMetrics({ profile });
15
+ }
16
+
17
+ function applyProfile(program, profile) {
18
+ let next = profile === undefined
19
+ ? program._withoutMaterializationConfig(["textMetrics"])
20
+ : program._withMaterializationConfig(["textMetrics"], profile);
21
+ if (next.compositionSpec !== undefined) {
22
+ const children = Object.fromEntries(next.compositionSpec.children.map(id => {
23
+ const child = next.children[id];
24
+ return [id, profile === undefined
25
+ ? child.materializationConfigs.textMetrics === undefined ? child : metricsProgram(next, child).removeTextMetrics()
26
+ : inheritTextMetrics(next, child)];
27
+ }));
28
+ next = next._withCompositionState({ children, compositionSpec: next.compositionSpec });
29
+ for (const id of next.compositionSpec.children) next = next.useProgram({ id });
30
+ return next.materializeComposition();
31
+ }
32
+ return rematerializeTypography(next);
33
+ }
34
+
35
+ export const applyTextMetrics = /* @__PURE__ */ closedAction(
36
+ { op: "applyTextMetrics", description: "Apply measured text widths to chart layout.", scope: "any" }, ["profile"],
37
+ function (args = {}) {
38
+ const profile = normalizeTextMetricProfile(args.profile);
39
+ applyProfile(this, profile);
40
+ return applyProfile(this, profile);
41
+ }
42
+ );
43
+
44
+ export const removeTextMetrics = /* @__PURE__ */ action(
45
+ { op: "removeTextMetrics", description: "Restore estimated text layout.", scope: "any" },
46
+ function (args = {}) {
47
+ noOptions(args, "removeTextMetrics");
48
+ if (this.materializationConfigs.textMetrics === undefined) throw new Error("removeTextMetrics requires an active profile.");
49
+ applyProfile(this, undefined);
50
+ return applyProfile(this, undefined);
51
+ }
52
+ );
53
+
54
+ export function registerTextMetricActions(ProgramClass) {
55
+ Object.assign(ProgramClass.prototype, { applyTextMetrics, removeTextMetrics });
56
+ }
@@ -28,7 +28,7 @@ function requireCompositionThemeHandlers() {
28
28
  return compositionThemeHandlers;
29
29
  }
30
30
 
31
- export const applyTheme = action(
31
+ export const applyTheme = /* @__PURE__ */ action(
32
32
  {
33
33
  op: "applyTheme",
34
34
  description: "Apply persistent visual defaults to the chart program.",
@@ -70,7 +70,7 @@ export const applyTheme = action(
70
70
  }
71
71
  );
72
72
 
73
- export const removeTheme = action(
73
+ export const removeTheme = /* @__PURE__ */ action(
74
74
  {
75
75
  op: "removeTheme",
76
76
  description: "Remove program theme defaults while preserving local styles.",
@@ -1,3 +1,4 @@
1
+ import { rematerializeTypography } from "../../materialization/typography.js";
1
2
  import { themeTokens } from "../../theme/defaults.js";
2
3
  import {
3
4
  normalizeThemeState,
@@ -46,10 +47,7 @@ function tokenFor(value, sourceTokens, targetTokens) {
46
47
  if (matches.length === 0) return undefined;
47
48
  const targets = new Set(matches.map(token => targetTokens[token]));
48
49
  if (targets.size !== 1) return undefined;
49
- for (const token of matches) {
50
- if (targetTokens[token] === targets.values().next().value) return token;
51
- }
52
- return undefined;
50
+ return matches[0];
53
51
  }
54
52
 
55
53
  function roleValues(role, sourceTokens, legacy = []) {
@@ -411,6 +409,11 @@ function recolorConfig(
411
409
  return { changed, value: changed ? next : value };
412
410
  }
413
411
 
412
+ function addMarkPropertyOverrides(overrides, id, property) {
413
+ overrides.add(`c:marks.${id}.${property}`);
414
+ overrides.add(`g:${id}.${property}`);
415
+ }
416
+
414
417
  function addMarkOverrides(overrides, program, op, args) {
415
418
  const match = /^(?:create|edit)(Point|Line|Area|Arc|Bar|Rect|Rule|Tick|Text)Mark$/u
416
419
  .exec(op);
@@ -425,42 +428,43 @@ function addMarkOverrides(overrides, program, op, args) {
425
428
  if (typeof id !== "string") return;
426
429
  for (const property of ["fill", "stroke", "fontFamily"]) {
427
430
  if (!Object.hasOwn(args, property)) continue;
428
- overrides.add(`c:marks.${id}.${property}`);
429
- overrides.add(`g:${id}.${property}`);
431
+ addMarkPropertyOverrides(overrides, id, property);
430
432
  }
431
433
  }
432
434
 
433
435
  function addFacadeMarkOverrides(overrides, op, args) {
434
436
  const facades = {
435
- createScatterPlot: ["scatterPlot", "point", ["fill", "stroke"]],
436
- createLinePlot: ["linePlot", "line", ["stroke"]],
437
- createPolarScatterPlot: ["polarScatterPlot", "point", ["fill", "stroke"]],
438
- createPolarLinePlot: ["polarLinePlot", "line", ["stroke"]],
439
- createRadarPlot: ["radarPlot", "line", ["stroke"]],
440
- createRugPlot: ["rugPlot", "tick", ["stroke"]],
441
- createStripPlot: ["stripPlot", "point", ["fill", "stroke"]],
442
- createBeeswarmPlot: ["beeswarmPlot", "point", ["fill", "stroke"]],
443
- createBarPlot: ["barPlot", "bar", ["fill", "stroke"]],
444
- createAreaPlot: ["areaPlot", "area", ["fill", "stroke"]],
445
- createPiePlot: ["piePlot", "arc", ["fill", "stroke"]],
446
- createRosePlot: ["rosePlot", "arc", ["fill", "stroke"]],
447
- createRadialBarPlot: ["radialBarPlot", "arc", ["fill", "stroke"]],
448
- createDensityPlot: ["densityPlot", "area", ["fill", "stroke"]],
449
- createParallelCoordinates: ["parallelCoordinates", "line", ["stroke"]],
450
- createHistogram: ["histogram", "bar", ["fill", "stroke"]],
451
- createHeatmap: ["heatmap", "rect", ["stroke"]],
452
- createHorizonPlot: ["horizonPlot", "area", ["stroke"]],
453
- editHorizon: ["horizonPlot", "area", ["stroke"]],
454
- createViolinPlot: ["violinPlot", "area", ["fill", "stroke"]]
437
+ createScatterPlot: ["point", true],
438
+ createLinePlot: ["line", false],
439
+ createPolarScatterPlot: ["point", true],
440
+ createPolarLinePlot: ["line", false],
441
+ createRadarPlot: ["line", false],
442
+ createRugPlot: ["tick", false],
443
+ createStripPlot: ["point", true],
444
+ createBeeswarmPlot: ["point", true],
445
+ createBarPlot: ["bar", true],
446
+ createAreaPlot: ["area", true],
447
+ createPiePlot: ["arc", true],
448
+ createRosePlot: ["arc", true],
449
+ createRadialBarPlot: ["arc", true],
450
+ createDensityPlot: ["area", true],
451
+ createParallelCoordinates: ["line", false],
452
+ createHistogram: ["bar", true],
453
+ createHeatmap: ["rect", false],
454
+ createHorizonPlot: ["area", false],
455
+ editHorizon: ["area", false],
456
+ createViolinPlot: ["area", true]
455
457
  };
456
458
  const facade = facades[op];
457
459
  if (facade === undefined) return;
458
- const [defaultId, option, properties] = facade;
460
+ const [option, includesFill] = facade;
461
+ const subject = op === "editHorizon" ? "HorizonPlot" : op.slice(6);
462
+ const defaultId = subject[0].toLowerCase() + subject.slice(1);
463
+ const properties = includesFill ? ["fill", "stroke"] : ["stroke"];
459
464
  const id = args.id ?? defaultId;
460
465
  for (const property of properties) {
461
466
  if (!Object.hasOwn(args[option] ?? {}, property)) continue;
462
- overrides.add(`c:marks.${id}.${property}`);
463
- overrides.add(`g:${id}.${property}`);
467
+ addMarkPropertyOverrides(overrides, id, property);
464
468
  }
465
469
  }
466
470
 
@@ -492,8 +496,7 @@ function addTextFacadeOverrides(overrides, node) {
492
496
  if (typeof id !== "string") return;
493
497
  for (const property of ["fill", "fontFamily"]) {
494
498
  if (!Object.hasOwn(node.args, property)) continue;
495
- overrides.add(`c:marks.${id}.${property}`);
496
- overrides.add(`g:${id}.${property}`);
499
+ addMarkPropertyOverrides(overrides, id, property);
497
500
  }
498
501
  }
499
502
 
@@ -509,8 +512,7 @@ function addErrorBarOverrides(overrides, program, node) {
509
512
  if (id === undefined) return;
510
513
  const config = program.markConfigs[id].errorBar;
511
514
  for (const target of [id, config.lowerCapId, config.upperCapId]) {
512
- overrides.add(`c:marks.${target}.stroke`);
513
- overrides.add(`g:${target}.stroke`);
515
+ addMarkPropertyOverrides(overrides, target, "stroke");
514
516
  }
515
517
  }
516
518
 
@@ -536,8 +538,7 @@ function addErrorBandOverrides(overrides, program, node) {
536
538
  const targets = errorBandTargets(program, node);
537
539
  if (targets === undefined) return;
538
540
  if (Object.hasOwn(node.args, "fill") && node.args.fill !== false) {
539
- overrides.add(`c:marks.${targets.id}.fill`);
540
- overrides.add(`g:${targets.id}.fill`);
541
+ addMarkPropertyOverrides(overrides, targets.id, "fill");
541
542
  }
542
543
  const boundary = node.op === "editErrorBandBoundary"
543
544
  ? node.args
@@ -548,8 +549,7 @@ function addErrorBandOverrides(overrides, program, node) {
548
549
  ? [targets.lower, targets.upper]
549
550
  : [(node.args.boundary === "lower" ? targets.lower : targets.upper)];
550
551
  for (const id of ids) {
551
- overrides.add(`c:marks.${id}.stroke`);
552
- overrides.add(`g:${id}.stroke`);
552
+ addMarkPropertyOverrides(overrides, id, "stroke");
553
553
  }
554
554
  }
555
555
 
@@ -589,8 +589,7 @@ function addReferenceOverrides(overrides, op, args) {
589
589
  const id = args.id ?? defaultId;
590
590
  for (const property of properties) {
591
591
  if (!Object.hasOwn(args, property)) continue;
592
- overrides.add(`c:marks.${id}.${property}`);
593
- overrides.add(`g:${id}.${property}`);
592
+ addMarkPropertyOverrides(overrides, id, property);
594
593
  }
595
594
  }
596
595
 
@@ -611,12 +610,10 @@ function addRegressionOverrides(overrides, node) {
611
610
  : child?.args.id ?? child?.args.target;
612
611
  if (typeof id !== "string") return;
613
612
  if (Object.hasOwn(style ?? {}, "color")) {
614
- overrides.add(`c:marks.${id}.fill`);
615
- overrides.add(`g:${id}.fill`);
613
+ addMarkPropertyOverrides(overrides, id, "fill");
616
614
  }
617
615
  if (Object.hasOwn(style ?? {}, "stroke")) {
618
- overrides.add(`c:marks.${id}.stroke`);
619
- overrides.add(`g:${id}.stroke`);
616
+ addMarkPropertyOverrides(overrides, id, "stroke");
620
617
  }
621
618
  }
622
619
 
@@ -835,39 +832,18 @@ function addParallelOverrides(overrides, program, op, args) {
835
832
  }
836
833
 
837
834
  function addLegendOverrides(overrides, program, args) {
838
- const legend = program.guideConfigs.legend ?? {};
839
- const prefixes = {
840
- series: "seriesLegend",
841
- color: "colorLegend",
842
- stroke: "strokeLegend",
843
- interval: "colorLegend",
844
- gradient: "colorGradient",
845
- strokeInterval: "strokeInterval",
846
- strokeGradient: "strokeGradient",
847
- size: "sizeLegend",
848
- strokeWidth: "strokeWidthLegend",
849
- opacity: "opacityLegend"
850
- };
851
- for (const kind of Object.keys(legend)) {
852
- if (Object.hasOwn(args.labels ?? {}, "color")) {
853
- overrides.add(`c:guides.legend.${kind}.labels.color`);
854
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Labels.fill`);
855
- }
856
- if (Object.hasOwn(args.labels ?? {}, "fontFamily")) {
857
- overrides.add(`c:guides.legend.${kind}.labels.fontFamily`);
858
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Labels.fontFamily`);
859
- }
860
- if (Object.hasOwn(args.titleStyle ?? {}, "color")) {
861
- overrides.add(`c:guides.legend.${kind}.titleStyle.color`);
862
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Title.fill`);
863
- }
864
- if (Object.hasOwn(args.titleStyle ?? {}, "fontFamily")) {
865
- overrides.add(`c:guides.legend.${kind}.titleStyle.fontFamily`);
866
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Title.fontFamily`);
867
- }
868
- if (Object.hasOwn(args.border ?? {}, "color")) {
869
- overrides.add(`c:guides.legend.${kind}.border.color`);
870
- if (prefixes[kind]) overrides.add(`g:${prefixes[kind]}Background.stroke`);
835
+ for (const kind of Object.keys(program.guideConfigs.legend ?? {})) {
836
+ const prefix = legendPrefix(kind);
837
+ for (const [option, suffix, property, graphicProperty] of [
838
+ ["labels", "Labels", "color", "fill"],
839
+ ["labels", "Labels", "fontFamily", "fontFamily"],
840
+ ["titleStyle", "Title", "color", "fill"],
841
+ ["titleStyle", "Title", "fontFamily", "fontFamily"],
842
+ ["border", "Background", "color", "stroke"]
843
+ ]) {
844
+ if (!Object.hasOwn(args[option] ?? {}, property)) continue;
845
+ overrides.add(`c:guides.legend.${kind}.${option}.${property}`);
846
+ if (prefix) overrides.add(`g:${prefix}${suffix}.${graphicProperty}`);
871
847
  }
872
848
  }
873
849
  }
@@ -1002,51 +978,18 @@ function collectOverrides(program) {
1002
978
  return overrides;
1003
979
  }
1004
980
 
1005
- function callExisting(program, graphicId, operation) {
1006
- return program.graphicSpec.objects[graphicId] !== undefined &&
1007
- typeof program[operation] === "function"
1008
- ? program[operation]()
1009
- : program;
1010
- }
1011
-
1012
- function rematerializeThemeTypography(program) {
1013
- if (program.compositionSpec !== undefined) {
1014
- return program.compositionSpec.type === "facet"
1015
- ? program.materializeComposition()
1016
- : program;
1017
- }
1018
-
1019
- let next = program;
1020
- for (const layer of next.semanticSpec.layers) {
1021
- if (layer.mark?.type === "text" &&
1022
- next.graphicSpec.objects[layer.id] !== undefined &&
1023
- typeof next.rematerializeTextMark === "function") {
1024
- next = next.rematerializeTextMark({ id: layer.id });
1025
- }
1026
- }
1027
- for (const [id, operation] of [
1028
- ["xAxisLabels", "editXAxisLabels"],
1029
- ["yAxisLabels", "editYAxisLabels"],
1030
- ["xAxisTitle", "editXAxisTitle"],
1031
- ["yAxisTitle", "editYAxisTitle"],
1032
- ["thetaAxisLabels", "editThetaAxisLabels"],
1033
- ["radialAxisLabels", "editRadialAxisLabels"],
1034
- ["thetaAxisTitle", "editThetaAxisTitle"],
1035
- ["radialAxisTitle", "editRadialAxisTitle"]
1036
- ]) {
1037
- next = callExisting(next, id, operation);
1038
- }
1039
- if (Object.keys(next.guideConfigs.legend ?? {}).length > 0 &&
1040
- typeof next.rematerializeLegend === "function") {
1041
- next = next.rematerializeLegend();
1042
- }
1043
- next = callExisting(next, "chartTitle", "rematerializeTitle");
1044
- return next;
1045
- }
1046
-
1047
981
  export function reconcileProgramTheme(program, { source, metadata }) {
1048
982
  const storedState = program.materializationConfigs.theme;
1049
983
  if (storedState === undefined) return program;
984
+ // Data/context/trace-only transitions cannot change authored appearance.
985
+ // Preserve all visual identities instead of rescanning every existing mark.
986
+ if (program.graphicSpec === source.graphicSpec &&
987
+ program.materializationConfigs === source.materializationConfigs &&
988
+ program.children === source.children &&
989
+ program.compositionSpec === source.compositionSpec &&
990
+ ["layers", "scales", "coordinates", "guides", "title"].every(
991
+ key => program.semanticSpec[key] === source.semanticSpec[key]
992
+ )) return program;
1050
993
  const composition = program.compositionSpec !== undefined;
1051
994
  const state = normalizeThemeState(storedState, { composition });
1052
995
  const sourceState = normalizeThemeState(
@@ -1088,15 +1031,16 @@ export function reconcileProgramTheme(program, { source, metadata }) {
1088
1031
 
1089
1032
  const facetsChanged = configs.value.facets !==
1090
1033
  themed.materializationConfigs.facets;
1034
+ const typographyChanged = sourceTokens.fontFamily !== targetTokens.fontFamily;
1091
1035
  if (facetsChanged && next.compositionSpec?.type === "facet") {
1092
1036
  next = next.materializeComposition();
1093
- } else if (sourceTokens.fontFamily !== targetTokens.fontFamily) {
1094
- next = rematerializeThemeTypography(next);
1037
+ } else if (typographyChanged) {
1038
+ next = rematerializeTypography(next);
1095
1039
  }
1096
1040
 
1097
1041
  const parallelChanged = configs.value.guides.axis?.parallel !==
1098
1042
  themed.guideConfigs.axis?.parallel;
1099
- if (parallelChanged && typeof next.rematerializeParallelAxes === "function") {
1043
+ if (parallelChanged && !typographyChanged && typeof next.rematerializeParallelAxes === "function") {
1100
1044
  next = next.rematerializeParallelAxes();
1101
1045
  }
1102
1046
 
@@ -115,12 +115,12 @@ function titleTextAction(kind, create) {
115
115
  }));
116
116
  }
117
117
 
118
- export const editTitleText = titleTextAction("title", false);
119
- export const createTitleText = titleTextAction("title", true);
120
- export const editSubtitleText = titleTextAction("subtitle", false);
121
- export const createSubtitleText = titleTextAction("subtitle", true);
118
+ export const editTitleText = /* @__PURE__ */ titleTextAction("title", false);
119
+ export const createTitleText = /* @__PURE__ */ titleTextAction("title", true);
120
+ export const editSubtitleText = /* @__PURE__ */ titleTextAction("subtitle", false);
121
+ export const createSubtitleText = /* @__PURE__ */ titleTextAction("subtitle", true);
122
122
 
123
- export const rematerializeTitle = action(
123
+ export const rematerializeTitle = /* @__PURE__ */ action(
124
124
  { op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
125
125
  withGuideLayoutValidation(function (args = {}) {
126
126
  noOptions(args, "rematerializeTitle");
@@ -142,7 +142,7 @@ export const rematerializeTitle = action(
142
142
  })
143
143
  );
144
144
 
145
- export const createTitle = action(
145
+ export const createTitle = /* @__PURE__ */ action(
146
146
  {
147
147
  op: "createTitle",
148
148
  description: "Create a chart title and optional subtitle.",
@@ -176,7 +176,7 @@ export const createTitle = action(
176
176
  })
177
177
  );
178
178
 
179
- export const editTitle = action(
179
+ export const editTitle = /* @__PURE__ */ action(
180
180
  {
181
181
  op: "editTitle",
182
182
  description: "Edit one stable chart title resource.",
@@ -214,7 +214,7 @@ export const editTitle = action(
214
214
  })
215
215
  );
216
216
 
217
- export const removeTitle = action(
217
+ export const removeTitle = /* @__PURE__ */ action(
218
218
  {
219
219
  op: "removeTitle",
220
220
  description: "Remove the complete chart title resource.",
@@ -212,7 +212,7 @@ export function resolveTitleLayout(program, config) {
212
212
  program.semanticSpec.title.subtitle,
213
213
  "Chart subtitle"
214
214
  )
215
- }, config);
215
+ }, config, program.materializationConfigs.textMetrics);
216
216
  const horizontal = ["top", "bottom"].includes(config.position);
217
217
  let component;
218
218
  if (horizontal) {
@@ -249,10 +249,10 @@ export function resolveTitleLayout(program, config) {
249
249
  ? undefined
250
250
  : component(block.subtitleLines, block.subtitleCenters);
251
251
  const titleBounds = unionTitleBounds([
252
- resolveTitleComponentBounds(title, config.titleStyle),
252
+ resolveTitleComponentBounds(title, config.titleStyle, program.materializationConfigs.textMetrics),
253
253
  ...(subtitle === undefined
254
254
  ? []
255
- : [resolveTitleComponentBounds(subtitle, config.subtitleStyle)])
255
+ : [resolveTitleComponentBounds(subtitle, config.subtitleStyle, program.materializationConfigs.textMetrics)])
256
256
  ]);
257
257
  validateLayout(
258
258
  config.position,
@@ -132,7 +132,7 @@ function guideOptions(guides, x, y, category, color) {
132
132
  };
133
133
  }
134
134
 
135
- export const createViolinPlot = action(
135
+ export const createViolinPlot = /* @__PURE__ */ action(
136
136
  {
137
137
  op: "createViolinPlot",
138
138
  description: "Create a categorical kernel-density violin plot."
@@ -74,7 +74,7 @@ function assertSplitSelectionCompatibility(program, owner, changesSplit) {
74
74
  }
75
75
  }
76
76
 
77
- export const editViolinPlot = action(
77
+ export const editViolinPlot = /* @__PURE__ */ action(
78
78
  {
79
79
  op: OPERATION,
80
80
  description: "Revise one violin plot's data and statistical roles."
@@ -1,4 +1,6 @@
1
- import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
1
+ import { validateKeys } from "./validation.js";
2
+ import { annotateError } from "./diagnostics.js";
3
+ import { cloneAndFreeze, freezeOwned, isOwned, isPlainObject } from "./immutable.js";
2
4
 
3
5
  const metadataByWrappedAction = new WeakMap();
4
6
  const implementationByWrappedAction = new WeakMap();
@@ -35,6 +37,8 @@ function summarizeObject(value, ancestors = new WeakSet()) {
35
37
  summary[`${key}Count`] = item.length;
36
38
  } else if (isPlainObject(item)) {
37
39
  summary[key] = summarizeObject(item, ancestors);
40
+ } else if (typeof item === "function") {
41
+ summary[`${key}Type`] = "function";
38
42
  } else if (item !== null && typeof item === "object") {
39
43
  summary[`${key}Type`] = item.constructor?.name ?? "object";
40
44
  } else {
@@ -71,13 +75,59 @@ export function createActionNode({ id, op, description, args }) {
71
75
  });
72
76
  }
73
77
 
78
+ // Persistent child tails make appending the active (last) branch independent
79
+ // of sibling count. The public children property remains a stable frozen Array.
80
+ const childLists = new WeakMap();
81
+
82
+ function childList(node) {
83
+ let list = childLists.get(node);
84
+ if (list !== undefined) return list;
85
+ let tail;
86
+ for (const child of node.children) tail = { value: child, previous: tail };
87
+ list = { tail, length: node.children.length };
88
+ if (isOwned(node)) childLists.set(node, list);
89
+ return list;
90
+ }
91
+
92
+ function withChildList(node, list) {
93
+ let children;
94
+ const next = {};
95
+ for (const key of Object.keys(node)) {
96
+ if (key !== "children") Object.defineProperty(next, key, {
97
+ value: node[key], enumerable: true
98
+ });
99
+ }
100
+ Object.defineProperty(next, "children", {
101
+ enumerable: true,
102
+ get() {
103
+ if (children === undefined) {
104
+ const values = new Array(list.length);
105
+ let item = list.tail;
106
+ for (let index = list.length - 1; index >= 0; index -= 1) {
107
+ values[index] = item.value;
108
+ item = item.previous;
109
+ }
110
+ children = freezeOwned(values);
111
+ }
112
+ return children;
113
+ }
114
+ });
115
+ childLists.set(next, list);
116
+ return freezeOwned(next);
117
+ }
118
+
119
+ function childAt(node, index) {
120
+ const list = childList(node);
121
+ return index === list.length - 1 ? list.tail.value : node.children[index];
122
+ }
123
+
74
124
  function nodeAtPath(root, path) {
75
125
  let node = root;
76
126
  for (const index of path) {
77
- if (!Number.isInteger(index) || index < 0 || index >= node.children.length) {
127
+ if (!Number.isInteger(index) || index < 0 || index >= childList(node).length) {
78
128
  throw new Error(`Unknown parent action path "${path.join(".")}".`);
79
129
  }
80
- node = node.children[index];
130
+ node = childAt(node, index);
81
131
  }
82
132
  return node;
83
133
  }
@@ -87,19 +137,28 @@ export function appendActionNodeAtPath(root, parentPath, actionNode) {
87
137
  throw new TypeError("Parent action path must be an array.");
88
138
  }
89
139
  const parent = nodeAtPath(root, parentPath);
90
- const path = [...parentPath, parent.children.length];
140
+ const path = [...parentPath, childList(parent).length];
91
141
 
92
142
  function append(node, depth) {
143
+ const list = childList(node);
93
144
  if (depth === parentPath.length) {
94
- return freezeOwned({
95
- ...node,
96
- children: freezeOwned([...node.children, actionNode])
145
+ return withChildList(node, {
146
+ tail: { value: actionNode, previous: list.tail }, length: list.length + 1
97
147
  });
98
148
  }
99
149
  const index = parentPath[depth];
100
- const children = [...node.children];
101
- children[index] = append(children[index], depth + 1);
102
- return freezeOwned({ ...node, children: freezeOwned(children) });
150
+ const child = append(childAt(node, index), depth + 1);
151
+ if (index === list.length - 1) {
152
+ return withChildList(node, {
153
+ tail: { value: child, previous: list.tail.previous }, length: list.length
154
+ });
155
+ }
156
+ // Restored/extension traces may address an older sibling explicitly.
157
+ let tail;
158
+ for (let position = 0; position < list.length; position += 1) {
159
+ tail = { value: position === index ? child : node.children[position], previous: tail };
160
+ }
161
+ return withChildList(node, { tail, length: list.length });
103
162
  }
104
163
 
105
164
  return { root: append(root, 0), path };
@@ -137,39 +196,52 @@ export function action(metadata, implementation) {
137
196
  });
138
197
 
139
198
  const wrappedAction = function wrappedAction(args = {}) {
140
- if (!isPlainObject(args)) {
141
- throw new TypeError("Action arguments must be a plain object.");
142
- }
143
-
144
- if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
145
- if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
146
-
147
- const summarizedArgs = summarizeArgs(args);
148
- const entered = this._enterAction({
149
- ...ownedMetadata,
150
- args: summarizedArgs
151
- });
152
- let result = implementation.call(entered, args);
153
-
154
- if (!(result instanceof this.constructor)) {
155
- throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
156
- }
157
-
158
- if (this.actionStack.length === 0 && actionCompletionHook !== undefined) {
159
- result = actionCompletionHook(result, {
160
- source: this,
161
- metadata: ownedMetadata,
199
+ try {
200
+ if (!isPlainObject(args)) {
201
+ throw new TypeError("Action arguments must be a plain object.");
202
+ }
203
+
204
+ if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
205
+ if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
206
+
207
+ const summarizedArgs = summarizeArgs(args);
208
+ const entered = this._enterAction({
209
+ ...ownedMetadata,
162
210
  args: summarizedArgs
163
211
  });
212
+ let result = implementation.call(entered, args);
213
+
164
214
  if (!(result instanceof this.constructor)) {
165
- throw new TypeError("Action completion hook must return a ChartProgram.");
215
+ throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
216
+ }
217
+
218
+ if (this.actionStack.length === 0 && actionCompletionHook !== undefined) {
219
+ result = actionCompletionHook(result, {
220
+ source: this,
221
+ metadata: ownedMetadata,
222
+ args: summarizedArgs
223
+ });
224
+ if (!(result instanceof this.constructor)) {
225
+ throw new TypeError("Action completion hook must return a ChartProgram.");
226
+ }
166
227
  }
228
+
229
+ return result._exitAction();
230
+ } catch (error) {
231
+ throw annotateError(error, { code: "action-failed", operation: ownedMetadata.op });
167
232
  }
168
-
169
- return result._exitAction();
170
233
  };
171
234
 
172
235
  metadataByWrappedAction.set(wrappedAction, ownedMetadata);
173
236
  implementationByWrappedAction.set(wrappedAction, implementation);
174
237
  return wrappedAction;
175
238
  }
239
+
240
+ // Private built-in convenience: action() owns the object boundary and trace,
241
+ // while each definition supplies its closed option vocabulary exactly once.
242
+ export function closedAction(metadata, options, implementation) {
243
+ return action(metadata, function (args = {}) {
244
+ if (options !== undefined) validateKeys(args, options, metadata.op);
245
+ return implementation.call(this, args);
246
+ });
247
+ }
@@ -0,0 +1,16 @@
1
+ const detailsByError = new WeakMap();
2
+
3
+ export function getErrorDetails(error) {
4
+ return error !== null && (typeof error === "object" || typeof error === "function")
5
+ ? detailsByError.get(error) : undefined;
6
+ }
7
+
8
+ // Attach metadata without modifying the thrown value or its existing class.
9
+ export function annotateError(error, details) {
10
+ if (!(error instanceof Error)) return error;
11
+ const previous = getErrorDetails(error);
12
+ const merged = { ...details, ...previous };
13
+ if (merged.candidates !== undefined) merged.candidates = Object.freeze([...merged.candidates]);
14
+ detailsByError.set(error, Object.freeze(merged));
15
+ return error;
16
+ }