ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  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/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,9 +1,9 @@
1
1
  import { ChartProgram } from "./ChartProgram.js";
2
2
  import { BasicChartProgram } from "./BasicChartProgram.js";
3
- import { cloneAndFreeze } from "./core/immutable.js";
3
+ import { cloneAndFreeze, freezeOwned } from "./core/immutable.js";
4
4
  import { hasRegisteredExtension, isBuiltinProgramClass } from "./core/extensionRegistry.js";
5
5
  import { packageVersion } from "./version.js";
6
- import { decodeValue, encodeValue, invalidSnapshot } from "./persistence/codec.js";
6
+ import { decodeValue, encodeValue, encodeSharedValue, invalidSnapshot } from "./persistence/codec.js";
7
7
  import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
8
8
  import {
9
9
  deriveTransformSchema,
@@ -25,7 +25,7 @@ function canonicalState(program, extensions, path = "payload") {
25
25
  }
26
26
 
27
27
  function envelope(kind, payload, extensions = []) {
28
- return JSON.stringify({ schemaVersion: kind === "editable" ? 2 : 1, kind, packageVersion, extensions, payload: encodeValue(payload) });
28
+ return JSON.stringify({ schemaVersion: kind === "editable" ? 3 : 1, kind, packageVersion, extensions, payload: kind === "editable" ? encodeSharedValue(payload) : encodeValue(payload) });
29
29
  }
30
30
 
31
31
  function readEnvelope(text, kind) {
@@ -33,7 +33,7 @@ function readEnvelope(text, kind) {
33
33
  let value;
34
34
  try { value = JSON.parse(text); } catch { throw invalidSnapshot("Snapshot must be valid JSON.", "snapshot"); }
35
35
  exactKeys(value, ["schemaVersion", "kind", "packageVersion", "extensions", "payload"], "snapshot");
36
- const versions = kind === "editable" ? [1, 2] : [1];
36
+ const versions = kind === "editable" ? [1, 2, 3] : [1];
37
37
  if (!versions.includes(value.schemaVersion) || value.kind !== kind) {
38
38
  throw invalidSnapshot(
39
39
  `Unsupported snapshot schema or kind; expected schema ${versions.join(" or ")} ${kind}.`,
@@ -45,6 +45,9 @@ function readEnvelope(text, kind) {
45
45
  value.extensions.some(name => typeof name !== "string" || !hasRegisteredExtension(name)) || (kind === "graphic" && value.extensions.length)) {
46
46
  throw invalidSnapshot("Snapshot requires unknown, duplicate, or inapplicable extensions.", "snapshot.extensions");
47
47
  }
48
+ if (value.schemaVersion < 3 && value.payload?.[0] === "shared") {
49
+ throw invalidSnapshot("Shared array payloads require editable snapshot version 3.", "snapshot.payload");
50
+ }
48
51
  return { ...value, payload: decodeValue(value.payload) };
49
52
  }
50
53
 
@@ -64,7 +67,7 @@ function restoreState(state, extensions, path = "payload", { requireSchemas = fa
64
67
  if (requireSchemas) {
65
68
  for (const dataset of program.semanticSpec.datasets) {
66
69
  if (dataset.schema === undefined) {
67
- throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2.`, `${path}.semanticSpec.datasets`);
70
+ throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2 or later.`, `${path}.semanticSpec.datasets`);
68
71
  }
69
72
  validateDatasetSchema(dataset.schema);
70
73
  }
@@ -93,7 +96,7 @@ function migrateStateSchemas(state) {
93
96
  sourceSchema,
94
97
  dataset.transform?.[0] ?? {},
95
98
  dataset.values,
96
- { sourceId: dataset.source, ownerId: dataset.id }
99
+ { sourceId: dataset.source, ownerId: dataset.id, sourceTransform: byId.get(dataset.source)?.transform?.[0] }
97
100
  );
98
101
  }
99
102
  active.delete(id);
@@ -118,6 +121,15 @@ export function serializeProgram(program) {
118
121
  return envelope("editable", canonicalState(program, extensions), [...extensions].sort());
119
122
  }
120
123
 
124
+ // Decoding creates fresh plain containers. Own them once across the whole graph
125
+ // so child constructors retain shared immutable data instead of copying each pool.
126
+ function ownDecodedState(value, seen = new WeakSet()) {
127
+ if (value === null || typeof value !== "object" || seen.has(value)) return;
128
+ seen.add(value);
129
+ for (const child of Object.values(value)) ownDecodedState(child, seen);
130
+ freezeOwned(value);
131
+ }
132
+
121
133
  export function deserializeProgram(text) {
122
134
  const stored = readEnvelope(text, "editable");
123
135
  const extensions = new Set();
@@ -128,6 +140,7 @@ export function deserializeProgram(text) {
128
140
  restoreState(payload, new Set());
129
141
  payload = migrateStateSchemas(payload);
130
142
  }
143
+ ownDecodedState(payload);
131
144
  const program = restoreState(payload, extensions, "payload", { requireSchemas: true });
132
145
  if (extensions.size !== stored.extensions.length || stored.extensions.some(name => !extensions.has(name))) {
133
146
  throw invalidSnapshot("Snapshot extensions must exactly match its action traces.", "snapshot.extensions");
@@ -1,3 +1,4 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { requireFiniteProperty } from "./validation.js";
2
3
  import { validateConcreteGraphicProperties } from
3
4
  "../../grammar/schemas/concreteGraphic.js";
@@ -25,7 +26,7 @@ function drawCircle(context, child, collectionId) {
25
26
  throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
26
27
  }
27
28
 
28
- context.fillStyle = properties.fill;
29
+ context.fillStyle = resolveCanvasSolidPaint(properties.fill);
29
30
  context.globalAlpha = opacity;
30
31
  context.beginPath();
31
32
  context.arc(x, y, radius, 0, Math.PI * 2);
@@ -44,8 +45,10 @@ function drawCircle(context, child, collectionId) {
44
45
  `Graphic "${graphicId}" requires a non-negative strokeWidth.`
45
46
  );
46
47
  }
47
- applyCanvasStroke(context, properties);
48
- context.stroke();
48
+ if (strokeWidth > 0) {
49
+ applyCanvasStroke(context, properties);
50
+ context.stroke();
51
+ }
49
52
  }
50
53
  }
51
54
 
@@ -4,10 +4,16 @@ import {
4
4
  } from "../../grammar/paint.js";
5
5
  import { resolveCanvasGradientCoordinates } from "./native.js";
6
6
 
7
+ // SVG's no-paint keyword is not a valid Canvas color assignment. Assign a real
8
+ // transparent color so the backend cannot retain a previous opaque paint.
9
+ export function resolveCanvasSolidPaint(paint) {
10
+ return paint === "none" ? "rgba(0,0,0,0)" : paint;
11
+ }
12
+
7
13
  export function applyCanvasFill(context, fill, bounds, graphicId) {
8
14
  validateFillPaint(fill, `${graphicId}.fill`);
9
15
  if (!isLinearGradientPaint(fill)) {
10
- context.fillStyle = fill;
16
+ context.fillStyle = resolveCanvasSolidPaint(fill);
11
17
  return fill;
12
18
  }
13
19
  if (typeof context.createLinearGradient !== "function") {
@@ -1,3 +1,6 @@
1
+ import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
2
+ import { prepareIsolatedGlyphs, drawIsolatedGlyph } from "./isolated.js";
3
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
4
  import { annotateError } from "../../core/diagnostics.js";
2
5
  import { drawCircleGraphic } from "./circle.js";
3
6
  import { drawLineGraphic } from "./line.js";
@@ -39,8 +42,6 @@ const DRAWING_CONTEXT_METHODS = Object.freeze([
39
42
  "rotate",
40
43
  "fillText"
41
44
  ]);
42
- const MAX_RASTER_DIMENSION = 32_767;
43
- const MAX_RASTER_PIXELS = 16_777_216;
44
45
 
45
46
  function requireDrawingContext(context) {
46
47
  if (context === null || typeof context !== "object") {
@@ -81,7 +82,7 @@ function fillCanvasBackground(context, id, canvas, width, height) {
81
82
  );
82
83
  }
83
84
  context.globalAlpha = 1;
84
- context.fillStyle = canvas.properties.background;
85
+ context.fillStyle = resolveCanvasSolidPaint(canvas.properties.background);
85
86
  context.fillRect(0, 0, width, height);
86
87
  }
87
88
 
@@ -143,9 +144,10 @@ export function resolveGraphicRenderTarget(graphicSpec) {
143
144
  });
144
145
  }
145
146
 
146
- export function drawResolvedGraphicSpec(target, context) {
147
+ export function drawResolvedGraphicSpec(target, context, options = {}) {
147
148
  requireDrawingContext(context);
148
149
  const { graphicSpec, canvasId, canvas, width, height } = target;
150
+ const isolated = options.isolated ?? prepareIsolatedGlyphs(target, context, options);
149
151
 
150
152
  fillCanvasBackground(context, canvasId, canvas, width, height);
151
153
 
@@ -161,14 +163,14 @@ export function drawResolvedGraphicSpec(target, context) {
161
163
  return;
162
164
  }
163
165
  if (!Array.isArray(object.items)) {
164
- drawConcreteGraphic(context, id, object);
166
+ drawConcreteGraphic(context, id, object, isolated);
165
167
  }
166
168
  },
167
169
  item({ id, object, owner }) {
168
170
  drawConcreteGraphic(context, id, {
169
171
  ...object,
170
172
  type: object.type ?? owner.type
171
- });
173
+ }, isolated);
172
174
  },
173
175
  exit({ id, object }) {
174
176
  if (
@@ -181,7 +183,7 @@ export function drawResolvedGraphicSpec(target, context) {
181
183
  });
182
184
  }
183
185
 
184
- export function render(program, context, { pixelRatio = 1 } = {}) {
186
+ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas) {
185
187
  const graphicSpec = requireProgramGraphicSpec(program);
186
188
 
187
189
  requireCanvasContext(context);
@@ -219,6 +221,7 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
219
221
  ), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
220
222
  }
221
223
  preflightCanvasGraphicSpec(target, pixelRatio);
224
+ const isolated = prepareIsolatedGlyphs(target, context, {pixelRatio, createCanvas});
222
225
 
223
226
  context.canvas.width = physicalWidth;
224
227
  context.canvas.height = physicalHeight;
@@ -234,16 +237,20 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
234
237
  try {
235
238
  context.scale(pixelRatio, pixelRatio);
236
239
  context.clearRect(0, 0, width, height);
237
- drawResolvedGraphicSpec(target, context);
240
+ drawResolvedGraphicSpec(target, context, {isolated, pixelRatio, createCanvas});
238
241
  } finally {
239
242
  context.restore();
240
243
  }
241
244
  }
242
245
 
243
- function drawConcreteGraphic(context, id, graphic) {
246
+ export function render(program, context, options) {
247
+ return renderCanvasProgram(program, context, options);
248
+ }
249
+
250
+ function drawConcreteGraphic(context, id, graphic, isolated) {
244
251
  if (graphic.type === "collection") {
245
252
  for (const item of graphic.items ?? []) {
246
- drawConcreteGraphic(context, item.id ?? id, item);
253
+ drawConcreteGraphic(context, item.id ?? id, item, isolated);
247
254
  }
248
255
  return;
249
256
  }
@@ -251,5 +258,9 @@ function drawConcreteGraphic(context, id, graphic) {
251
258
  if (draw === undefined) {
252
259
  throw new Error(`Canvas renderer does not support "${graphic.type}" yet.`);
253
260
  }
254
- draw(context, id, graphic);
261
+ if (["normal","difference"].includes(graphic.properties?.blend)) {
262
+ drawIsolatedGlyph(context, id, graphic, draw, isolated);
263
+ } else {
264
+ draw(context, id, graphic);
265
+ }
255
266
  }
@@ -0,0 +1,166 @@
1
+ import { resolvePrimitiveGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
2
+ import { walkGraphicTreeEvents } from "../../grammar/schemas/graphicTree.js";
3
+ import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
4
+
5
+ function canvasFactory(context, supplied) {
6
+ if (supplied !== undefined) return supplied;
7
+ const canvas = context.canvas;
8
+ if (typeof canvas?.ownerDocument?.createElement === "function") {
9
+ return (width, height) => {
10
+ const source = canvas.ownerDocument.createElement("canvas");
11
+ source.width = width;
12
+ source.height = height;
13
+ return source;
14
+ };
15
+ }
16
+ if (canvas?.constructor && canvas.constructor !== Object) {
17
+ return (width, height) => new canvas.constructor(width, height);
18
+ }
19
+ throw new TypeError("Isolated glyph painting requires a compatible Canvas surface.");
20
+ }
21
+
22
+ function requireSurface(surface) {
23
+ const context = surface?.getContext?.("2d");
24
+ if (!context || ["save", "restore", "scale", "translate", "beginPath", "fillRect",
25
+ "fill", "stroke", "moveTo", "lineTo", "bezierCurveTo", "closePath", "setLineDash"]
26
+ .some(method => typeof context[method] !== "function")) {
27
+ throw new TypeError("Isolated glyph painting requires a compatible Canvas 2D surface.");
28
+ }
29
+ return context;
30
+ }
31
+
32
+ // This is renderer-private state: no surface or backend object enters graphicSpec.
33
+ export function prepareIsolatedGlyphs(target, context, { pixelRatio = 1, createCanvas } = {}) {
34
+ const requested = Object.values(target.graphicSpec.objects).some(object =>
35
+ ["normal","difference"].includes(object.properties?.blend) ||
36
+ object.items?.some(item => ["normal","difference"].includes(item.properties?.blend)));
37
+ if (!requested) return undefined;
38
+ if (typeof context.drawImage !== "function" || typeof context.globalCompositeOperation !== "string") {
39
+ throw new TypeError("Isolated glyph painting requires Canvas drawImage and compositing support.");
40
+ }
41
+ const factory = canvasFactory(context, createCanvas);
42
+ let surface;
43
+ try {
44
+ surface = factory(1, 1);
45
+ const source = requireSurface(surface);
46
+ const hasGradient = Object.values(target.graphicSpec.objects).some(object =>
47
+ [object, ...(object.items ?? [])].some(item =>
48
+ ["normal","difference"].includes(item.properties?.blend) && item.properties.fill?.type === "linear-gradient"));
49
+ const normal=Object.values(target.graphicSpec.objects).some(object=>
50
+ [object,...(object.items??[])].some(item=>item.properties?.blend==="normal"));
51
+ if(normal){
52
+ if(typeof source.setTransform!=="function")throw new TypeError("Normal glyph opacity requires Canvas transform support.");
53
+ source.save();try{
54
+ source.globalCompositeOperation="destination-in";
55
+ if(source.globalCompositeOperation!=="destination-in")throw new TypeError("Normal glyph opacity requires source attenuation.");
56
+ }finally{source.restore();}
57
+ const circle=Object.values(target.graphicSpec.objects).some(object=>
58
+ [object,...(object.items??[])].some(item=>item.properties?.blend==="normal"&&(item.type??object.type)==="circle"));
59
+ if(circle&&typeof source.arc!=="function")throw new TypeError("Normal circle painting requires Canvas arcs.");
60
+ }
61
+ if (hasGradient && (typeof source.createLinearGradient !== "function" ||
62
+ typeof source.createLinearGradient(0, 0, 1, 0)?.addColorStop !== "function")) {
63
+ throw new TypeError("Isolated gradient painting requires Canvas linear gradients.");
64
+ }
65
+ } catch (error) {
66
+ throw new TypeError("Isolated glyph painting requires a compatible Canvas surface.", {cause:error});
67
+ }
68
+ context.save();
69
+ try {
70
+ const modes=new Set();
71
+ for(const object of Object.values(target.graphicSpec.objects))for(const item of [object,...(object.items??[])]){
72
+ if(item.properties?.blend!==undefined)modes.add(item.properties.blend==="normal"?"source-over":item.properties.blend);
73
+ }
74
+ for(const mode of modes){
75
+ context.globalCompositeOperation=mode;
76
+ if(context.globalCompositeOperation!==mode)throw new TypeError(`Canvas context does not support ${mode} compositing.`);
77
+ }
78
+ } finally { context.restore(); }
79
+
80
+ const plans = new Map();
81
+ // Ordered root graphics may be siblings of the Canvas, not its descendants.
82
+ const frames = [{offset:[0,0],clip:{left:0,top:0,right:target.width,bottom:target.height}}];
83
+ function plan(id, object, type) {
84
+ const properties = object.properties;
85
+ if (!["normal","difference"].includes(properties?.blend)) return;
86
+ const bounds = resolvePrimitiveGraphicBounds(type, properties);
87
+ if (!bounds) throw new Error(`Graphic "${id}" requires finite isolated paint bounds.`);
88
+ const {offset: [x, y], clip} = frames.at(-1);
89
+ const clipped = {
90
+ left: Math.max(bounds.left + x, clip.left),
91
+ right: Math.min(bounds.right + x, clip.right),
92
+ top: Math.max(bounds.top + y, clip.top),
93
+ bottom: Math.min(bounds.bottom + y, clip.bottom)
94
+ };
95
+ if (clipped.right <= clipped.left || clipped.bottom <= clipped.top) {
96
+ plans.set(id, {empty: true});
97
+ return;
98
+ }
99
+ // Align to destination pixels, including fractional nested translations.
100
+ const left = Math.max(Math.floor(clip.left * pixelRatio), Math.floor(clipped.left * pixelRatio) - 1);
101
+ const top = Math.max(Math.floor(clip.top * pixelRatio), Math.floor(clipped.top * pixelRatio) - 1);
102
+ const right = Math.min(Math.ceil(clip.right * pixelRatio), Math.ceil(clipped.right * pixelRatio) + 1);
103
+ const bottom = Math.min(Math.ceil(clip.bottom * pixelRatio), Math.ceil(clipped.bottom * pixelRatio) + 1);
104
+ const width = right - left, height = bottom - top;
105
+ if (![width, height].every(Number.isSafeInteger) || width < 1 || height < 1 ||
106
+ width > MAX_RASTER_DIMENSION || height > MAX_RASTER_DIMENSION ||
107
+ width * height > MAX_RASTER_PIXELS) {
108
+ throw new RangeError(`Graphic "${id}" isolated paint surface exceeds the raster allocation limit.`);
109
+ }
110
+ plans.set(id, {width, height, left: left / pixelRatio - x, top: top / pixelRatio - y});
111
+ }
112
+ walkGraphicTreeEvents(target.graphicSpec, {
113
+ enter({id, object}) {
114
+ if (object.type === "canvas") {
115
+ const parent = frames.at(-1);
116
+ const offset = id === target.canvasId ? [0, 0] :
117
+ [parent.offset[0] + object.properties.x, parent.offset[1] + object.properties.y];
118
+ const properties = object.properties;
119
+ const local = {left: offset[0], top: offset[1],
120
+ right: offset[0] + properties.width, bottom: offset[1] + properties.height};
121
+ const clip = {
122
+ left: Math.max(parent.clip.left, local.left), top: Math.max(parent.clip.top, local.top),
123
+ right: Math.min(parent.clip.right, local.right), bottom: Math.min(parent.clip.bottom, local.bottom)
124
+ };
125
+ frames.push({offset, clip});
126
+ }
127
+ if (!object.items) plan(id, object, object.type);
128
+ },
129
+ item({id, object, owner}) { plan(id, object, object.type ?? owner.type); },
130
+ exit({object}) { if (object.type === "canvas") frames.pop(); }
131
+ });
132
+ return {plans, surface, pixelRatio};
133
+ }
134
+
135
+ // Paint fill and outline together with opacity 1, then composite the source once.
136
+ export function drawIsolatedGlyph(context, id, graphic, draw, prepared) {
137
+ const plan = prepared?.plans.get(id);
138
+ if (!plan) throw new Error(`Graphic "${id}" has no prepared isolated paint source.`);
139
+ if (plan.empty) return;
140
+ const {surface, pixelRatio} = prepared;
141
+ surface.width = plan.width;
142
+ surface.height = plan.height;
143
+ const source = requireSurface(surface);
144
+ source.scale(pixelRatio, pixelRatio);
145
+ source.translate(-plan.left, -plan.top);
146
+ const {blend, opacity = 1, ...properties} = graphic.properties;
147
+ draw(source, id, {...graphic, properties: {...properties, opacity: 1}});
148
+ if(blend==="normal"){
149
+ // Attenuate the complete source once. drawImage(globalAlpha) introduces
150
+ // additional native image-alpha quantization; source masking avoids it.
151
+ source.save();
152
+ try{
153
+ source.setTransform(1,0,0,1,0,0);
154
+ source.globalCompositeOperation="destination-in";
155
+ source.globalAlpha=opacity;
156
+ source.fillStyle="#fff";
157
+ source.fillRect(0,0,plan.width,plan.height);
158
+ }finally{source.restore();}
159
+ }
160
+ context.save();
161
+ try {
162
+ context.globalAlpha = blend==="normal"?1:opacity;
163
+ context.globalCompositeOperation = blend==="normal"?"source-over":blend;
164
+ context.drawImage(surface, plan.left, plan.top, plan.width / pixelRatio, plan.height / pixelRatio);
165
+ } finally { context.restore(); }
166
+ }
@@ -0,0 +1,3 @@
1
+ // Shared raster allocation limits for root canvases and isolated glyph sources.
2
+ export const MAX_RASTER_DIMENSION = 32_767;
3
+ export const MAX_RASTER_PIXELS = 16_777_216;
@@ -33,6 +33,7 @@ function drawLine(context, child, collectionId) {
33
33
  throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
34
34
  }
35
35
 
36
+ if (strokeWidth === 0) return;
36
37
  context.globalAlpha = opacity;
37
38
  applyCanvasStroke(context, properties, properties.strokeDash ?? []);
38
39
  context.beginPath();
@@ -82,7 +82,7 @@ function drawPath(context, child, collectionId) {
82
82
  applyCanvasFill(context, properties.fill, bounds, graphicId);
83
83
  context.fill();
84
84
  }
85
- if (hasStroke) {
85
+ if (hasStroke && strokeWidth > 0) {
86
86
  applyCanvasStroke(context, properties, strokeDash);
87
87
  context.stroke();
88
88
  }
@@ -43,14 +43,17 @@ function drawRect(context, child, collectionId) {
43
43
  bottom: y + height
44
44
  }, graphicId);
45
45
  context.fillRect(x, y, width, height);
46
- applyCanvasStroke(context, properties);
47
- context.beginPath();
48
- context.moveTo(x, y);
49
- context.lineTo(x + width, y);
50
- context.lineTo(x + width, y + height);
51
- context.lineTo(x, y + height);
52
- context.lineTo(x, y);
53
- context.stroke();
46
+ // Canvas ignores lineWidth = 0 and would retain a previous nonzero width.
47
+ if (strokeWidth > 0) {
48
+ applyCanvasStroke(context, properties);
49
+ context.beginPath();
50
+ context.moveTo(x, y);
51
+ context.lineTo(x + width, y);
52
+ context.lineTo(x + width, y + height);
53
+ context.lineTo(x, y + height);
54
+ context.closePath();
55
+ context.stroke();
56
+ }
54
57
  }
55
58
 
56
59
  export function drawRectGraphic(context, id, graphic) {
@@ -1,8 +1,9 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { resolveStrokeDetails } from "../../grammar/strokeStyle.js";
2
3
 
3
4
  export function applyCanvasStroke(context, properties, strokeDash = []) {
4
5
  const details = resolveStrokeDetails(properties);
5
- context.strokeStyle = properties.stroke;
6
+ context.strokeStyle = resolveCanvasSolidPaint(properties.stroke);
6
7
  context.lineWidth = properties.strokeWidth;
7
8
  context.lineCap = details.lineCap;
8
9
  context.lineJoin = details.lineJoin;
@@ -1,3 +1,4 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { requireFiniteProperty, requireStringProperty } from "./validation.js";
2
3
  import { validateConcreteGraphicProperties } from
3
4
  "../../grammar/schemas/concreteGraphic.js";
@@ -72,15 +73,16 @@ function drawText(context, child, collectionId) {
72
73
  context.save();
73
74
 
74
75
  try {
75
- context.fillStyle = fill;
76
+ context.fillStyle = resolveCanvasSolidPaint(fill);
76
77
  context.globalAlpha = opacity;
77
78
  const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
78
- context.font = `${rendererFontWeight} ${fontSize}px ${fontFamily}`;
79
+ context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${fontFamily}`;
79
80
  context.textAlign = textAlign;
80
81
  context.textBaseline = textBaseline;
81
82
  context.translate(x, y);
82
83
  context.rotate(rotation);
83
- context.fillText(text, 0, 0);
84
+ if (properties.lines === undefined) context.fillText(text, 0, 0);
85
+ else for (const line of properties.lines) context.fillText(line.text, line.x, line.y);
84
86
  } finally {
85
87
  context.restore();
86
88
  }
@@ -73,10 +73,10 @@ export async function renderToPDFBuffer(program, options = {}) {
73
73
  const target = resolveGraphicRenderTarget(graphicSpec);
74
74
  requirePDFPageDimensions(target);
75
75
  preflightCanvasGraphicSpec(target);
76
- const { PDFDocument } = await loadNativeBackend();
76
+ const { PDFDocument, createCanvas } = await loadNativeBackend();
77
77
  const document = new PDFDocument(metadata);
78
78
  const context = document.beginPage(target.width, target.height);
79
- drawResolvedGraphicSpec(target, context);
79
+ drawResolvedGraphicSpec(target, context, {createCanvas});
80
80
  document.endPage();
81
81
  const buffer = document.close();
82
82
  return Object.freeze({
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
- import { render } from "./canvas/index.js";
3
+ import { renderCanvasProgram } from "./canvas/index.js";
4
4
  import { validateRendererOptions } from "./options.js";
5
5
  import { loadNativeBackend } from "./nativeBackend.js";
6
6
 
@@ -12,7 +12,7 @@ export async function renderToPNGBuffer(program, options = {}) {
12
12
  const { pixelRatio = 1 } = options;
13
13
  const { createCanvas } = await loadNativeBackend();
14
14
  const canvas = createCanvas(1, 1);
15
- render(program, canvas.getContext("2d"), { pixelRatio });
15
+ renderCanvasProgram(program, canvas.getContext("2d"), { pixelRatio }, createCanvas);
16
16
  // Canvas drawing is synchronous; encoding uses the native async worker.
17
17
  const buffer = await canvas.encode("png");
18
18
  return Object.freeze({
@@ -265,7 +265,8 @@ function serializeRect(state, graphicId, properties) {
265
265
  ["stroke", properties.stroke],
266
266
  ["stroke-width", formatNumber(strokeWidth)],
267
267
  ...strokeDetailAttributes(properties, strokeWidth),
268
- ["opacity", formatNumber(requireOpacity(properties, graphicId))]
268
+ ["opacity", formatNumber(requireOpacity(properties, graphicId))],
269
+ ["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
269
270
  ]);
270
271
  }
271
272
 
@@ -362,7 +363,8 @@ function serializePath(state, graphicId, properties) {
362
363
  ["stroke-dasharray", strokeDash.length === 0
363
364
  ? undefined
364
365
  : strokeDash.map(formatNumber).join(" ")],
365
- ["opacity", formatNumber(requireOpacity(properties, graphicId))]
366
+ ["opacity", formatNumber(requireOpacity(properties, graphicId))],
367
+ ["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
366
368
  ]);
367
369
  }
368
370
 
@@ -419,13 +421,16 @@ function serializeText(state, graphicId, properties) {
419
421
  ["font-family", fontFamily],
420
422
  ["font-size", formatNumber(fontSize)],
421
423
  ["font-weight", normalizeRendererFontWeight(fontWeight)],
424
+ ["font-style", properties.fontStyle],
422
425
  ["text-anchor", TEXT_ANCHORS[textAlign]],
423
426
  ["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
424
427
  ["opacity", formatNumber(opacity)],
425
428
  ["transform", rotation === 0
426
429
  ? undefined
427
430
  : `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
428
- ], escapeText(text));
431
+ ], properties.lines === undefined ? escapeText(text) : properties.lines.map(line =>
432
+ element("tspan", [["x", formatNumber(x + line.x)], ["y", formatNumber(y + line.y)]], escapeText(line.text))
433
+ ).join(""));
429
434
  }
430
435
 
431
436
  function serializeConcreteGraphic(state, id, graphic) {
@@ -579,6 +584,10 @@ export function renderToSVG(program, options = {}) {
579
584
  resourceNamespace:
580
585
  resolvedOptions.resourceNamespace ?? resourceNamespace(graphicSpec)
581
586
  };
587
+ walkGraphicTreeEvents(graphicSpec, {
588
+ enter({object}) { if (object.properties?.blend === "difference") state.hasBlend = true; },
589
+ item({object}) { if (object.properties?.blend === "difference") state.hasBlend = true; }
590
+ });
582
591
  const body = serializeBody(
583
592
  graphicSpec,
584
593
  rootId,
@@ -602,6 +611,7 @@ export function renderToSVG(program, options = {}) {
602
611
  ["xmlns", SVG_NAMESPACE],
603
612
  ["width", formatNumber(width)],
604
613
  ["height", formatNumber(height)],
605
- ["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`]
614
+ ["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`],
615
+ ["style", state.hasBlend ? "isolation:isolate" : undefined]
606
616
  ], `${accessible}${definitions}${body}`);
607
617
  }
@@ -0,0 +1 @@
1
+ const p=Object.freeze(["x","y"]),w=Object.freeze(["line","ticks","labels","title"]);function x(n){const e=n.semanticSpec?.guides?.axis??{},a=(n.guideConfigs??n.materializationConfigs?.guides)?.axis??{};return[...new Set([...Object.keys(e),...Object.keys(a)])].flatMap(i=>{const s=Object.hasOwn(e,i)?e[i]:void 0,c=Object.hasOwn(a,i)?a[i]:void 0,d=p.includes(i)?i:s?.channel;if(!p.includes(d))return[];const t=[...new Set((n.semanticSpec.layers??[]).filter(f=>f.encoding?.[d]?.scale===s?.scale).map(f=>f.coordinate).filter(Boolean))];return[{id:i,channel:d,semantic:s,config:c,scale:s?.scale,coordinate:s?.coordinate??(t.length===1?t[0]:void 0),named:i!==d,sides:[...new Set(i!==d?[s.position]:w.map(f=>c?.[f]?.position).filter(Boolean))]}]})}function g(n,e){return n===e?{}:{target:n}}function O(n,e,a={},{create:i=!1,whole:s=!1}={}){const c=x(n),d=c.filter(r=>r.channel===e);let t;if(Object.hasOwn(a,"target")){const r=a.target;if(t=c.find(l=>l.id===r),t===void 0||t.channel!==e)throw new Error(`Unknown ${e}-axis target "${r}".`)}else if(t=d.find(r=>r.id===e),t===void 0){if(d.length>1)throw new Error(`Multiple ${e}-axes require an explicit target.`);t=d[0]}const{target:f,...o}=a,u=t?.id??e;if(!t?.named&&o.position!==void 0&&c.some(r=>r.named)){const r=[...new Set((n.semanticSpec.layers??[]).filter(l=>l.encoding?.[e]?.scale===(o.scale??t?.scale??e)).map(l=>l.coordinate).filter(Boolean))];m(n,{id:u,channel:e,coordinate:t?.coordinate??(r.length===1?r[0]:void 0),position:o.position})}if(t?.named){if(o.scale!==void 0&&o.scale!==t.scale||o.coordinate!==void 0&&o.coordinate!==t.coordinate)throw new Error(`Axis "${u}" resource binding does not match the requested filter.`);if(!s&&o.position!==void 0&&o.position!==t.semantic.position)throw new Error(`Named axis "${u}" components share one side; edit the complete axis position.`);i&&(o.scale??=t.scale),s||(o.position??=t.semantic.position)}return{id:u,owner:t,options:o}}function m(n,{id:e,channel:a,coordinate:i,position:s}){for(const c of x(n))if(c.id!==e&&c.coordinate===i&&c.sides.includes(s))throw new Error(`Axis side "${s}" is already occupied on coordinate "${i}".`)}function b(n,e=n){const a=["Line","Ticks","Labels","Title"];if(e!==n)return a.map(s=>`axis_${e}__${s}`);const i=n==="radius"?"radial":n;return a.map(s=>`${i}Axis${s}`)}export{m as assertCartesianAxisSideAvailable,b as axisGraphicIds,g as axisTargetArgs,x as cartesianAxisOwners,O as selectCartesianAxis};
@@ -0,0 +1,38 @@
1
+ import { annotateError } from "../core/diagnostics.js";
2
+ import { findLayer } from "./layers.js";
3
+ import { findDataset } from "./datasets.js";
4
+
5
+ export function namespaceGraphicId(namespace, id) {
6
+ const encode = value => Array.from(value, character =>
7
+ character.codePointAt(0).toString(16).padStart(6, "0")
8
+ ).join("");
9
+ return `g${encode(namespace)}_${id}`;
10
+ }
11
+
12
+ function pointOwners(program, id) {
13
+ const layer = findLayer(program, id);
14
+ const dataset = layer?.mark?.type === "point" ? findDataset(program, layer.data) : undefined;
15
+ const owners = Array.isArray(dataset?.values) ? [{ layer, dataset }] : [];
16
+ for (const childId of program.compositionSpec?.children ?? []) {
17
+ const prefix = namespaceGraphicId(`${program.compositionSpec.id}-${childId}`, "");
18
+ const child = program.children[childId];
19
+ if (!id.startsWith(prefix) || child === undefined) continue;
20
+ const localId = id.slice(prefix.length);
21
+ if (child.graphicSpec.objects[localId] === undefined) continue;
22
+ owners.push(...pointOwners(child, localId));
23
+ }
24
+ return owners;
25
+ }
26
+
27
+ export function resolveGraphicItemCapacity(program, { id, label, type }) {
28
+ const owners = pointOwners(program, id);
29
+ if (owners.length > 1) throw annotateError(new Error("Point graphic ownership is ambiguous."), {
30
+ code: "ambiguous-resource", resourceId: id, candidates: owners.map(owner => owner.layer.id)
31
+ });
32
+ return {
33
+ pointOwned: owners.length === 1,
34
+ rawCount: owners[0]?.dataset.values.length,
35
+ graphicType: type ?? program.graphicSpec.objects[id]?.type,
36
+ label
37
+ };
38
+ }
@@ -37,3 +37,22 @@ export function resolveEligibleLayer(program, {
37
37
  if (candidates.length === 0) throw annotateError(new Error(`${label} requires an eligible layer.`), { code: "missing-resource", optionPath: targetOption });
38
38
  throw annotateError(new Error(`${label} ${targetOption} is ambiguous; provide ${targetOption}.`), { code: "ambiguous-resource", optionPath: targetOption, candidates: candidates.map(layer => layer.id) });
39
39
  }
40
+
41
+ // Configured composite plots retain their established owner errors while sharing
42
+ // the explicit/current/unique resolution order.
43
+ export function resolveConfiguredOwner(program, requested, {
44
+ config, operation, kind, article = "a", materialized = false, missing, ambiguous
45
+ }, validateUserId) {
46
+ const eligible = program.semanticSpec.layers.filter(layer => {
47
+ const value = program.markConfigs[layer.id]?.[config];
48
+ return materialized ? value?.materialized === true : value !== undefined;
49
+ });
50
+ const id = requested === undefined ? program.context.currentMark
51
+ : validateUserId(requested, `${kind[0].toUpperCase() + kind.slice(1)}-plot owner id`);
52
+ const owner = eligible.find(layer => layer.id === id);
53
+ if (owner !== undefined) return owner;
54
+ if (requested !== undefined) throw new Error(`Unknown ${kind}-plot owner "${id}".`);
55
+ if (eligible.length === 1) return eligible[0];
56
+ if (eligible.length === 0) throw new Error(missing ?? `${operation} requires ${article} ${kind} plot.`);
57
+ throw new Error(ambiguous ?? `${operation} target is ambiguous; provide target.`);
58
+ }