ggaction 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +23 -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 +40 -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/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 +3 -3
  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 +23 -17
  49. package/src/actions/data/density.js +4 -4
  50. package/src/actions/data/derived.js +25 -15
  51. package/src/actions/data/ecdf.js +2 -2
  52. package/src/actions/data/edit.js +54 -29
  53. package/src/actions/data/filter.js +29 -24
  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 +58 -54
  59. package/src/actions/data/interval.js +4 -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 +9 -7
  63. package/src/actions/data/revise.js +80 -0
  64. package/src/actions/data/revisionBindings.js +13 -33
  65. package/src/actions/data/shared.js +30 -2
  66. package/src/actions/data/sort.js +21 -0
  67. package/src/actions/data/stack.js +2 -2
  68. package/src/actions/data/summary.js +3 -3
  69. package/src/actions/data/timeUnit.js +2 -2
  70. package/src/actions/data/window.js +2 -2
  71. package/src/actions/encodings/angle.js +5 -2
  72. package/src/actions/encodings/appearance.js +10 -6
  73. package/src/actions/encodings/barWidth.js +1 -1
  74. package/src/actions/encodings/channels.js +2 -2
  75. package/src/actions/encodings/color/continuous.js +4 -1
  76. package/src/actions/encodings/color/index.js +5 -2
  77. package/src/actions/encodings/density.js +2 -2
  78. package/src/actions/encodings/histogram.js +1 -1
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/offset.js +6 -3
  81. package/src/actions/encodings/parallel.js +4 -5
  82. package/src/actions/encodings/pathOrder.js +6 -11
  83. package/src/actions/encodings/position/index.js +4 -4
  84. package/src/actions/encodings/position/resolve.js +6 -1
  85. package/src/actions/encodings/ranged.js +11 -6
  86. package/src/actions/encodings/remove.js +4 -5
  87. package/src/actions/encodings/ruleAppearance.js +5 -2
  88. package/src/actions/encodings/seriesLayout.js +1 -1
  89. package/src/actions/encodings/stroke.js +5 -2
  90. package/src/actions/encodings/strokeDash.js +8 -3
  91. package/src/actions/encodings/text.js +7 -2
  92. package/src/actions/errorBands/create.js +11 -17
  93. package/src/actions/errorBands/edit.js +13 -131
  94. package/src/actions/errorBars/create.js +8 -13
  95. package/src/actions/errorBars/edit.js +10 -121
  96. package/src/actions/facets/actions.js +30 -16
  97. package/src/actions/facets/derive.js +8 -2
  98. package/src/actions/facets/guides.js +4 -5
  99. package/src/actions/facets/replay.js +4 -5
  100. package/src/actions/gradientPlots/components.js +5 -6
  101. package/src/actions/gradientPlots/create.js +4 -5
  102. package/src/actions/gradientPlots/edit.js +4 -5
  103. package/src/actions/gradientPlots/materialize.js +2 -2
  104. package/src/actions/gradientPlots/rebind.js +4 -5
  105. package/src/actions/guides/axes/axes.js +23 -4
  106. package/src/actions/guides/axes/axis.js +2 -2
  107. package/src/actions/guides/axes/edit.js +2 -2
  108. package/src/actions/guides/axes/labels.js +9 -9
  109. package/src/actions/guides/axes/lines.js +9 -7
  110. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  111. package/src/actions/guides/axes/parallel.js +5 -5
  112. package/src/actions/guides/axes/remove.js +4 -4
  113. package/src/actions/guides/axes/tickGroups.js +4 -4
  114. package/src/actions/guides/axes/ticks.js +13 -4
  115. package/src/actions/guides/axes/titles.js +10 -10
  116. package/src/actions/guides/grids/grid.js +18 -29
  117. package/src/actions/guides/guides.js +1 -1
  118. package/src/actions/guides/legends/blocks.js +1 -1
  119. package/src/actions/guides/legends/categorical/actions.js +4 -4
  120. package/src/actions/guides/legends/categorical/components.js +38 -33
  121. package/src/actions/guides/legends/categorical/layout.js +1 -1
  122. package/src/actions/guides/legends/categorical/symbols.js +27 -30
  123. package/src/actions/guides/legends/continuous/common.js +46 -8
  124. package/src/actions/guides/legends/continuous/gradient.js +89 -155
  125. package/src/actions/guides/legends/continuous/interval.js +13 -13
  126. package/src/actions/guides/legends/continuous/opacity.js +30 -79
  127. package/src/actions/guides/legends/continuous/stroke.js +22 -322
  128. package/src/actions/guides/legends/edit.js +1 -1
  129. package/src/actions/guides/legends/focused.js +5 -5
  130. package/src/actions/guides/legends/lane.js +6 -6
  131. package/src/actions/guides/legends/remove.js +4 -5
  132. package/src/actions/guides/legends/size.js +21 -82
  133. package/src/actions/guides/legends/strokeWidth.js +17 -77
  134. package/src/actions/guides/polar/axes/facade.js +4 -4
  135. package/src/actions/guides/polar/axes/labels.js +5 -5
  136. package/src/actions/guides/polar/axes/lines.js +9 -15
  137. package/src/actions/guides/polar/axes/ticks.js +9 -15
  138. package/src/actions/guides/polar/axes/titles.js +5 -5
  139. package/src/actions/guides/polar/grids.js +11 -13
  140. package/src/actions/index.js +2 -0
  141. package/src/actions/intervals/revision.js +126 -0
  142. package/src/actions/marks/arc/actions.js +3 -3
  143. package/src/actions/marks/area/actions.js +4 -4
  144. package/src/actions/marks/bar/create.js +9 -2
  145. package/src/actions/marks/bar/edit.js +13 -6
  146. package/src/actions/marks/bar/materialize.js +12 -38
  147. package/src/actions/marks/line/actions.js +3 -3
  148. package/src/actions/marks/point/create.js +9 -2
  149. package/src/actions/marks/point/edit.js +13 -6
  150. package/src/actions/marks/point/jitter.js +6 -8
  151. package/src/actions/marks/point/materialize.js +20 -26
  152. package/src/actions/marks/point/packing.js +6 -8
  153. package/src/actions/marks/rect/actions.js +26 -11
  154. package/src/actions/marks/references.js +8 -10
  155. package/src/actions/marks/remove.js +5 -49
  156. package/src/actions/marks/rule/actions.js +28 -12
  157. package/src/actions/marks/text/actions.js +30 -11
  158. package/src/actions/marks/text/layout.js +12 -20
  159. package/src/actions/marks/text/removal.js +1 -1
  160. package/src/actions/marks/text/selection.js +4 -5
  161. package/src/actions/marks/tick/actions.js +26 -11
  162. package/src/actions/primitives/createGraphics.js +5 -4
  163. package/src/actions/primitives/editGraphics.js +5 -4
  164. package/src/actions/primitives/graphicProperties.js +6 -0
  165. package/src/actions/primitives/semantic.js +10 -3
  166. package/src/actions/primitives/semanticAction.js +44 -7
  167. package/src/actions/primitives/semanticValidation/dataset.js +14 -4
  168. package/src/actions/primitives/semanticValidation/layer.js +22 -2
  169. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  170. package/src/actions/regression/components.js +10 -14
  171. package/src/actions/regression/create.js +28 -8
  172. package/src/actions/regression/edit.js +50 -16
  173. package/src/actions/regression/follow.js +22 -0
  174. package/src/actions/resources/remove.js +12 -11
  175. package/src/actions/scales/channels.js +11 -11
  176. package/src/actions/scales/consumers/common.js +2 -1
  177. package/src/actions/scales/create.js +3 -2
  178. package/src/actions/scales/definitions.js +1 -1
  179. package/src/actions/scales/edit.js +2 -2
  180. package/src/actions/scales/materialize.js +1 -1
  181. package/src/actions/scales/offset.js +2 -2
  182. package/src/actions/scales/parallel.js +2 -2
  183. package/src/actions/scales/patch.js +1 -1
  184. package/src/actions/scales/preview.js +28 -1
  185. package/src/actions/scales/quantitativeColor.js +4 -5
  186. package/src/actions/selection/actions.js +25 -36
  187. package/src/actions/textMetrics/index.js +56 -0
  188. package/src/actions/theme/actions.js +2 -2
  189. package/src/actions/theme/reconcile.js +67 -121
  190. package/src/actions/titles/actions.js +8 -8
  191. package/src/actions/titles/resolve.js +3 -3
  192. package/src/actions/violinPlots/create.js +1 -1
  193. package/src/actions/violinPlots/edit.js +1 -1
  194. package/src/core/action.js +109 -36
  195. package/src/core/diagnostics.js +16 -0
  196. package/src/core/extensionRegistry.js +27 -0
  197. package/src/core/font.js +18 -0
  198. package/src/core/identifiers.js +5 -4
  199. package/src/core/immutable.js +10 -3
  200. package/src/core/optionRequirements.js +8 -0
  201. package/src/core/programState.js +6 -2
  202. package/src/core/resourceReferences.js +15 -6
  203. package/src/core/textMetricProfile.js +28 -0
  204. package/src/core/textMetrics.js +24 -3
  205. package/src/core/validation.js +22 -10
  206. package/src/diagnostics.js +1 -0
  207. package/src/grammar/bin.js +39 -10
  208. package/src/grammar/datasetSchema.js +386 -0
  209. package/src/grammar/density.js +53 -5
  210. package/src/grammar/filter.js +133 -39
  211. package/src/grammar/impute.js +3 -2
  212. package/src/grammar/interval.js +32 -1
  213. package/src/grammar/itemMissing.js +59 -0
  214. package/src/grammar/regression/derive.js +57 -16
  215. package/src/grammar/regression/index.js +1 -0
  216. package/src/grammar/regression/models.js +51 -22
  217. package/src/grammar/regression/parameters.js +56 -4
  218. package/src/grammar/scales/definition.js +7 -0
  219. package/src/grammar/scales/types.js +8 -0
  220. package/src/grammar/schemas/graphicBounds.js +11 -11
  221. package/src/grammar/schemas/semanticPath.js +3 -2
  222. package/src/grammar/sort.js +88 -0
  223. package/src/grammar/summary.js +117 -19
  224. package/src/grammar/transformTopology.js +1 -0
  225. package/src/grammar/transforms.js +21 -11
  226. package/src/inspection.js +495 -0
  227. package/src/inspectionDescriptors.js +3 -0
  228. package/src/layout/labels.js +11 -10
  229. package/src/layout/legend.js +4 -4
  230. package/src/layout/legendItems.js +6 -6
  231. package/src/layout/text.js +10 -9
  232. package/src/layout/title.js +9 -9
  233. package/src/materialization/bars/resolve.js +4 -2
  234. package/src/materialization/facetGuides/placement.js +1 -1
  235. package/src/materialization/facetGuides/preparation.js +1 -1
  236. package/src/materialization/facetHeaders.js +11 -10
  237. package/src/materialization/guides/layout.js +3 -3
  238. package/src/materialization/revisionIdentity.js +19 -0
  239. package/src/materialization/scaleGuideDependencies.js +10 -0
  240. package/src/materialization/scales/resolve.js +67 -4
  241. package/src/materialization/selection/items/common.js +1 -1
  242. package/src/materialization/text.js +6 -4
  243. package/src/materialization/typography.js +31 -0
  244. package/src/mcp/cli.js +5 -3
  245. package/src/persistence/codec.js +65 -0
  246. package/src/persistence/validation.js +185 -0
  247. package/src/persistence.js +147 -0
  248. package/src/renderers/canvas/index.js +5 -4
  249. package/src/renderers/nativeBackend.js +10 -0
  250. package/src/renderers/options.js +13 -0
  251. package/src/renderers/pdf.js +25 -54
  252. package/src/renderers/png.js +22 -17
  253. package/src/renderers/svg.js +10 -14
  254. package/src/renderers/text.js +1 -11
  255. package/src/selectors/coordinates.js +3 -1
  256. package/src/selectors/datasets.js +8 -6
  257. package/src/selectors/layers.js +6 -4
  258. package/src/selectors/markOwners.js +46 -0
  259. package/src/selectors/scales.js +6 -4
  260. package/src/theme/defaults.js +2 -1
  261. package/src/version.js +2 -0
  262. package/types/accessibility.d.ts +37 -0
  263. package/types/basic.d.ts +4 -2
  264. package/types/diagnostics.d.ts +15 -0
  265. package/types/index.d.ts +5 -0
  266. package/types/inspection.d.ts +17 -0
  267. package/types/pdf.d.ts +9 -0
  268. package/types/persistence.d.ts +7 -0
  269. package/types/png.d.ts +9 -0
  270. package/types/program.d.ts +181 -28
@@ -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
+ }
@@ -0,0 +1,147 @@
1
+ import { ChartProgram } from "./ChartProgram.js";
2
+ import { BasicChartProgram } from "./BasicChartProgram.js";
3
+ import { cloneAndFreeze } from "./core/immutable.js";
4
+ import { hasRegisteredExtension, isBuiltinProgramClass } from "./core/extensionRegistry.js";
5
+ import { packageVersion } from "./version.js";
6
+ import { decodeValue, encodeValue, invalidSnapshot } from "./persistence/codec.js";
7
+ import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
8
+ import {
9
+ deriveTransformSchema,
10
+ inferDatasetSchema,
11
+ validateDatasetSchema
12
+ } from "./grammar/datasetSchema.js";
13
+ import { restoreResolvedScaleBindings } from "./actions/scales/preview.js";
14
+
15
+ function canonicalState(program, extensions, path = "payload") {
16
+ if (!(program instanceof ChartProgram || program instanceof BasicChartProgram) || !isBuiltinProgramClass(program.constructor)) {
17
+ throw invalidSnapshot(`Cannot persist an unregistered program class at ${path}.`, path);
18
+ }
19
+ if (program.actionStack.length !== 0) throw invalidSnapshot("Cannot persist an open actionStack.", `${path}.actionStack`);
20
+ const state = Object.fromEntries(STATE_KEYS.map(key => [key, program[key]]));
21
+ state.children = Object.fromEntries(Object.entries(program.children).map(([id, child]) => [id, canonicalState(child, extensions, `${path}.children.${id}`)]));
22
+ validateTrace(program.trace, program.constructor, extensions, `${path}.trace`);
23
+ validateProgramState(program, path);
24
+ return state;
25
+ }
26
+
27
+ function envelope(kind, payload, extensions = []) {
28
+ return JSON.stringify({ schemaVersion: kind === "editable" ? 2 : 1, kind, packageVersion, extensions, payload: encodeValue(payload) });
29
+ }
30
+
31
+ function readEnvelope(text, kind) {
32
+ if (typeof text !== "string") throw invalidSnapshot("Snapshot must be a JSON string.", "snapshot");
33
+ let value;
34
+ try { value = JSON.parse(text); } catch { throw invalidSnapshot("Snapshot must be valid JSON.", "snapshot"); }
35
+ exactKeys(value, ["schemaVersion", "kind", "packageVersion", "extensions", "payload"], "snapshot");
36
+ const versions = kind === "editable" ? [1, 2] : [1];
37
+ if (!versions.includes(value.schemaVersion) || value.kind !== kind) {
38
+ throw invalidSnapshot(
39
+ `Unsupported snapshot schema or kind; expected schema ${versions.join(" or ")} ${kind}.`,
40
+ "snapshot"
41
+ );
42
+ }
43
+ if (typeof value.packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value.packageVersion)) throw invalidSnapshot("Invalid snapshot packageVersion.", "snapshot.packageVersion");
44
+ if (!Array.isArray(value.extensions) || new Set(value.extensions).size !== value.extensions.length ||
45
+ value.extensions.some(name => typeof name !== "string" || !hasRegisteredExtension(name)) || (kind === "graphic" && value.extensions.length)) {
46
+ throw invalidSnapshot("Snapshot requires unknown, duplicate, or inapplicable extensions.", "snapshot.extensions");
47
+ }
48
+ return { ...value, payload: decodeValue(value.payload) };
49
+ }
50
+
51
+ function restoreState(state, extensions, path = "payload", { requireSchemas = false } = {}) {
52
+ exactKeys(state, STATE_KEYS, path);
53
+ if (!Array.isArray(state.actionStack) || state.actionStack.length) throw invalidSnapshot("Snapshot actionStack must be empty.", `${path}.actionStack`);
54
+ requireObject(state.children, `${path}.children`);
55
+ validateTrace(state.trace, ChartProgram, extensions, `${path}.trace`);
56
+ const children = Object.fromEntries(Object.entries(state.children).map(([id, child]) => [id, restoreState(
57
+ child,
58
+ extensions,
59
+ `${path}.children.${id}`,
60
+ { requireSchemas }
61
+ )]));
62
+ const program = new ChartProgram({ ...state, children });
63
+ validateProgramState(program, path);
64
+ if (requireSchemas) {
65
+ for (const dataset of program.semanticSpec.datasets) {
66
+ if (dataset.schema === undefined) {
67
+ throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2.`, `${path}.semanticSpec.datasets`);
68
+ }
69
+ validateDatasetSchema(dataset.schema);
70
+ }
71
+ }
72
+ return restoreResolvedScaleBindings(program);
73
+ }
74
+
75
+ function migrateStateSchemas(state) {
76
+ const datasets = state.semanticSpec.datasets;
77
+ const byId = new Map(datasets.map(dataset => [dataset.id, dataset]));
78
+ const schemas = new Map();
79
+ const active = new Set();
80
+ function schemaFor(id) {
81
+ if (schemas.has(id)) return schemas.get(id);
82
+ if (active.has(id)) throw invalidSnapshot(`Cyclic data source "${id}".`, "payload.semanticSpec.datasets");
83
+ active.add(id);
84
+ const dataset = byId.get(id);
85
+ let schema;
86
+ if (dataset.schema !== undefined) {
87
+ schema = validateDatasetSchema(dataset.schema);
88
+ } else if (dataset.source === undefined) {
89
+ schema = inferDatasetSchema(dataset.values ?? []);
90
+ } else {
91
+ const sourceSchema = schemaFor(dataset.source);
92
+ schema = deriveTransformSchema(
93
+ sourceSchema,
94
+ dataset.transform?.[0] ?? {},
95
+ dataset.values,
96
+ { sourceId: dataset.source, ownerId: dataset.id }
97
+ );
98
+ }
99
+ active.delete(id);
100
+ schemas.set(id, schema);
101
+ return schema;
102
+ }
103
+ const semanticSpec = {
104
+ ...state.semanticSpec,
105
+ datasets: datasets.map(dataset => ({ ...dataset, schema: schemaFor(dataset.id) }))
106
+ };
107
+ return {
108
+ ...state,
109
+ semanticSpec,
110
+ children: Object.fromEntries(Object.entries(state.children).map(
111
+ ([id, child]) => [id, migrateStateSchemas(child)]
112
+ ))
113
+ };
114
+ }
115
+
116
+ export function serializeProgram(program) {
117
+ const extensions = new Set();
118
+ return envelope("editable", canonicalState(program, extensions), [...extensions].sort());
119
+ }
120
+
121
+ export function deserializeProgram(text) {
122
+ const stored = readEnvelope(text, "editable");
123
+ const extensions = new Set();
124
+ let payload = stored.payload;
125
+ if (stored.schemaVersion === 1) {
126
+ // Validate the historical state before adding any inferred metadata. Migration
127
+ // must never repair an invalid reference graph or malformed payload.
128
+ restoreState(payload, new Set());
129
+ payload = migrateStateSchemas(payload);
130
+ }
131
+ const program = restoreState(payload, extensions, "payload", { requireSchemas: true });
132
+ if (extensions.size !== stored.extensions.length || stored.extensions.some(name => !extensions.has(name))) {
133
+ throw invalidSnapshot("Snapshot extensions must exactly match its action traces.", "snapshot.extensions");
134
+ }
135
+ return program;
136
+ }
137
+
138
+ export function serializeGraphic(program) {
139
+ validateGraphic(program?.graphicSpec);
140
+ return envelope("graphic", program.graphicSpec);
141
+ }
142
+
143
+ export function deserializeGraphic(text) {
144
+ const { payload } = readEnvelope(text, "graphic");
145
+ validateGraphic(payload);
146
+ return cloneAndFreeze({ graphicSpec: payload });
147
+ }
@@ -1,3 +1,4 @@
1
+ import { annotateError } from "../../core/diagnostics.js";
1
2
  import { drawCircleGraphic } from "./circle.js";
2
3
  import { drawLineGraphic } from "./line.js";
3
4
  import { drawPathGraphic } from "./path.js";
@@ -208,14 +209,14 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
208
209
  physicalWidth > MAX_RASTER_DIMENSION ||
209
210
  physicalHeight > MAX_RASTER_DIMENSION
210
211
  ) {
211
- throw new RangeError(
212
+ throw annotateError(new RangeError(
212
213
  `render physical Canvas dimensions must not exceed ${MAX_RASTER_DIMENSION}.`
213
- );
214
+ ), { code: "resource-limit", operation: "render", optionPath: "physicalDimensions", limit: MAX_RASTER_DIMENSION, actual: Math.max(physicalWidth, physicalHeight) });
214
215
  }
215
216
  if (physicalWidth * physicalHeight > MAX_RASTER_PIXELS) {
216
- throw new RangeError(
217
+ throw annotateError(new RangeError(
217
218
  `render physical Canvas pixel count must not exceed ${MAX_RASTER_PIXELS}.`
218
- );
219
+ ), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
219
220
  }
220
221
  preflightCanvasGraphicSpec(target, pixelRatio);
221
222
 
@@ -0,0 +1,10 @@
1
+ export async function loadNativeBackend() {
2
+ try {
3
+ return await import("@napi-rs/canvas");
4
+ } catch (error) {
5
+ if (error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes("@napi-rs/canvas")) {
6
+ throw new Error("PNG/PDF output requires @napi-rs/canvas. Install it with: npm install ggaction @napi-rs/canvas", { cause: error });
7
+ }
8
+ throw error;
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ import { annotateError } from "../core/diagnostics.js";
2
+ import { isPlainObject } from "../core/immutable.js";
3
+
4
+ export function validateRendererOptions(options, allowed, label) {
5
+ if (!isPlainObject(options)) {
6
+ throw annotateError(new TypeError(`${label} must be a plain object.`), { code: "invalid-option" });
7
+ }
8
+ for (const key of Object.keys(options)) {
9
+ if (!allowed.has(key)) {
10
+ throw annotateError(new TypeError(`${label} does not support option "${key}".`), { code: "invalid-option", optionPath: key });
11
+ }
12
+ }
13
+ }
@@ -1,7 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
 
4
- import { PDFDocument } from "@napi-rs/canvas";
4
+ import { loadNativeBackend } from "./nativeBackend.js";
5
5
 
6
6
  import {
7
7
  drawResolvedGraphicSpec,
@@ -9,8 +9,10 @@ import {
9
9
  resolveGraphicRenderTarget
10
10
  } from "./canvas/index.js";
11
11
  import { preflightCanvasGraphicSpec } from "./canvas/native.js";
12
+ import { validateRendererOptions } from "./options.js";
12
13
 
13
14
  const PDF_OPTIONS = new Set(["output", "metadata"]);
15
+ const BUFFER_OPTIONS = new Set(["metadata"]);
14
16
  const PDF_METADATA = new Set([
15
17
  "title",
16
18
  "author",
@@ -19,30 +21,9 @@ const PDF_METADATA = new Set([
19
21
  ]);
20
22
  const MAX_PDF_DIMENSION = 16_777_216;
21
23
 
22
- function requirePlainObject(value, label) {
23
- if (
24
- value === null ||
25
- typeof value !== "object" ||
26
- Array.isArray(value) ||
27
- Object.getPrototypeOf(value) !== Object.prototype
28
- ) {
29
- throw new TypeError(`${label} must be a plain object.`);
30
- }
31
- return value;
32
- }
33
-
34
- function requireClosedKeys(value, allowed, label) {
35
- for (const key of Object.keys(value)) {
36
- if (!allowed.has(key)) {
37
- throw new TypeError(`${label} does not support option "${key}".`);
38
- }
39
- }
40
- }
41
-
42
24
  function requireMetadata(metadata) {
43
25
  if (metadata === undefined) return undefined;
44
- requirePlainObject(metadata, "renderToPDF metadata");
45
- requireClosedKeys(metadata, PDF_METADATA, "renderToPDF metadata");
26
+ validateRendererOptions(metadata, PDF_METADATA, "renderToPDF metadata");
46
27
 
47
28
  const resolved = {};
48
29
  for (const key of ["title", "author", "subject"]) {
@@ -75,26 +56,6 @@ function requireMetadata(metadata) {
75
56
  return resolved;
76
57
  }
77
58
 
78
- function requirePDFOptions(options) {
79
- requirePlainObject(options, "renderToPDF options");
80
- requireClosedKeys(options, PDF_OPTIONS, "renderToPDF");
81
- if (typeof options.output !== "string" || options.output.length === 0) {
82
- throw new TypeError("renderToPDF requires a non-empty output path.");
83
- }
84
- return {
85
- output: resolve(options.output),
86
- metadata: requireMetadata(options.metadata)
87
- };
88
- }
89
-
90
- function renderPDFBuffer(target, metadata) {
91
- const document = new PDFDocument(metadata);
92
- const context = document.beginPage(target.width, target.height);
93
- drawResolvedGraphicSpec(target, context);
94
- document.endPage();
95
- return document.close();
96
- }
97
-
98
59
  function requirePDFPageDimensions(target) {
99
60
  if (![target.width, target.height].every(value =>
100
61
  Number.isSafeInteger(value) && value > 0 && value <= MAX_PDF_DIMENSION
@@ -105,22 +66,32 @@ function requirePDFPageDimensions(target) {
105
66
  }
106
67
  }
107
68
 
108
- export async function renderToPDF(program, options = {}) {
109
- const { output, metadata } = requirePDFOptions(options);
69
+ export async function renderToPDFBuffer(program, options = {}) {
70
+ validateRendererOptions(options, BUFFER_OPTIONS, "renderToPDFBuffer options");
71
+ const metadata = requireMetadata(options.metadata);
110
72
  const graphicSpec = requireProgramGraphicSpec(program);
111
73
  const target = resolveGraphicRenderTarget(graphicSpec);
112
74
  requirePDFPageDimensions(target);
113
75
  preflightCanvasGraphicSpec(target);
114
- const buffer = renderPDFBuffer(target, metadata);
76
+ const { PDFDocument } = await loadNativeBackend();
77
+ const document = new PDFDocument(metadata);
78
+ const context = document.beginPage(target.width, target.height);
79
+ drawResolvedGraphicSpec(target, context);
80
+ document.endPage();
81
+ const buffer = document.close();
82
+ return Object.freeze({
83
+ buffer, width: target.width, height: target.height, pages: 1, bytes: buffer.length
84
+ });
85
+ }
115
86
 
87
+ export async function renderToPDF(program, options = {}) {
88
+ validateRendererOptions(options, PDF_OPTIONS, "renderToPDF options");
89
+ if (typeof options.output !== "string" || options.output.length === 0) {
90
+ throw new TypeError("renderToPDF requires a non-empty output path.");
91
+ }
92
+ const { buffer, ...result } = await renderToPDFBuffer(program, { metadata: options.metadata });
93
+ const output = resolve(options.output);
116
94
  await mkdir(dirname(output), { recursive: true });
117
95
  await writeFile(output, buffer);
118
-
119
- return Object.freeze({
120
- output,
121
- width: target.width,
122
- height: target.height,
123
- pages: 1,
124
- bytes: buffer.length
125
- });
96
+ return Object.freeze({ output, ...result });
126
97
  }
@@ -1,29 +1,34 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
+ import { render } from "./canvas/index.js";
4
+ import { validateRendererOptions } from "./options.js";
5
+ import { loadNativeBackend } from "./nativeBackend.js";
3
6
 
4
- import { createCanvas } from "@napi-rs/canvas";
7
+ const PNG_OPTIONS = new Set(["output", "pixelRatio"]);
8
+ const BUFFER_OPTIONS = new Set(["pixelRatio"]);
5
9
 
6
- import { render } from "./canvas/index.js";
10
+ export async function renderToPNGBuffer(program, options = {}) {
11
+ validateRendererOptions(options, BUFFER_OPTIONS, "renderToPNGBuffer options");
12
+ const { pixelRatio = 1 } = options;
13
+ const { createCanvas } = await loadNativeBackend();
14
+ const canvas = createCanvas(1, 1);
15
+ render(program, canvas.getContext("2d"), { pixelRatio });
16
+ // Canvas drawing is synchronous; encoding uses the native async worker.
17
+ const buffer = await canvas.encode("png");
18
+ return Object.freeze({
19
+ buffer, width: canvas.width, height: canvas.height, pixelRatio, bytes: buffer.length
20
+ });
21
+ }
7
22
 
8
- export async function renderToPNG(program, { output, pixelRatio = 1 } = {}) {
23
+ export async function renderToPNG(program, options = {}) {
24
+ validateRendererOptions(options, PNG_OPTIONS, "renderToPNG options");
25
+ const { output, pixelRatio } = options;
9
26
  if (typeof output !== "string" || output.length === 0) {
10
27
  throw new TypeError("renderToPNG requires a non-empty output path.");
11
28
  }
12
-
13
- const canvas = createCanvas(1, 1);
14
- const context = canvas.getContext("2d");
15
- render(program, context, { pixelRatio });
16
-
29
+ const { buffer, ...result } = await renderToPNGBuffer(program, { pixelRatio });
17
30
  const path = resolve(output);
18
- const buffer = canvas.toBuffer("image/png");
19
31
  await mkdir(dirname(path), { recursive: true });
20
32
  await writeFile(path, buffer);
21
-
22
- return Object.freeze({
23
- output: path,
24
- width: canvas.width,
25
- height: canvas.height,
26
- pixelRatio,
27
- bytes: buffer.length
28
- });
33
+ return Object.freeze({ output: path, ...result });
29
34
  }
@@ -1,3 +1,4 @@
1
+ import { isOwned } from "../core/immutable.js";
1
2
  import {
2
3
  requireSingleOrderedGraphicByType,
3
4
  walkGraphicTreeEvents
@@ -17,6 +18,7 @@ import {
17
18
  requireStringProperty
18
19
  } from "./canvas/validation.js";
19
20
  import { normalizeRendererFontWeight } from "./text.js";
21
+ import { validateRendererOptions } from "./options.js";
20
22
 
21
23
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
22
24
  const SVG_OPTIONS = new Set(["title", "description", "resourceNamespace"]);
@@ -37,14 +39,20 @@ const DOMINANT_BASELINES = Object.freeze({
37
39
  bottom: "text-after-edge"
38
40
  });
39
41
 
42
+ const namespaceByGraphic = new WeakMap();
43
+
40
44
  function resourceNamespace(graphicSpec) {
45
+ const cached = namespaceByGraphic.get(graphicSpec);
46
+ if (cached !== undefined) return cached;
41
47
  const serialized = JSON.stringify(graphicSpec);
42
48
  let hash = 0xcbf29ce484222325n;
43
49
  for (let index = 0; index < serialized.length; index += 1) {
44
50
  hash ^= BigInt(serialized.charCodeAt(index));
45
51
  hash = BigInt.asUintN(64, hash * 0x100000001b3n);
46
52
  }
47
- return hash.toString(36);
53
+ const namespace = hash.toString(36);
54
+ if (isOwned(graphicSpec)) namespaceByGraphic.set(graphicSpec, namespace);
55
+ return namespace;
48
56
  }
49
57
 
50
58
  function escapeText(value) {
@@ -525,19 +533,7 @@ function serializeBody(graphicSpec, rootId, rootCanvas, width, height, state) {
525
533
  }
526
534
 
527
535
  function requireSVGOptions(options) {
528
- if (
529
- options === null ||
530
- typeof options !== "object" ||
531
- Array.isArray(options) ||
532
- Object.getPrototypeOf(options) !== Object.prototype
533
- ) {
534
- throw new TypeError("renderToSVG options must be a plain object.");
535
- }
536
- for (const key of Object.keys(options)) {
537
- if (!SVG_OPTIONS.has(key)) {
538
- throw new TypeError(`renderToSVG does not support option "${key}".`);
539
- }
540
- }
536
+ validateRendererOptions(options, SVG_OPTIONS, "renderToSVG options");
541
537
  for (const key of SVG_OPTIONS) {
542
538
  if (
543
539
  options[key] !== undefined &&
@@ -1,11 +1 @@
1
- const MIN_RENDERER_NUMERIC_FONT_WEIGHT = 100;
2
- const MAX_RENDERER_NUMERIC_FONT_WEIGHT = 900;
3
-
4
- export function normalizeRendererFontWeight(fontWeight) {
5
- if (typeof fontWeight !== "number") return fontWeight;
6
- const rounded = Math.round(fontWeight / 100) * 100;
7
- return Math.min(
8
- MAX_RENDERER_NUMERIC_FONT_WEIGHT,
9
- Math.max(MIN_RENDERER_NUMERIC_FONT_WEIGHT, rounded)
10
- );
11
- }
1
+ export { normalizeRendererFontWeight } from "../core/font.js";
@@ -1,3 +1,5 @@
1
+ import { annotateError } from "../core/diagnostics.js";
2
+
1
3
  export function findCoordinate(program, id) {
2
4
  return program.semanticSpec.coordinates.find(coordinate => coordinate.id === id);
3
5
  }
@@ -8,6 +10,6 @@ export function hasCoordinate(program, id) {
8
10
 
9
11
  export function requireCoordinate(program, id) {
10
12
  const coordinate = findCoordinate(program, id);
11
- if (coordinate === undefined) throw new Error(`Unknown coordinate "${id}".`);
13
+ if (coordinate === undefined) throw annotateError(new Error(`Unknown coordinate "${id}".`), { code: "missing-resource", resourceId: id });
12
14
  return coordinate;
13
15
  }