ggaction 0.0.13 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  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 +39 -7
  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/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -1,11 +1,21 @@
1
1
  import { resolveBarChannels } from "../grammar/bars/policy.js";
2
+ import {
3
+ assertMarkLabelPlacementSupport,
4
+ normalizeMarkLabelPlacement,
5
+ resolveMarkLabelPlacement
6
+ } from "../layout/labels.js";
2
7
  import { resolveMarkLabelValues } from "../grammar/markLabels.js";
3
8
  import { formatTextValue } from "../grammar/text.js";
4
9
  import { normalizePositionDatum } from "../grammar/positionDatum.js";
5
10
  import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
6
11
  import { findDataset } from "../selectors/datasets.js";
12
+ import { findCoordinate } from "../selectors/coordinates.js";
7
13
  import { findLayer } from "../selectors/layers.js";
14
+ import { unionConcreteGraphicBounds } from
15
+ "../grammar/schemas/graphicBounds.js";
16
+ import { polarDirection } from "../grammar/polar.js";
8
17
  import { resolveMarkItems } from "./selection/policies/index.js";
18
+ import { resolveMarkSelection } from "./selection/state.js";
9
19
  import { mapScaleConsumerValues } from "./scales/map.js";
10
20
  import { resolveRowEncodingValues } from "./rowEncoding.js";
11
21
 
@@ -83,6 +93,172 @@ function sourceAnchor(program, source, item) {
83
93
  return { x: item.properties.x, y: item.properties.y };
84
94
  }
85
95
 
96
+ function explicitIntervalAxis(source) {
97
+ const axes = ["x", "y"].filter(axis =>
98
+ source.encoding?.[axis] !== undefined &&
99
+ source.encoding?.[`${axis}2`] !== undefined
100
+ );
101
+ return axes.length === 1 ? axes[0] : undefined;
102
+ }
103
+
104
+ function sourcePlacementContext(program, source) {
105
+ const coordinateType = findCoordinate(program, source.coordinate)?.type;
106
+ return {
107
+ markType: source.mark.type,
108
+ coordinateType,
109
+ intervalAxis: source.mark.type === "bar"
110
+ ? resolveBarChannels(source).measure
111
+ : source.mark.type === "rect"
112
+ ? explicitIntervalAxis(source)
113
+ : undefined
114
+ };
115
+ }
116
+
117
+ export function validateSourceMarkLabelPlacement(program, sourceId, value) {
118
+ const source = findLayer(program, sourceId);
119
+ if (source === undefined) {
120
+ throw new Error(`Unknown mark label placement source "${sourceId}".`);
121
+ }
122
+ const placement = normalizeMarkLabelPlacement(value);
123
+ assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
124
+ return placement;
125
+ }
126
+
127
+ function rectangleBounds(item) {
128
+ const { x, y, width, height } = item.properties;
129
+ if (![x, y, width, height].every(Number.isFinite)) {
130
+ throw new TypeError("Interval mark label placement requires finite rectangle geometry.");
131
+ }
132
+ return {
133
+ left: Math.min(x, x + width),
134
+ right: Math.max(x, x + width),
135
+ top: Math.min(y, y + height),
136
+ bottom: Math.max(y, y + height)
137
+ };
138
+ }
139
+
140
+ function mappedChannel(program, source, item, channel) {
141
+ const value = item.channels[channel];
142
+ const primary = channel === "x2" ? "x" : channel === "y2" ? "y" : channel;
143
+ const scale = program.resolvedScales[
144
+ source.encoding?.[channel]?.scale ?? source.encoding?.[primary]?.scale
145
+ ];
146
+ return value === undefined || scale === undefined
147
+ ? undefined
148
+ : mapScaleConsumerValues([value], scale, channel)[0];
149
+ }
150
+
151
+ function scaleIncreaseDirection(program, source, axis) {
152
+ const scale = program.resolvedScales[source.encoding?.[axis]?.scale];
153
+ const domain = scale?.domain;
154
+ if (!Array.isArray(domain) || domain.length < 2) {
155
+ return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
156
+ }
157
+ const mapped = mapScaleConsumerValues(
158
+ [domain[0], domain.at(-1)],
159
+ scale,
160
+ axis
161
+ );
162
+ const delta = mapped[1] - mapped[0];
163
+ if (!Number.isFinite(delta) || delta === 0) {
164
+ return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
165
+ }
166
+ return axis === "x"
167
+ ? { x: Math.sign(delta), y: 0 }
168
+ : { x: 0, y: Math.sign(delta) };
169
+ }
170
+
171
+ function intervalPlacementGeometry(program, source, item, axis) {
172
+ const bounds = rectangleBounds(item);
173
+ const primary = mappedChannel(program, source, item, axis);
174
+ const secondary = mappedChannel(program, source, item, `${axis}2`);
175
+ if (![primary, secondary].every(Number.isFinite)) {
176
+ throw new TypeError(
177
+ `${source.mark.type === "bar" ? "Bar" : "Rect"} semantic label placement requires resolved ${axis}/${axis}2 endpoints.`
178
+ );
179
+ }
180
+ const cross = axis === "x"
181
+ ? (bounds.top + bounds.bottom) / 2
182
+ : (bounds.left + bounds.right) / 2;
183
+ const start = axis === "x" ? { x: primary, y: cross } : { x: cross, y: primary };
184
+ const end = axis === "x" ? { x: secondary, y: cross } : { x: cross, y: secondary };
185
+ const delta = { x: end.x - start.x, y: end.y - start.y };
186
+ const length = Math.hypot(delta.x, delta.y);
187
+ const endOutward = length > 0
188
+ ? { x: delta.x / length, y: delta.y / length }
189
+ : scaleIncreaseDirection(program, source, axis);
190
+ return {
191
+ kind: "interval",
192
+ center: {
193
+ x: (bounds.left + bounds.right) / 2,
194
+ y: (bounds.top + bounds.bottom) / 2
195
+ },
196
+ start: {
197
+ point: start,
198
+ outward: { x: -endOutward.x, y: -endOutward.y }
199
+ },
200
+ end: { point: end, outward: endOutward },
201
+ bounds
202
+ };
203
+ }
204
+
205
+ function pointPlacementGeometry(program, source, item) {
206
+ const center = { x: item.properties.x, y: item.properties.y };
207
+ const coordinateType = findCoordinate(program, source.coordinate)?.type;
208
+ if (coordinateType !== "polar") return { kind: "point", center };
209
+ const thetaScale = program.resolvedScales[source.encoding?.theta?.scale];
210
+ const theta = item.channels.theta === undefined || thetaScale === undefined
211
+ ? undefined
212
+ : mapScaleConsumerValues([item.channels.theta], thetaScale, "theta")[0];
213
+ const outward = Number.isFinite(theta) ? polarDirection(theta) : { x: 0, y: -1 };
214
+ const objectBounds = unionConcreteGraphicBounds(
215
+ program.graphicSpec,
216
+ item.graphicIds
217
+ , program.materializationConfigs.textMetrics);
218
+ const corners = objectBounds === undefined ? [] : [
219
+ { x: objectBounds.left, y: objectBounds.top },
220
+ { x: objectBounds.right, y: objectBounds.top },
221
+ { x: objectBounds.right, y: objectBounds.bottom },
222
+ { x: objectBounds.left, y: objectBounds.bottom }
223
+ ];
224
+ const support = Math.max(0, ...corners.map(point =>
225
+ (point.x - center.x) * outward.x + (point.y - center.y) * outward.y
226
+ ));
227
+ return {
228
+ kind: "point",
229
+ center,
230
+ end: {
231
+ point: {
232
+ x: center.x + outward.x * support,
233
+ y: center.y + outward.y * support
234
+ },
235
+ outward
236
+ }
237
+ };
238
+ }
239
+
240
+ function sourcePlacementGeometry(program, source, item) {
241
+ if (source.mark.type === "bar") {
242
+ return intervalPlacementGeometry(
243
+ program,
244
+ source,
245
+ item,
246
+ resolveBarChannels(source).measure
247
+ );
248
+ }
249
+ if (source.mark.type === "rect") {
250
+ const axis = explicitIntervalAxis(source);
251
+ return axis === undefined
252
+ ? { kind: "point", center: sourceAnchor(program, source, item) }
253
+ : intervalPlacementGeometry(program, source, item, axis);
254
+ }
255
+ if (source.mark.type === "arc") return item.geometry;
256
+ if (source.mark.type === "point") {
257
+ return pointPlacementGeometry(program, source, item);
258
+ }
259
+ return { kind: "point", center: sourceAnchor(program, source, item) };
260
+ }
261
+
86
262
  function contentValue(encoding, rowOrItem, source) {
87
263
  return Object.hasOwn(encoding, "field")
88
264
  ? source === undefined
@@ -91,8 +267,7 @@ function contentValue(encoding, rowOrItem, source) {
91
267
  : encoding.datum;
92
268
  }
93
269
 
94
- function concreteItem(config, position, value, format) {
95
- const text = formatTextValue(value, format);
270
+ function concreteTextItem(config, position, text, { offsets = true } = {}) {
96
271
  const x = position.x;
97
272
  const y = position.y;
98
273
  if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
@@ -101,8 +276,8 @@ function concreteItem(config, position, value, format) {
101
276
  return {
102
277
  type: "text",
103
278
  properties: {
104
- x: x + config.dx,
105
- y: y + config.dy,
279
+ x: x + (offsets ? config.dx : 0),
280
+ y: y + (offsets ? config.dy : 0),
106
281
  text,
107
282
  fill: config.fill,
108
283
  opacity: config.opacity,
@@ -116,6 +291,10 @@ function concreteItem(config, position, value, format) {
116
291
  };
117
292
  }
118
293
 
294
+ function concreteItem(config, position, value, format) {
295
+ return concreteTextItem(config, position, formatTextValue(value, format));
296
+ }
297
+
119
298
  function relativeLuminance(color) {
120
299
  if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) {
121
300
  return undefined;
@@ -147,18 +326,90 @@ function resolveSourceTextItems(program, layer, config) {
147
326
  if (source === undefined) {
148
327
  throw new Error(`Text mark "${layer.id}" requires source layer "${layer.source}".`);
149
328
  }
150
- const items = resolveMarkItems(program, source.id);
151
- const values = layer.encoding.text.content === undefined ? undefined
152
- : resolveMarkLabelValues(source, items, layer.encoding.text);
153
- return items.flatMap((item, index) => {
154
- const anchor = sourceAnchor(program, source, item);
155
- const concrete = concreteItem(
156
- sourceTextConfig(config, source, item),
157
- anchor,
158
- values === undefined ? contentValue(layer.encoding.text, item, source) : values[index],
329
+ const requested = program.markConfigs[layer.id]?.labelAuthoring?.selection ?? {
330
+ kind: "all"
331
+ };
332
+ let allItems;
333
+ let items;
334
+ if (requested.kind === "all") {
335
+ allItems = resolveMarkItems(program, source.id);
336
+ items = allItems;
337
+ } else if (requested.kind === "inline") {
338
+ const resolved = resolveMarkSelection(program, source.id, requested.selector);
339
+ allItems = resolved.items;
340
+ const selected = new Set(resolved.keys);
341
+ items = allItems.filter(item => selected.has(item.key));
342
+ } else if (requested.kind === "named") {
343
+ const definition = program.materializationConfigs.selections?.[requested.id];
344
+ if (definition === undefined) {
345
+ throw new Error(`Unknown label selection "${requested.id}".`);
346
+ }
347
+ if (definition.target !== source.id) {
348
+ throw new Error(
349
+ `Label selection "${requested.id}" must target source mark "${source.id}".`
350
+ );
351
+ }
352
+ const resolved = resolveMarkSelection(program, source.id, definition.selector);
353
+ allItems = resolved.items;
354
+ const selected = new Set(resolved.keys);
355
+ items = allItems.filter(item => selected.has(item.key));
356
+ } else {
357
+ throw new Error(`Unknown label selection kind "${requested.kind}".`);
358
+ }
359
+ const resolvedValues = layer.encoding.text.content === undefined ? undefined
360
+ : resolveMarkLabelValues(source, allItems, layer.encoding.text);
361
+ const values = resolvedValues === undefined ? undefined : new Map(
362
+ allItems.map((item, index) => [item.key, resolvedValues[index]])
363
+ );
364
+ return items.flatMap(item => {
365
+ let resolvedConfig = sourceTextConfig(config, source, item);
366
+ const text = formatTextValue(
367
+ values === undefined
368
+ ? contentValue(layer.encoding.text, item, source)
369
+ : values.get(item.key),
159
370
  layer.encoding.text.format
160
371
  );
161
- return concrete === undefined ? [] : [{ graphic: concrete, anchor }];
372
+ if (text === undefined) return [];
373
+ const placement = program.markConfigs[layer.id]?.labelAuthoring?.placement;
374
+ if (placement === undefined) {
375
+ const anchor = sourceAnchor(program, source, item);
376
+ const concrete = concreteTextItem(resolvedConfig, anchor, text);
377
+ return concrete === undefined ? [] : [{ graphic: concrete, anchor }];
378
+ }
379
+ assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
380
+ const resolved = resolveMarkLabelPlacement({
381
+ placement,
382
+ geometry: sourcePlacementGeometry(program, source, item),
383
+ text: {
384
+ text,
385
+ fontSize: resolvedConfig.fontSize,
386
+ fontFamily: resolvedConfig.fontFamily,
387
+ fontWeight: resolvedConfig.fontWeight,
388
+ textAlign: resolvedConfig.align,
389
+ textBaseline: resolvedConfig.baseline,
390
+ rotation: resolvedConfig.rotation,
391
+ dx: resolvedConfig.dx,
392
+ dy: resolvedConfig.dy
393
+ }
394
+ }, program.materializationConfigs.textMetrics);
395
+ if (!resolved.visible) return [];
396
+ if (
397
+ resolved.anchor.startsWith("outside") &&
398
+ config.fillExplicit !== true
399
+ ) {
400
+ resolvedConfig = config;
401
+ }
402
+ const anchor = { x: resolved.sourceX, y: resolved.sourceY };
403
+ return [{
404
+ graphic: concreteTextItem(
405
+ resolvedConfig,
406
+ { x: resolved.x, y: resolved.y },
407
+ text,
408
+ { offsets: false }
409
+ ),
410
+ anchor,
411
+ placement: resolved
412
+ }];
162
413
  });
163
414
  }
164
415
 
@@ -0,0 +1,31 @@
1
+ import { applyMaterializationPlan, buildMaterializationPlan } from "./planner.js";
2
+ import { withGuideLayoutTransaction } from "./guides/layout.js";
3
+
4
+ // Font tokens and measured-width profiles invalidate the same existing owners.
5
+ // Incomplete text remains incomplete until its own prerequisites are authored.
6
+ export function rematerializeTypography(program) {
7
+ if (program.compositionSpec !== undefined) {
8
+ return program.compositionSpec.type === "facet" ? program.materializeComposition() : program;
9
+ }
10
+ const exists = (id, op) => program.graphicSpec.objects[id] !== undefined && typeof program[op] === "function";
11
+ const marks = program.semanticSpec.layers
12
+ .filter(layer => layer.mark?.type === "text" && exists(layer.id, "rematerializeTextMark"))
13
+ .map(layer => ({ op: "rematerializeTextMark", args: { id: layer.id } }));
14
+ const guides = [];
15
+ for (const [id, op] of [
16
+ ["xAxisLabels", "editXAxisLabels"], ["yAxisLabels", "editYAxisLabels"],
17
+ ["xAxisTitle", "editXAxisTitle"], ["yAxisTitle", "editYAxisTitle"],
18
+ ["thetaAxisLabels", "editThetaAxisLabels"], ["radialAxisLabels", "editRadialAxisLabels"],
19
+ ["thetaAxisTitle", "editThetaAxisTitle"], ["radialAxisTitle", "editRadialAxisTitle"]
20
+ ]) if (exists(id, op)) guides.push({ op });
21
+ const parallel = program.guideConfigs.axis?.parallel?.axes;
22
+ if (parallel !== undefined && typeof program.rematerializeParallelAxes === "function") {
23
+ guides.push({ op: "rematerializeParallelAxes", args: { target: parallel.target } });
24
+ }
25
+ if (Object.keys(program.guideConfigs.legend ?? {}).length > 0 && typeof program.rematerializeLegend === "function") {
26
+ guides.push({ op: "rematerializeLegend" });
27
+ }
28
+ const layout = exists("chartTitle", "rematerializeTitle") ? [{ op: "rematerializeTitle" }] : [];
29
+ return withGuideLayoutTransaction(program, next =>
30
+ applyMaterializationPlan(next, buildMaterializationPlan({ marks, guides, layout })));
31
+ }
package/src/mcp/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runGgactionMcpServer } from "./server.js";
4
-
5
3
  try {
4
+ const { runGgactionMcpServer } = await import("./server.js");
6
5
  await runGgactionMcpServer();
7
6
  } catch (error) {
8
- const message = error 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
+ }