ggaction 0.0.21 → 0.0.23

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 (265) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +83 -90
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/package.json +11 -4
  8. package/src/BasicChartProgram.js +1 -1
  9. package/src/accessibility.js +1 -0
  10. package/src/actions/boxPlots/components.js +1 -96
  11. package/src/actions/boxPlots/create.js +6 -1
  12. package/src/actions/boxPlots/edit.js +8 -0
  13. package/src/actions/boxPlots/materialize.js +3 -0
  14. package/src/actions/boxPlots/options.js +1 -1
  15. package/src/actions/canvas/actions.js +2 -1
  16. package/src/actions/canvas/index.js +3 -0
  17. package/src/actions/canvas/plotBorder.js +2 -1
  18. package/src/actions/canvas/plotOrder.js +14 -0
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +4 -4
  21. package/src/actions/charts/density.js +1 -1
  22. package/src/actions/charts/ecdf.js +1 -1
  23. package/src/actions/charts/endpoints.js +54 -5
  24. package/src/actions/charts/heatmap.js +1 -1
  25. package/src/actions/charts/histogram.js +1 -1
  26. package/src/actions/charts/horizon.js +1 -1
  27. package/src/actions/charts/index.js +2 -1
  28. package/src/actions/charts/interval-regression.js +3 -3
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/radar.js +63 -3
  31. package/src/actions/charts/raincloud.js +37 -6
  32. package/src/actions/charts/rug-strip.js +2 -2
  33. package/src/actions/charts/scatter.js +3 -3
  34. package/src/actions/charts/text.js +1 -1
  35. package/src/actions/composition/actions.js +10 -3
  36. package/src/actions/coordinates/actions.js +2 -0
  37. package/src/actions/data/basic.js +6 -0
  38. package/src/actions/data/derived.js +6 -2
  39. package/src/actions/data/edit.js +22 -36
  40. package/src/actions/data/filter.js +1 -376
  41. package/src/actions/data/flatten.js +19 -0
  42. package/src/actions/data/index.js +6 -0
  43. package/src/actions/data/lookup.js +50 -0
  44. package/src/actions/data/raincloudFilter.js +56 -0
  45. package/src/actions/data/shared.js +10 -0
  46. package/src/actions/data/timeUnit.js +1 -1
  47. package/src/actions/distributions/revision.js +3 -1
  48. package/src/actions/encodings/angle.js +2 -1
  49. package/src/actions/encodings/appearance.js +38 -13
  50. package/src/actions/encodings/channels.js +14 -3
  51. package/src/actions/encodings/color/continuous.js +26 -10
  52. package/src/actions/encodings/color/index.js +50 -20
  53. package/src/actions/encodings/color/policy.js +57 -4
  54. package/src/actions/encodings/parallel.js +4 -0
  55. package/src/actions/encodings/position/policies/bar.js +10 -6
  56. package/src/actions/encodings/position/policies/line.js +1 -152
  57. package/src/actions/encodings/position/resolve.js +4 -1
  58. package/src/actions/encodings/ranged.js +46 -0
  59. package/src/actions/encodings/remove.js +1 -1
  60. package/src/actions/encodings/ruleAppearance.js +7 -6
  61. package/src/actions/encodings/shared.js +14 -2
  62. package/src/actions/encodings/stroke.js +20 -14
  63. package/src/actions/encodings/strokeDash.js +9 -11
  64. package/src/actions/encodings/text.js +30 -2
  65. package/src/actions/errorBars/edit.js +24 -4
  66. package/src/actions/facets/actions.js +1 -793
  67. package/src/actions/facets/derive.js +34 -5
  68. package/src/actions/facets/replay.js +1 -38
  69. package/src/actions/gradientPlots/components.js +21 -9
  70. package/src/actions/gradientPlots/create.js +4 -2
  71. package/src/actions/gradientPlots/edit.js +18 -1
  72. package/src/actions/gradientPlots/materialize.js +35 -36
  73. package/src/actions/gradientPlots/options.js +1 -1
  74. package/src/actions/gradientPlots/paint.js +13 -7
  75. package/src/actions/guides/applicability.js +2 -0
  76. package/src/actions/guides/axes/axes.js +39 -0
  77. package/src/actions/guides/axes/axis.js +9 -5
  78. package/src/actions/guides/axes/edit.js +11 -8
  79. package/src/actions/guides/axes/labels.js +24 -8
  80. package/src/actions/guides/axes/parallel/policy.js +3 -5
  81. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  82. package/src/actions/guides/axes/policy.js +22 -0
  83. package/src/actions/guides/axes/tickGroups.js +4 -2
  84. package/src/actions/guides/axes/ticks.js +22 -4
  85. package/src/actions/guides/axes/titles.js +19 -6
  86. package/src/actions/guides/facadeAxes.js +3 -0
  87. package/src/actions/guides/grids/grid.js +3 -1
  88. package/src/actions/guides/grids/resolve.js +7 -11
  89. package/src/actions/guides/guides.js +21 -1
  90. package/src/actions/guides/legends/categorical/actions.js +1 -515
  91. package/src/actions/guides/legends/categorical/components.js +1 -4
  92. package/src/actions/guides/legends/categorical/layout.js +5 -2
  93. package/src/actions/guides/legends/categorical/options.js +4 -0
  94. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  95. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  96. package/src/actions/guides/legends/colorSize.js +2 -0
  97. package/src/actions/guides/legends/continuous/common.js +1 -1
  98. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  99. package/src/actions/guides/legends/continuous/interval.js +1 -1
  100. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  101. package/src/actions/guides/legends/edit.js +1 -1
  102. package/src/actions/guides/legends/focused.js +1 -1
  103. package/src/actions/guides/legends/lane.js +1 -1
  104. package/src/actions/guides/legends/sampling.js +1 -140
  105. package/src/actions/guides/legends/strokeWidth.js +3 -2
  106. package/src/actions/guides/legends/transition.js +19 -2
  107. package/src/actions/guides/polar/axes/facade.js +5 -4
  108. package/src/actions/guides/polar/axes/labels.js +5 -4
  109. package/src/actions/guides/polar/axes/titles.js +5 -4
  110. package/src/actions/guides/polar/resolve.js +2 -1
  111. package/src/actions/guides/tickValues.js +7 -7
  112. package/src/actions/marks/arc/actions.js +5 -7
  113. package/src/actions/marks/area/actions.js +9 -3
  114. package/src/actions/marks/bar/create.js +4 -2
  115. package/src/actions/marks/bar/edit.js +9 -6
  116. package/src/actions/marks/bar/materialize.js +5 -3
  117. package/src/actions/marks/dependentReferences.js +87 -0
  118. package/src/actions/marks/index.js +2 -0
  119. package/src/actions/marks/lifecycle.js +2 -1
  120. package/src/actions/marks/line/actions.js +24 -6
  121. package/src/actions/marks/line/materialize.js +7 -3
  122. package/src/actions/marks/point/create.js +5 -4
  123. package/src/actions/marks/point/edit.js +6 -3
  124. package/src/actions/marks/point/jitter.js +6 -6
  125. package/src/actions/marks/point/materialize.js +36 -39
  126. package/src/actions/marks/rect/actions.js +16 -4
  127. package/src/actions/marks/references.js +183 -35
  128. package/src/actions/marks/rule/actions.js +20 -9
  129. package/src/actions/marks/shared.js +2 -1
  130. package/src/actions/marks/text/actions.js +10 -3
  131. package/src/actions/marks/tick/actions.js +11 -2
  132. package/src/actions/primitives/editGraphics.js +28 -3
  133. package/src/actions/primitives/index.js +9 -0
  134. package/src/actions/primitives/semanticAction.js +13 -7
  135. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  136. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  137. package/src/actions/regression/create.js +6 -1
  138. package/src/actions/regression/edit.js +10 -1
  139. package/src/actions/scales/channels.js +1 -1
  140. package/src/actions/scales/consumers/common.js +19 -1
  141. package/src/actions/scales/consumers/families.js +2 -2
  142. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  143. package/src/actions/scales/edit.js +11 -0
  144. package/src/actions/scales/editPolicy.js +5 -3
  145. package/src/actions/scales/preview.js +2 -1
  146. package/src/actions/scales/quantitativeColor.js +2 -4
  147. package/src/actions/selection/actions.js +10 -1
  148. package/src/actions/theme/reconcile.js +37 -9
  149. package/src/core/compositionState.js +26 -7
  150. package/src/core/datasetReferences.js +17 -0
  151. package/src/core/facetAxisPolicy.js +30 -0
  152. package/src/core/immutable.js +1 -0
  153. package/src/core/resourceReferences.js +5 -3
  154. package/src/core/validation.js +4 -0
  155. package/src/core/vocabulary.js +24 -2
  156. package/src/grammar/aggregate.js +1 -0
  157. package/src/grammar/arcs.js +1 -1
  158. package/src/grammar/areaSeries.js +1 -1
  159. package/src/grammar/axisTextPlacement.js +1 -1
  160. package/src/grammar/bars/policy.js +2 -2
  161. package/src/grammar/curveCommands.js +35 -2
  162. package/src/grammar/datasetDependencies.js +97 -0
  163. package/src/grammar/datasetSchema.js +32 -2
  164. package/src/grammar/facetHeaders.js +1 -0
  165. package/src/grammar/facets/dependencies.js +6 -2
  166. package/src/grammar/facets/guides.js +7 -5
  167. package/src/grammar/facets/legendIntent.js +6 -2
  168. package/src/grammar/facets/scales.js +27 -7
  169. package/src/grammar/flatten.js +85 -0
  170. package/src/grammar/histogram.js +13 -1
  171. package/src/grammar/horizon.js +1 -494
  172. package/src/grammar/jitter.js +2 -2
  173. package/src/grammar/legendSpacing.js +45 -0
  174. package/src/grammar/lineMissing.js +90 -0
  175. package/src/grammar/lineSeries.js +1 -376
  176. package/src/grammar/lookup.js +97 -0
  177. package/src/grammar/markFilter.js +4 -2
  178. package/src/grammar/markLabels.js +8 -3
  179. package/src/grammar/numeric.js +1 -1
  180. package/src/grammar/pathCommands.js +1 -1
  181. package/src/grammar/pathSeries.js +1 -109
  182. package/src/grammar/pointPolygon.js +81 -0
  183. package/src/grammar/pointShapes.js +15 -15
  184. package/src/grammar/rects.js +1 -1
  185. package/src/grammar/regression/derive.js +1 -195
  186. package/src/grammar/regression/parameters.js +7 -0
  187. package/src/grammar/scales/appearance.js +1 -1
  188. package/src/grammar/scales/color.js +1 -1
  189. package/src/grammar/scales/colorConsumers.js +1 -63
  190. package/src/grammar/scales/continuous.js +58 -1
  191. package/src/grammar/scales/definition.js +1 -1
  192. package/src/grammar/scales/ordinal.js +5 -2
  193. package/src/grammar/scales/policies.js +3 -3
  194. package/src/grammar/schemas/graphicBounds.js +40 -12
  195. package/src/grammar/schemas/graphicClip.js +23 -0
  196. package/src/grammar/schemas/graphicTree.js +16 -0
  197. package/src/grammar/statisticalReference.js +22 -0
  198. package/src/grammar/text.js +65 -1
  199. package/src/grammar/ticks.js +92 -1
  200. package/src/grammar/timeUnit.js +9 -3
  201. package/src/grammar/transformTopology.js +3 -1
  202. package/src/grammar/transforms.js +24 -0
  203. package/src/inspection.js +1 -495
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -1
  206. package/src/layout/facetContentFlow.js +101 -0
  207. package/src/layout/facetHeaderTitles.js +1 -0
  208. package/src/layout/facetTopLane.js +29 -0
  209. package/src/layout/facets.js +1 -1
  210. package/src/layout/guideCollisions.js +9 -3
  211. package/src/layout/legendGroup.js +1 -1
  212. package/src/layout/legendItems.js +26 -10
  213. package/src/layout/legendSpacing.js +66 -0
  214. package/src/layout/plotDrawOrder.js +17 -0
  215. package/src/materialization/bars/histogram.js +8 -2
  216. package/src/materialization/bars/resolve.js +1 -1
  217. package/src/materialization/composition.js +1 -0
  218. package/src/materialization/compositionSnapshot.js +22 -2
  219. package/src/materialization/dataProvenance.js +1 -1
  220. package/src/materialization/facetContentBounds.js +96 -0
  221. package/src/materialization/facetGuides/layout.js +1 -1
  222. package/src/materialization/facetGuides/placement.js +39 -11
  223. package/src/materialization/facetGuides/preparation.js +43 -24
  224. package/src/materialization/facetHeaders.js +1 -1
  225. package/src/materialization/facetTopLane.js +25 -0
  226. package/src/materialization/facets.js +1 -1
  227. package/src/materialization/graphicHierarchy.js +30 -1
  228. package/src/materialization/graphicTranslation.js +2 -0
  229. package/src/materialization/guides/layout.js +1 -1
  230. package/src/materialization/guides/resources.js +1 -1
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/markClip.js +26 -0
  233. package/src/materialization/marks/capabilities.js +3 -2
  234. package/src/materialization/marks/grouping.js +7 -5
  235. package/src/materialization/marks/index.js +4 -2
  236. package/src/materialization/marks/policies.js +4 -0
  237. package/src/materialization/plotDrawOrder.js +44 -0
  238. package/src/materialization/rect.js +11 -3
  239. package/src/materialization/scaleGuideDependencies.js +13 -5
  240. package/src/materialization/selection/items/common.js +1 -1
  241. package/src/materialization/selection/items/path.js +1 -0
  242. package/src/materialization/selection/policies/index.js +2 -1
  243. package/src/materialization/text.js +10 -5
  244. package/src/mcp/cli.js +0 -0
  245. package/src/persistence/validation.js +114 -18
  246. package/src/persistence.js +4 -2
  247. package/src/renderers/canvas/index.js +16 -0
  248. package/src/renderers/canvas/isolated.js +11 -1
  249. package/src/renderers/canvas/native.js +7 -0
  250. package/src/renderers/canvas/scene.js +66 -0
  251. package/src/renderers/canvas/text.js +3 -2
  252. package/src/renderers/svg.js +1 -617
  253. package/src/renderers/svgSerialization.js +643 -0
  254. package/src/renderers/transitionScene.js +67 -0
  255. package/src/scene.js +89 -0
  256. package/src/selectors/axes.js +1 -1
  257. package/src/selectors/datasets.js +1 -0
  258. package/src/selectors/grids.js +9 -0
  259. package/src/selectors/markClip.js +28 -0
  260. package/src/selectors/raincloud.js +7 -0
  261. package/src/version.js +1 -1
  262. package/types/basic.d.ts +7 -0
  263. package/types/index.d.ts +8 -0
  264. package/types/program.d.ts +286 -70
  265. package/types/scene.d.ts +35 -0
@@ -0,0 +1,643 @@
1
+ import { collectTransitionGraphics } from "./transitionScene.js";
2
+ import { isOwned } from "../core/immutable.js";
3
+ import { validateGraphicClip } from "../grammar/schemas/graphicClip.js";
4
+ import {
5
+ requireSingleOrderedGraphicByType,
6
+ walkGraphicTreeEvents
7
+ } from "../grammar/schemas/graphicTree.js";
8
+ import {
9
+ validateConcreteGraphicProperties,
10
+ validateConcreteGraphicValue
11
+ } from "../grammar/schemas/concreteGraphic.js";
12
+ import { resolvePathCommandBounds } from
13
+ "../grammar/schemas/graphicBounds.js";
14
+ import {
15
+ isLinearGradientPaint,
16
+ resolveLinearGradientCoordinates,
17
+ validateFillPaint
18
+ } from "../grammar/paint.js";
19
+ import {
20
+ requireFiniteProperty,
21
+ requireStringProperty
22
+ } from "./canvas/validation.js";
23
+ import { normalizeRendererFontWeight } from "./text.js";
24
+ import { validateRendererOptions } from "./options.js";
25
+
26
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
27
+ const SVG_OPTIONS = new Set(["title", "description", "resourceNamespace"]);
28
+ const RESOURCE_NAMESPACE = /^[A-Za-z][A-Za-z0-9_-]*$/;
29
+ const TEXT_ANCHORS = Object.freeze({
30
+ left: "start",
31
+ start: "start",
32
+ center: "middle",
33
+ right: "end",
34
+ end: "end"
35
+ });
36
+ const DOMINANT_BASELINES = Object.freeze({
37
+ top: "text-before-edge",
38
+ hanging: "hanging",
39
+ middle: "middle",
40
+ alphabetic: "alphabetic",
41
+ ideographic: "ideographic",
42
+ bottom: "text-after-edge"
43
+ });
44
+
45
+ const namespaceByGraphic = new WeakMap();
46
+
47
+ function resourceNamespace(graphicSpec) {
48
+ const cached = namespaceByGraphic.get(graphicSpec);
49
+ if (cached !== undefined) return cached;
50
+ const serialized = JSON.stringify(graphicSpec);
51
+ let hash = 0xcbf29ce484222325n;
52
+ for (let index = 0; index < serialized.length; index += 1) {
53
+ hash ^= BigInt(serialized.charCodeAt(index));
54
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
55
+ }
56
+ const namespace = hash.toString(36);
57
+ if (isOwned(graphicSpec)) namespaceByGraphic.set(graphicSpec, namespace);
58
+ return namespace;
59
+ }
60
+
61
+ function escapeText(value) {
62
+ for (const character of value) {
63
+ const point = character.codePointAt(0);
64
+ if (!(
65
+ point === 9 || point === 10 || point === 13 ||
66
+ (point >= 0x20 && point <= 0xd7ff) ||
67
+ (point >= 0xe000 && point <= 0xfffd) ||
68
+ (point >= 0x10000 && point <= 0x10ffff)
69
+ )) {
70
+ throw new TypeError("SVG output requires XML 1.0-compatible strings.");
71
+ }
72
+ }
73
+ return value
74
+ .replaceAll("&", "&amp;")
75
+ .replaceAll("<", "&lt;")
76
+ .replaceAll(">", "&gt;");
77
+ }
78
+
79
+ function escapeAttribute(value) {
80
+ return escapeText(String(value))
81
+ .replaceAll("\"", "&quot;")
82
+ .replaceAll("'", "&apos;");
83
+ }
84
+
85
+ function formatNumber(value) {
86
+ if (!Number.isFinite(value)) {
87
+ throw new TypeError("SVG renderer requires finite numeric output.");
88
+ }
89
+ return Object.is(value, -0) ? "0" : String(value);
90
+ }
91
+
92
+ function attributes(entries) {
93
+ return entries
94
+ .filter(([, value]) => value !== undefined)
95
+ .map(([name, value]) => `${name}="${escapeAttribute(value)}"`)
96
+ .join(" ");
97
+ }
98
+
99
+ function element(name, entries, content) {
100
+ const serialized = attributes(entries);
101
+ const opening = serialized.length === 0 ? `<${name}` : `<${name} ${serialized}`;
102
+ if (content === undefined) return `${opening}/>`;
103
+ return `${opening}>${content}</${name}>`;
104
+ }
105
+
106
+ function requireOpacity(properties, graphicId) {
107
+ const opacity = properties.opacity ?? 1;
108
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
109
+ throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
110
+ }
111
+ return opacity;
112
+ }
113
+
114
+ function requireStrokeDash(properties, graphicId) {
115
+ const strokeDash = properties.strokeDash ?? [];
116
+ if (
117
+ !Array.isArray(strokeDash) ||
118
+ !strokeDash.every(value => Number.isFinite(value) && value >= 0)
119
+ ) {
120
+ throw new Error(
121
+ `Graphic "${graphicId}" requires a non-negative finite strokeDash array.`
122
+ );
123
+ }
124
+ return strokeDash;
125
+ }
126
+
127
+ function strokeDetailAttributes(properties, strokeWidth) {
128
+ if (typeof properties.stroke !== "string" || !(strokeWidth > 0)) return [];
129
+ return [
130
+ ["stroke-linecap", Object.hasOwn(properties, "lineCap")
131
+ ? properties.lineCap
132
+ : undefined],
133
+ ["stroke-linejoin", Object.hasOwn(properties, "lineJoin")
134
+ ? properties.lineJoin
135
+ : undefined],
136
+ ["stroke-miterlimit", Object.hasOwn(properties, "miterLimit")
137
+ ? formatNumber(properties.miterLimit)
138
+ : undefined]
139
+ ];
140
+ }
141
+
142
+ function requireCanvasGeometry(id, canvas, { nested }) {
143
+ const properties = canvas.properties ?? {};
144
+ const width = requireFiniteProperty(properties, "width", id);
145
+ const height = requireFiniteProperty(properties, "height", id);
146
+ if (width < 0 || height < 0) {
147
+ const label = nested ? `Nested Canvas "${id}"` : "Canvas";
148
+ throw new Error(`${label} width and height must not be negative.`);
149
+ }
150
+ return {
151
+ ...(nested ? {
152
+ x: requireFiniteProperty(properties, "x", id),
153
+ y: requireFiniteProperty(properties, "y", id)
154
+ } : {}),
155
+ width,
156
+ height
157
+ };
158
+ }
159
+
160
+ function requireCanvasBackground(id, canvas) {
161
+ const background = canvas.properties?.background;
162
+ if (background === undefined) return undefined;
163
+ if (typeof background !== "string") {
164
+ throw new Error(
165
+ `Graphic "${id}" requires a string background property.`
166
+ );
167
+ }
168
+ return background;
169
+ }
170
+
171
+ function addLinearGradient(state, fill, bounds, graphicId) {
172
+ const coordinates = resolveLinearGradientCoordinates(fill, bounds);
173
+ const id =
174
+ `ggaction-gradient-${state.resourceNamespace}-${state.gradientCount}`;
175
+ state.gradientCount += 1;
176
+ const stops = fill.stops.map(stop => element("stop", [
177
+ ["offset", formatNumber(stop.offset)],
178
+ ["stop-color", stop.color]
179
+ ])).join("");
180
+ state.definitions.push(element("linearGradient", [
181
+ ["id", id],
182
+ ["gradientUnits", "userSpaceOnUse"],
183
+ ["x1", formatNumber(coordinates.from.x)],
184
+ ["y1", formatNumber(coordinates.from.y)],
185
+ ["x2", formatNumber(coordinates.to.x)],
186
+ ["y2", formatNumber(coordinates.to.y)]
187
+ ], stops));
188
+ return `url(#${id})`;
189
+ }
190
+
191
+ function serializeFill(state, fill, bounds, graphicId) {
192
+ validateFillPaint(fill, `${graphicId}.fill`);
193
+ if (!isLinearGradientPaint(fill)) return fill;
194
+ return addLinearGradient(state, fill, bounds, graphicId);
195
+ }
196
+
197
+ function serializeCircle(state, graphicId, properties) {
198
+ validateConcreteGraphicProperties("circle", properties);
199
+ const x = requireFiniteProperty(properties, "x", graphicId);
200
+ const y = requireFiniteProperty(properties, "y", graphicId);
201
+ const radius = requireFiniteProperty(properties, "radius", graphicId);
202
+ if (radius < 0) {
203
+ throw new Error(`Graphic "${graphicId}" requires a non-negative radius.`);
204
+ }
205
+ if (typeof properties.fill !== "string") {
206
+ throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
207
+ }
208
+ let strokeWidth;
209
+ if (properties.stroke !== undefined) {
210
+ if (typeof properties.stroke !== "string") {
211
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
212
+ }
213
+ strokeWidth = requireFiniteProperty(properties, "strokeWidth", graphicId);
214
+ if (strokeWidth < 0) {
215
+ throw new Error(
216
+ `Graphic "${graphicId}" requires a non-negative strokeWidth.`
217
+ );
218
+ }
219
+ }
220
+ return element("circle", [
221
+ ["cx", formatNumber(x)],
222
+ ["cy", formatNumber(y)],
223
+ ["r", formatNumber(radius)],
224
+ ["fill", properties.fill],
225
+ ["stroke", properties.stroke],
226
+ ["stroke-width", strokeWidth === undefined
227
+ ? undefined
228
+ : formatNumber(strokeWidth)],
229
+ ...strokeDetailAttributes(properties, strokeWidth),
230
+ ["opacity", formatNumber(requireOpacity(properties, graphicId))]
231
+ ]);
232
+ }
233
+
234
+ function serializeRect(state, graphicId, properties) {
235
+ validateConcreteGraphicProperties("rect", properties);
236
+ const x = requireFiniteProperty(properties, "x", graphicId);
237
+ const y = requireFiniteProperty(properties, "y", graphicId);
238
+ const width = requireFiniteProperty(properties, "width", graphicId);
239
+ const height = requireFiniteProperty(properties, "height", graphicId);
240
+ const strokeWidth = requireFiniteProperty(
241
+ properties,
242
+ "strokeWidth",
243
+ graphicId
244
+ );
245
+ if (width < 0 || height < 0 || strokeWidth < 0) {
246
+ throw new Error(
247
+ `Graphic "${graphicId}" requires non-negative rect dimensions and strokeWidth.`
248
+ );
249
+ }
250
+ if (properties.fill === undefined) {
251
+ throw new Error(`Graphic "${graphicId}" requires a fill property.`);
252
+ }
253
+ if (typeof properties.stroke !== "string") {
254
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
255
+ }
256
+ const fill = serializeFill(state, properties.fill, {
257
+ left: x,
258
+ right: x + width,
259
+ top: y,
260
+ bottom: y + height
261
+ }, graphicId);
262
+ return element("rect", [
263
+ ["x", formatNumber(x)],
264
+ ["y", formatNumber(y)],
265
+ ["width", formatNumber(width)],
266
+ ["height", formatNumber(height)],
267
+ ["fill", fill],
268
+ ["stroke", properties.stroke],
269
+ ["stroke-width", formatNumber(strokeWidth)],
270
+ ...strokeDetailAttributes(properties, strokeWidth),
271
+ ["opacity", formatNumber(requireOpacity(properties, graphicId))],
272
+ ["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
273
+ ]);
274
+ }
275
+
276
+ function serializeLine(state, graphicId, properties) {
277
+ validateConcreteGraphicProperties("line", properties);
278
+ const strokeWidth = requireFiniteProperty(
279
+ properties,
280
+ "strokeWidth",
281
+ graphicId
282
+ );
283
+ if (strokeWidth < 0) {
284
+ throw new Error(
285
+ `Graphic "${graphicId}" requires a non-negative strokeWidth.`
286
+ );
287
+ }
288
+ if (typeof properties.stroke !== "string") {
289
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
290
+ }
291
+ const strokeDash = requireStrokeDash(properties, graphicId);
292
+ return element("line", [
293
+ ["x1", formatNumber(requireFiniteProperty(properties, "x1", graphicId))],
294
+ ["y1", formatNumber(requireFiniteProperty(properties, "y1", graphicId))],
295
+ ["x2", formatNumber(requireFiniteProperty(properties, "x2", graphicId))],
296
+ ["y2", formatNumber(requireFiniteProperty(properties, "y2", graphicId))],
297
+ ["fill", "none"],
298
+ ["stroke", properties.stroke],
299
+ ["stroke-width", formatNumber(strokeWidth)],
300
+ ...strokeDetailAttributes(properties, strokeWidth),
301
+ ["stroke-dasharray", strokeDash.length === 0
302
+ ? undefined
303
+ : strokeDash.map(formatNumber).join(" ")],
304
+ ["opacity", formatNumber(requireOpacity(properties, graphicId))]
305
+ ]);
306
+ }
307
+
308
+ function serializePathData(commands) {
309
+ return commands.map(command => {
310
+ if (command.op === "M" || command.op === "L") {
311
+ return `${command.op}${formatNumber(command.x)} ${formatNumber(command.y)}`;
312
+ }
313
+ if (command.op === "C") {
314
+ return `C${formatNumber(command.x1)} ${formatNumber(command.y1)} ` +
315
+ `${formatNumber(command.x2)} ${formatNumber(command.y2)} ` +
316
+ `${formatNumber(command.x)} ${formatNumber(command.y)}`;
317
+ }
318
+ return "Z";
319
+ }).join(" ");
320
+ }
321
+
322
+ function serializePath(state, graphicId, properties) {
323
+ validateConcreteGraphicProperties("path", properties);
324
+ const commands = properties.commands;
325
+ if (!Array.isArray(commands)) {
326
+ throw new Error(
327
+ `Graphic "${graphicId}" requires concrete path commands.`
328
+ );
329
+ }
330
+ const hasFill = properties.fill !== undefined;
331
+ const hasStroke = properties.stroke !== undefined;
332
+ if (!hasFill && !hasStroke) {
333
+ throw new Error(`Graphic "${graphicId}" requires a fill or stroke property.`);
334
+ }
335
+ if (hasFill && commands.at(-1).op !== "Z") {
336
+ throw new Error(`Graphic "${graphicId}" requires a final Z command when filled.`);
337
+ }
338
+ if (hasStroke && typeof properties.stroke !== "string") {
339
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
340
+ }
341
+ const strokeWidth = hasStroke
342
+ ? requireFiniteProperty(properties, "strokeWidth", graphicId)
343
+ : undefined;
344
+ if (hasStroke && strokeWidth < 0) {
345
+ throw new Error(
346
+ `Graphic "${graphicId}" requires a non-negative strokeWidth.`
347
+ );
348
+ }
349
+ const strokeDash = requireStrokeDash(properties, graphicId);
350
+ let fill = "none";
351
+ if (hasFill) {
352
+ const bounds = resolvePathCommandBounds(commands);
353
+ if (bounds === undefined) {
354
+ throw new Error(`Graphic "${graphicId}" requires finite path fill bounds.`);
355
+ }
356
+ fill = serializeFill(state, properties.fill, bounds, graphicId);
357
+ }
358
+ return element("path", [
359
+ ["d", serializePathData(commands)],
360
+ ["fill", fill],
361
+ ["stroke", properties.stroke],
362
+ ["stroke-width", strokeWidth === undefined
363
+ ? undefined
364
+ : formatNumber(strokeWidth)],
365
+ ...strokeDetailAttributes(properties, strokeWidth),
366
+ ["stroke-dasharray", strokeDash.length === 0
367
+ ? undefined
368
+ : strokeDash.map(formatNumber).join(" ")],
369
+ ["opacity", formatNumber(requireOpacity(properties, graphicId))],
370
+ ["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
371
+ ]);
372
+ }
373
+
374
+ function serializeText(state, graphicId, properties) {
375
+ validateConcreteGraphicProperties("text", properties);
376
+ const x = requireFiniteProperty(properties, "x", graphicId);
377
+ const y = requireFiniteProperty(properties, "y", graphicId);
378
+ const fontSize = requireFiniteProperty(properties, "fontSize", graphicId);
379
+ const rotation = properties.rotation ?? 0;
380
+ const opacity = requireOpacity(properties, graphicId);
381
+ const text = properties.text;
382
+ validateConcreteGraphicValue("text", "text", text);
383
+ const fill = requireStringProperty(properties, "fill", graphicId);
384
+ const fontFamily = requireStringProperty(
385
+ properties,
386
+ "fontFamily",
387
+ graphicId
388
+ );
389
+ const textAlign = requireStringProperty(
390
+ properties,
391
+ "textAlign",
392
+ graphicId
393
+ );
394
+ const textBaseline = requireStringProperty(
395
+ properties,
396
+ "textBaseline",
397
+ graphicId
398
+ );
399
+ const fontWeight = properties.fontWeight ?? "normal";
400
+ if (fontSize <= 0) {
401
+ throw new Error(`Graphic "${graphicId}" requires a positive fontSize.`);
402
+ }
403
+ if (!Number.isFinite(rotation)) {
404
+ throw new Error(`Graphic "${graphicId}" requires a finite rotation.`);
405
+ }
406
+ if (
407
+ !(
408
+ (typeof fontWeight === "string" && fontWeight.length > 0) ||
409
+ Number.isFinite(fontWeight)
410
+ )
411
+ ) {
412
+ throw new Error(`Graphic "${graphicId}" requires a valid fontWeight.`);
413
+ }
414
+ if (TEXT_ANCHORS[textAlign] === undefined) {
415
+ throw new Error(`Graphic "${graphicId}" has invalid textAlign.`);
416
+ }
417
+ if (DOMINANT_BASELINES[textBaseline] === undefined) {
418
+ throw new Error(`Graphic "${graphicId}" has invalid textBaseline.`);
419
+ }
420
+ const rotationDegrees = rotation * 180 / Math.PI;
421
+ return element("text", [
422
+ ["x", formatNumber(x)],
423
+ ["y", formatNumber(y)],
424
+ ["fill", fill],
425
+ ["font-family", fontFamily],
426
+ ["font-size", formatNumber(fontSize)],
427
+ ["font-weight", normalizeRendererFontWeight(fontWeight)],
428
+ ["font-style", properties.fontStyle],
429
+ ["text-anchor", TEXT_ANCHORS[textAlign]],
430
+ ["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
431
+ ["opacity", formatNumber(opacity)],
432
+ ["transform", rotation === 0
433
+ ? undefined
434
+ : `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
435
+ ], properties.lines === undefined ? escapeText(text) : properties.lines.map(line =>
436
+ element("tspan", [["x", formatNumber(x + line.x)], ["y", formatNumber(y + line.y)]], escapeText(line.text))
437
+ ).join(""));
438
+ }
439
+
440
+ function serializeConcreteGraphic(state, id, graphic) {
441
+ if (graphic.type === "collection") {
442
+ const items = (graphic.items ?? []).map(item =>
443
+ serializeConcreteGraphic(state, item.id ?? id, item)
444
+ ).join("");
445
+ return element("g", [], items);
446
+ }
447
+ const properties = graphic.properties ?? {};
448
+ if (graphic.type === "circle") {
449
+ return serializeCircle(state, id, properties);
450
+ }
451
+ if (graphic.type === "rect") {
452
+ return serializeRect(state, id, properties);
453
+ }
454
+ if (graphic.type === "line") {
455
+ return serializeLine(state, id, properties);
456
+ }
457
+ if (graphic.type === "path") {
458
+ return serializePath(state, id, properties);
459
+ }
460
+ if (graphic.type === "text") {
461
+ return serializeText(state, id, properties);
462
+ }
463
+ throw new Error(`SVG renderer does not support "${graphic.type}" yet.`);
464
+ }
465
+
466
+ function openNestedCanvas(state, id, canvas) {
467
+ const { x, y, width, height } = requireCanvasGeometry(
468
+ id,
469
+ canvas,
470
+ { nested: true }
471
+ );
472
+ const clipId = `ggaction-clip-${state.resourceNamespace}-${state.clipCount}`;
473
+ state.clipCount += 1;
474
+ state.definitions.push(element("clipPath", [["id", clipId]], element(
475
+ "rect",
476
+ [
477
+ ["x", "0"],
478
+ ["y", "0"],
479
+ ["width", formatNumber(width)],
480
+ ["height", formatNumber(height)]
481
+ ]
482
+ )));
483
+ const background = requireCanvasBackground(id, canvas);
484
+ const backgroundRect = background === undefined
485
+ ? ""
486
+ : element("rect", [
487
+ ["x", "0"],
488
+ ["y", "0"],
489
+ ["width", formatNumber(width)],
490
+ ["height", formatNumber(height)],
491
+ ["fill", background]
492
+ ]);
493
+ return `<g ${attributes([
494
+ ["transform", `translate(${formatNumber(x)} ${formatNumber(y)})`],
495
+ ["clip-path", `url(#${clipId})`]
496
+ ])}>${backgroundRect}`;
497
+ }
498
+
499
+ function serializeBody(graphicSpec, rootId, rootCanvas, width, height, state) {
500
+ const output = [];
501
+ const background = requireCanvasBackground(rootId, rootCanvas);
502
+ if (background !== undefined) {
503
+ output.push(element("rect", [
504
+ ["x", "0"],
505
+ ["y", "0"],
506
+ ["width", formatNumber(width)],
507
+ ["height", formatNumber(height)],
508
+ ["fill", background]
509
+ ]));
510
+ }
511
+ walkGraphicTreeEvents(graphicSpec, {
512
+ enter({ id, object }) {
513
+ if (id === rootId) return;
514
+ if (object.clip !== undefined) {
515
+ validateGraphicClip(object.clip, object.type);
516
+ const clipId = `ggaction-clip-${state.resourceNamespace}-${state.clipCount++}`;
517
+ state.definitions.push(element("clipPath", [["id", clipId]], element("rect",
518
+ Object.entries(object.clip).map(([key, value]) => [key, formatNumber(value)]))));
519
+ output.push(`<g clip-path="url(#${clipId})">`);
520
+ }
521
+ if (object.type === "canvas") {
522
+ output.push(openNestedCanvas(state, id, object));
523
+ return;
524
+ }
525
+ if (object.type === "collection") {
526
+ output.push("<g>");
527
+ return;
528
+ }
529
+ if (!Array.isArray(object.items)) {
530
+ output.push(serializeConcreteGraphic(state, id, object));
531
+ }
532
+ },
533
+ item({ id, object, owner }) {
534
+ output.push(serializeConcreteGraphic(state, id, {
535
+ ...object,
536
+ type: object.type ?? owner.type
537
+ }));
538
+ },
539
+ exit({ id, object }) {
540
+ if (
541
+ id !== rootId &&
542
+ (object.type === "collection" || object.type === "canvas")
543
+ ) {
544
+ output.push("</g>");
545
+ }
546
+ if (object.clip !== undefined) output.push("</g>");
547
+ }
548
+ });
549
+ return output.join("");
550
+ }
551
+
552
+ function requireSVGOptions(options) {
553
+ validateRendererOptions(options, SVG_OPTIONS, "renderToSVG options");
554
+ for (const key of SVG_OPTIONS) {
555
+ if (
556
+ options[key] !== undefined &&
557
+ (typeof options[key] !== "string" || options[key].length === 0)
558
+ ) {
559
+ throw new TypeError(`renderToSVG ${key} must be a non-empty string.`);
560
+ }
561
+ }
562
+ if (
563
+ options.resourceNamespace !== undefined &&
564
+ !RESOURCE_NAMESPACE.test(options.resourceNamespace)
565
+ ) {
566
+ throw new TypeError(
567
+ "renderToSVG resourceNamespace must start with a letter and contain only letters, numbers, _ or -."
568
+ );
569
+ }
570
+ return options;
571
+ }
572
+
573
+ export function renderToSVG(program, options = {}) {
574
+ const resolvedOptions = requireSVGOptions(options);
575
+ const graphicSpec = program?.graphicSpec;
576
+ if (
577
+ graphicSpec === null ||
578
+ typeof graphicSpec !== "object" ||
579
+ graphicSpec.objects === null ||
580
+ typeof graphicSpec.objects !== "object" ||
581
+ !Array.isArray(graphicSpec.order)
582
+ ) {
583
+ throw new TypeError("renderToSVG requires a program with a graphicSpec.");
584
+ }
585
+ const { id: rootId, object: rootCanvas } =
586
+ requireSingleOrderedGraphicByType(graphicSpec, "canvas");
587
+ const { width, height } = requireCanvasGeometry(
588
+ rootId,
589
+ rootCanvas,
590
+ { nested: false }
591
+ );
592
+ const state = {
593
+ clipCount: 1,
594
+ definitions: [],
595
+ gradientCount: 1,
596
+ resourceNamespace:
597
+ resolvedOptions.resourceNamespace ?? resourceNamespace(graphicSpec)
598
+ };
599
+ walkGraphicTreeEvents(graphicSpec, {
600
+ enter({object}) {
601
+ if (object.clip !== undefined) validateGraphicClip(object.clip, object.type);
602
+ if (object.properties?.blend === "difference") state.hasBlend = true;
603
+ },
604
+ item({object}) { if (object.properties?.blend === "difference") state.hasBlend = true; }
605
+ });
606
+ const body = serializeBody(
607
+ graphicSpec,
608
+ rootId,
609
+ rootCanvas,
610
+ width,
611
+ height,
612
+ state
613
+ );
614
+ const accessible = [
615
+ resolvedOptions.title === undefined
616
+ ? ""
617
+ : element("title", [], escapeText(resolvedOptions.title)),
618
+ resolvedOptions.description === undefined
619
+ ? ""
620
+ : element("desc", [], escapeText(resolvedOptions.description))
621
+ ].join("");
622
+ const definitions = state.definitions.length === 0
623
+ ? ""
624
+ : element("defs", [], state.definitions.join(""));
625
+ return element("svg", [
626
+ ["xmlns", SVG_NAMESPACE],
627
+ ["width", formatNumber(width)],
628
+ ["height", formatNumber(height)],
629
+ ["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`],
630
+ ["style", state.hasBlend ? "isolation:isolate" : undefined]
631
+ ], `${accessible}${definitions}${body}`);
632
+ }
633
+
634
+ // Internal adapter: only concrete graphics and opaque correspondence keys enter here.
635
+ export function renderTransitionGraphics(graphicSpec, identities) {
636
+ const scene = collectTransitionGraphics(graphicSpec, identities);
637
+ const state = { definitions: [], gradientCount: 1, clipCount: 1,
638
+ resourceNamespace: `scene-${resourceNamespace(graphicSpec)}` };
639
+ const nodes = scene.nodes.map(({ properties, clips, ...node }) => ({ ...node,
640
+ svg: serializeConcreteGraphic(state, node.key, { type: node.type, properties }) }));
641
+ return { version: 1, width: scene.width, height: scene.height,
642
+ definitions: state.definitions.join(""), nodes };
643
+ }