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
@@ -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
  }
@@ -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 instanceof Error ? error.message : String(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
+ }