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,44 @@
1
+ import { cartesianAxisOwners, axisGraphicIds, PARALLEL_AXIS_GRAPHICS } from "../selectors/axes.js";
2
+ import { gridNames } from "../selectors/grids.js";
3
+ import { resolvePlotDrawOrder } from "../layout/plotDrawOrder.js";
4
+
5
+ export function hasPlotUnderlays(program) {
6
+ return program.compositionSpec === undefined && (
7
+ program.materializationConfigs.canvas?.plotBorder?.underlay === true ||
8
+ cartesianAxisOwners(program).some(owner => owner.config?.frame?.underlay === true)
9
+ );
10
+ }
11
+
12
+ export function plotDrawOrderResources(program) {
13
+ const axes = cartesianAxisOwners(program).map(owner => ({
14
+ ids: axisGraphicIds(owner.channel, owner.id), underlay: owner.config?.frame?.underlay === true
15
+ }));
16
+ for (const channel of ["theta", "radius"]) if (program.semanticSpec.guides.axis?.[channel] !== undefined) {
17
+ axes.push({ ids: axisGraphicIds(channel), underlay: false });
18
+ }
19
+ if (program.semanticSpec.guides.axis?.parallel !== undefined) axes.push({ ids: Object.values(PARALLEL_AXIS_GRAPHICS), underlay: false });
20
+ return {
21
+ children: program.graphicSpec.objects["plot-main"]?.children ?? [],
22
+ grids: ["horizontal", "vertical"].flatMap((direction, index) => {
23
+ const config = program.guideConfigs.grid?.[direction];
24
+ return config === undefined ? [] : [{ id: gridNames(direction).graphic, zIndex: config.zIndex, index }];
25
+ }),
26
+ border: program.materializationConfigs.canvas?.plotBorder === undefined ? undefined : {
27
+ id: "plotBorder", underlay: program.materializationConfigs.canvas.plotBorder.underlay === true
28
+ }, axes
29
+ };
30
+ }
31
+
32
+ export function resolveProgramPlotDrawOrder(program) {
33
+ return resolvePlotDrawOrder(plotDrawOrderResources(program));
34
+ }
35
+
36
+ export function firstOverlayGuide(program) {
37
+ const { children, grids, border, axes } = plotDrawOrderResources(program);
38
+ const overlay = new Set([
39
+ ...grids.filter(grid => grid.zIndex > 0).map(grid => grid.id),
40
+ ...(border?.underlay === false ? [border.id] : []),
41
+ ...axes.filter(axis => !axis.underlay).flatMap(axis => axis.ids)
42
+ ]);
43
+ return children.find(id => overlay.has(id));
44
+ }
@@ -27,7 +27,7 @@ function requiredChannels(layer, mode) {
27
27
  return [
28
28
  ...position,
29
29
  ...(layer.encoding?.color ? ["color"] : []),
30
- ...(layer.encoding?.stroke ? ["stroke"] : [])
30
+ ...["stroke", "opacity", "strokeWidth"].filter(channel => layer.encoding?.[channel])
31
31
  ];
32
32
  }
33
33
 
@@ -96,6 +96,10 @@ export function resolveRectRows(program, layer, dataset) {
96
96
  const stroke = layer.encoding?.stroke === undefined
97
97
  ? undefined
98
98
  : mappedEncoding(program, layer, dataset, "stroke").values;
99
+ const opacity = layer.encoding?.opacity === undefined ? undefined
100
+ : mappedEncoding(program, layer, dataset, "opacity").values;
101
+ const strokeWidth = layer.encoding?.strokeWidth === undefined ? undefined
102
+ : mappedEncoding(program, layer, dataset, "strokeWidth").values;
99
103
  const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
100
104
 
101
105
  const rows = rectUsesFields(layer) ? dataset.values : [{}];
@@ -105,7 +109,9 @@ export function resolveRectRows(program, layer, dataset) {
105
109
  (x !== undefined && !Number.isFinite(x.values[index])) ||
106
110
  (y !== undefined && !Number.isFinite(y.values[index])) ||
107
111
  typeof fill !== "string" ||
108
- (stroke !== undefined && typeof stroke[index] !== "string")
112
+ (stroke !== undefined && typeof stroke[index] !== "string") ||
113
+ (opacity !== undefined && !Number.isFinite(opacity[index])) ||
114
+ (strokeWidth !== undefined && !Number.isFinite(strokeWidth[index]))
109
115
  ) return [];
110
116
 
111
117
  let geometry;
@@ -138,7 +144,9 @@ export function resolveRectRows(program, layer, dataset) {
138
144
  return [{
139
145
  row,
140
146
  sourceIndex: index,
141
- properties: { ...geometry, ...appearance(config, fill, stroke?.[index]) }
147
+ properties: { ...geometry, ...appearance(config, fill, stroke?.[index]),
148
+ ...(opacity === undefined ? {} : { opacity: opacity[index] }),
149
+ ...(strokeWidth === undefined ? {} : { strokeWidth: strokeWidth[index] }) }
142
150
  }];
143
151
  });
144
152
  }
@@ -16,14 +16,22 @@ const GRIDS = Object.freeze([
16
16
  Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
17
17
  ]);
18
18
 
19
- export function planAxisTickGridRematerialization(program, channel, axisId = channel) {
19
+ export function planAxisTickGridRematerialization(program, channel, axisId = channel, { syncLabels = false } = {}) {
20
20
  const ticks = program.guideConfigs.axis?.[axisId]?.ticks;
21
21
  const role = channel === "x" ? "vertical" : "horizontal";
22
22
  const config = program.guideConfigs.grid?.[role];
23
- return buildMaterializationPlan({ guides:
24
- config?.inferredValues === true && config.scale === ticks?.scale
25
- ? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : []
26
- });
23
+ const labels = syncLabels && program.guideConfigs.axis?.[axisId]?.labels
24
+ ? [{ op: `edit${channel.toUpperCase()}AxisLabels`, args: {
25
+ ...axisTargetArgs(axisId, channel),
26
+ ...(ticks.mode === "values" ? { values: ticks.values } : {
27
+ count: ticks.count,
28
+ ...(ticks.rounding === undefined ? {} : { rounding: ticks.rounding })
29
+ })
30
+ } }] : [];
31
+ return buildMaterializationPlan({ guides: [...labels,
32
+ ...(config?.inferredValues === true && config.scale === ticks?.scale
33
+ ? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : [])
34
+ ] });
27
35
  }
28
36
 
29
37
  function usesPositionalScale(program, id) {
@@ -102,7 +102,7 @@ function collectionBounds(program, items) {
102
102
  const bounds = unionConcreteGraphicBounds(
103
103
  program.graphicSpec,
104
104
  items.map(item => item.id)
105
- , program.materializationConfigs.textMetrics);
105
+ , program.materializationConfigs.textMetrics, {includeOwnerClips: false});
106
106
  if (bounds === undefined) return {};
107
107
  const { left, top, right, bottom } = bounds;
108
108
  return { x: left, y: top, width: right - left, height: bottom - top };
@@ -60,6 +60,7 @@ export function resolveLineItems(program, layer, dataset) {
60
60
  dataset.values,
61
61
  layer,
62
62
  {
63
+ scales: program.semanticSpec.scales,
63
64
  xDomain: program.resolvedScales[x?.scale]?.domain,
64
65
  ...(x?.bin === undefined ? {} : {
65
66
  xBinBoundaries: resolveLineBins(
@@ -8,6 +8,7 @@ import { pointSelectionPolicy } from "./point.js";
8
8
  import { ruleSelectionPolicy } from "./rule.js";
9
9
  import { tickSelectionPolicy } from "./tick.js";
10
10
  import { rectSelectionPolicy } from "./rect.js";
11
+ import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
11
12
 
12
13
  const POLICIES = Object.freeze({
13
14
  arc: arcSelectionPolicy,
@@ -42,5 +43,5 @@ export function resolveMarkItems(program, target, grain = "item") {
42
43
  if (!policy.supportedGrains.includes(grain)) {
43
44
  throw new Error(`Mark "${target}" does not support ${grain} selection grain.`);
44
45
  }
45
- return policy.resolveItems(program, layer, dataset, grain);
46
+ return policy.resolveItems(program, layer, applyItemMissingPolicy(layer, dataset), grain);
46
47
  }
@@ -5,11 +5,10 @@ import {
5
5
  resolveMarkLabelPlacement
6
6
  } from "../layout/labels.js";
7
7
  import { resolveMarkLabelValues } from "../grammar/markLabels.js";
8
- import { formatTextValue, resolveTextLines } from "../grammar/text.js";
8
+ import { formatTextValue, resolveTextLines, resolveTextOffsetDataset, textOffsetValue } from "../grammar/text.js";
9
9
  import { normalizePositionDatum } from "../grammar/positionDatum.js";
10
10
  import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
11
11
  import { findDataset } from "../selectors/datasets.js";
12
- import { applyItemMissingPolicy } from "../grammar/itemMissing.js";
13
12
  import { findCoordinate } from "../selectors/coordinates.js";
14
13
  import { findLayer } from "../selectors/layers.js";
15
14
  import { unionConcreteGraphicBounds } from
@@ -427,8 +426,9 @@ function resolveRowTextItems(program, layer, config) {
427
426
  if (sourceDataset === undefined) {
428
427
  throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
429
428
  }
430
- const dataset = applyItemMissingPolicy(layer, sourceDataset);
431
- const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
429
+ const dataset = resolveTextOffsetDataset(layer, sourceDataset);
430
+ const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color,
431
+ layer.encoding?.dx, layer.encoding?.dy];
432
432
  const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
433
433
  ? dataset.values.length
434
434
  : 1;
@@ -465,7 +465,12 @@ function resolveRowTextItems(program, layer, config) {
465
465
  const fills = resolveRowEncodingValues(program, layer, dataset, "color");
466
466
  return rows.flatMap((row, index) => {
467
467
  const concrete = concreteItem(
468
- fills === undefined ? config : { ...config, fill: fills[index] },
468
+ {
469
+ ...config,
470
+ ...(fills === undefined ? {} : { fill: fills[index] }),
471
+ dx: textOffsetValue(layer.encoding?.dx, row, config.dx),
472
+ dy: textOffsetValue(layer.encoding?.dy, row, config.dy)
473
+ },
469
474
  { x: x[index], y: y[index] },
470
475
  contentValue(layer.encoding.text, row),
471
476
  layer.encoding.text.format
package/src/mcp/cli.js CHANGED
File without changes
@@ -1,4 +1,15 @@
1
+ import { validateFacetAxisPolicy } from "../core/facetAxisPolicy.js";
2
+ import { resolveFacetFamily } from "../grammar/facets/dependencies.js";
3
+ import { validateRegressionPlacement } from "../grammar/regression/parameters.js";
4
+ import { validateGraphicClip } from "../grammar/schemas/graphicClip.js";
5
+ import { validatePointShape } from "../grammar/pointShapes.js";
6
+ import {deriveLookupRows,sameLookupValue} from "../grammar/lookup.js";
7
+ import {deriveTransformSchema,datasetSchemaContext} from "../grammar/datasetSchema.js";
8
+ import {orderedDatasetAncestors} from "../grammar/datasetDependencies.js";
9
+ import {validateLegendSpacing} from "../grammar/legendSpacing.js";
10
+ import { requestedMarkClip, validateMarkClipCoordinate } from "../selectors/markClip.js";
1
11
  import { validateAuthoredFacetLegend } from "../grammar/facets/legendIntent.js";
12
+ import { validateFacetHeaderRotations } from "../grammar/facetHeaders.js";
2
13
  import { AXIS_RESOURCE_CHANNELS } from "../core/vocabulary.js";
3
14
  import { cartesianAxisOwners, assertCartesianAxisSideAvailable } from "../selectors/axes.js";
4
15
  import { axisGraphicIds } from "../selectors/axes.js";
@@ -20,16 +31,19 @@ import { validateGraphicItemLimit } from "../core/validation.js";
20
31
  import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
21
32
  import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
22
33
  import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
34
+ import { validateTickSelectionRounding, axisTickSelection } from "../grammar/ticks.js";
23
35
  import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
24
36
  import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
25
37
  import { validateRegressionFitStatistics, requireRegressionStatisticsSource, regressionStatisticsRows } from "../grammar/regression/statistics.js";
26
38
  import { resolveGraphicItemCapacity } from "../selectors/graphicCapacity.js";
27
39
  import { isContinuousColorScaleType } from "../grammar/scales/types.js";
40
+ import { validateTickColorChannels } from "../grammar/scales/colorConsumers.js";
28
41
  import { normalizeColorInterpolationOptions } from "../grammar/scales/color.js";
29
42
  import { normalizeTransformParameters } from "../grammar/scales/transformed.js";
30
43
  import { validateLegendGroupState } from "../grammar/legendGroups.js";
31
44
  import { normalizeGradientDirection, validateLegendGradient } from "../grammar/legendGradient.js";
32
45
  import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
46
+ import { resolveTextOffsetDataset } from "../grammar/text.js";
33
47
  import { isPlainObject } from "../core/immutable.js";
34
48
  import { validateUserId } from "../core/identifiers.js";
35
49
  import { collectAllResourceReferences } from "../core/resourceReferences.js";
@@ -48,6 +62,21 @@ export const STATE_KEYS = Object.freeze([
48
62
  "children", "compositionSpec", "context", "trace", "actionStack"
49
63
  ]);
50
64
 
65
+ function validateStoredColorLegendSampling(program, config, path) {
66
+ requireObject(config.sampling, path);
67
+ const sample = config.sampling;
68
+ exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
69
+ if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
70
+ if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
71
+ validateGeneratedItemLimit(sample.count, "Color legend sample count");
72
+ const scale = program.resolvedScales[config.scale];
73
+ if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
74
+ if (sample.mode === "values") {
75
+ requireArray(sample.values, path);
76
+ if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
77
+ }
78
+ }
79
+
51
80
  export function requireObject(value, path) {
52
81
  if (!isPlainObject(value)) throw invalidSnapshot(`${path} must be a plain object.`, path);
53
82
  }
@@ -79,9 +108,10 @@ export function validateGraphic(graphic, path = "payload.graphicSpec") {
79
108
  if (!id.length) throw invalidSnapshot("Graphic IDs must be non-empty.", path);
80
109
  requireObject(object, `${path}.objects.${id}`);
81
110
  validateGraphicType(object.type);
82
- if (Object.keys(object).some(key => !["type", "properties", "items", "children"].includes(key))) {
111
+ if (Object.keys(object).some(key => !["type", "properties", "items", "children", "clip"].includes(key))) {
83
112
  throw invalidSnapshot(`Unknown graphic member at ${id}.`, path);
84
113
  }
114
+ if (object.clip !== undefined) validateGraphicClip(object.clip, object.type);
85
115
  if (object.properties !== undefined) validateConcreteGraphicProperties(object.type, object.properties);
86
116
  if (object.items !== undefined) {
87
117
  if (object.type === "canvas" || object.properties !== undefined) throw invalidSnapshot(`Invalid item owner "${id}".`, path);
@@ -143,7 +173,7 @@ function validateSemanticProperties(program, value, property) {
143
173
  validateStoredSemanticValue(program, parsed, value);
144
174
  }
145
175
 
146
- export function validateProgramState(program, path) {
176
+ export function validateProgramState(program, path, { allowMissingLookupSchemas = false } = {}) {
147
177
  const semantic = program.semanticSpec;
148
178
  exactKeys(semantic, ["datasets", "layers", "scales", "coordinates", "guides", "title"], `${path}.semanticSpec`);
149
179
  const resources = new Map();
@@ -161,6 +191,12 @@ export function validateProgramState(program, path) {
161
191
  }
162
192
  resources.set(referenceKind, entries);
163
193
  for (const resource of entries.values()) {
194
+ if (kind === "layer") validateTickColorChannels(resource);
195
+ if (kind === "layer" && ["dx", "dy"].some(channel => resource.encoding?.[channel] !== undefined)) {
196
+ const dataset = semantic.datasets.find(item => item.id === resource.data);
197
+ if (dataset === undefined) throw new Error("Numeric text offsets require an existing dataset.");
198
+ resolveTextOffsetDataset(resource, dataset);
199
+ }
164
200
  if (kind === "scale") {
165
201
  normalizeColorInterpolationOptions(resource.interpolate, resource);
166
202
  if (resource.type === "pow") normalizeTransformParameters("pow", {
@@ -181,6 +217,15 @@ export function validateProgramState(program, path) {
181
217
  if (program.compositionSpec?.type === "facet" && isPlainObject(program.compositionSpec.facet.guides.legend)) {
182
218
  validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
183
219
  }
220
+ if (program.compositionSpec?.type === "facet") {
221
+ validateFacetAxisPolicy(program.compositionSpec.facet.guides.axes,
222
+ resolveFacetFamily(program.semanticSpec).family, program.compositionSpec.facet.repeat !== undefined);
223
+ const headers = program.materializationConfigs.facets?.[program.compositionSpec.id]?.headers;
224
+ if (headers !== undefined) validateFacetHeaderRotations(headers);
225
+ if (headers?.row?.title !== undefined && program.compositionSpec.facet.grid === undefined) {
226
+ throw new TypeError("Facet row field title requires a row-column facet grid.");
227
+ }
228
+ }
184
229
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
185
230
  for (const config of [
186
231
  ...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
@@ -189,17 +234,54 @@ export function validateProgramState(program, path) {
189
234
  if (config?.strokeDash !== undefined) validateGuideDash(config.strokeDash);
190
235
  if (config?.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
191
236
  }
192
- for (const owner of cartesianAxisOwners(program)) {
237
+ const cartesianOwners = cartesianAxisOwners(program);
238
+ for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
239
+ for (const config of [axis?.ticks, axis?.labels]) {
240
+ if (config === undefined || !Object.hasOwn(config, "rounding")) continue;
241
+ if (!cartesianOwners.some(owner => owner.id === id)) {
242
+ throw invalidSnapshot("Tick rounding requires a Cartesian axis owner.", path);
243
+ }
244
+ validateTickSelectionRounding(config, program.resolvedScales[config.scale]?.type);
245
+ }
246
+ }
247
+ for (const owner of cartesianOwners) {
193
248
  const channel = owner.id;
194
249
  const axis = owner.config;
250
+ if (axis?.ticks && axis?.labels &&
251
+ (Object.hasOwn(axis.ticks, "rounding") || Object.hasOwn(axis.labels, "rounding")) &&
252
+ JSON.stringify(axisTickSelection(axis.ticks)) !== JSON.stringify(axisTickSelection(axis.labels))) {
253
+ throw invalidSnapshot("Stored tick rounding conflicts with axis labels.", path);
254
+ }
195
255
  if (axis?.frame !== undefined) {
196
- exactKeys(axis.frame, ["offset"], `${path}.guides.axis.${channel}.frame`);
197
- validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
256
+ validateOptionObject(axis.frame, ["offset", "underlay"], `Stored axis "${channel}" frame`, { allowEmpty: false });
257
+ if (Object.hasOwn(axis.frame, "offset")) validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
258
+ if (Object.hasOwn(axis.frame, "underlay") && axis.frame.underlay !== true) {
259
+ throw new Error("Stored axis underlay must be canonical true intent.");
260
+ }
198
261
  }
199
262
  if (axis?.labels !== undefined) validateAxisTextPlacement(axis.labels);
200
263
  if (axis?.title !== undefined) validateAxisTextPlacement(axis.title, { title: true });
201
264
  }
265
+ for (const config of Object.values(program.guideConfigs.grid ?? {})) {
266
+ if (!Object.hasOwn(config, "rounding")) continue;
267
+ validateTickSelectionRounding(config, program.resolvedScales[config.scale]?.type);
268
+ const owners = cartesianOwners.map(owner => owner.config?.ticks)
269
+ .filter(ticks => ticks?.scale === config.scale);
270
+ if (config.inferredValues !== true || config.rounding !== "nearest" || owners.length === 0 ||
271
+ owners.some(ticks => JSON.stringify(axisTickSelection(ticks)) !== JSON.stringify(axisTickSelection(config)))) {
272
+ throw invalidSnapshot("Stored grid rounding requires its matching inferred tick policy.", path);
273
+ }
274
+ }
275
+ const border = program.materializationConfigs.canvas?.plotBorder;
276
+ if (isPlainObject(border) && Object.hasOwn(border, "underlay") && border.underlay !== true) {
277
+ throw new Error("Stored plot border underlay must be canonical true intent.");
278
+ }
202
279
  for (const id of Object.keys(program.guideConfigs.axis ?? {})) {
280
+ const config = program.guideConfigs.axis[id];
281
+ if (config?.frame !== undefined && Object.hasOwn(config.frame, "underlay") &&
282
+ !cartesianOwners.some(owner => owner.id === id)) {
283
+ throw new Error("Stored axis underlay requires a Cartesian axis owner.");
284
+ }
203
285
  if (!AXIS_RESOURCE_CHANNELS.includes(id) && !Object.hasOwn(program.semanticSpec.guides.axis ?? {}, id)) {
204
286
  throw invalidSnapshot("Named axis configs require their semantic owner.", path);
205
287
  }
@@ -242,6 +324,7 @@ export function validateProgramState(program, path) {
242
324
  if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
243
325
  for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
244
326
  validateLegendClip(kind,config);
327
+ validateLegendSpacing(kind,config);
245
328
  if (localLegendPosition(config) !== undefined) {
246
329
  const descriptor = describeLegendBlock(program, kind, config);
247
330
  for (const [key, override] of Object.entries(config.blockOverrides)) {
@@ -301,22 +384,17 @@ export function validateProgramState(program, path) {
301
384
  if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
302
385
  validateLegendGradient(config.gradient, "Stored legend gradient");
303
386
  }
387
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols" && config.sampling !== undefined) {
388
+ validateStoredColorLegendSampling(program, config, path);
389
+ if (config.sampling.mode !== "values" || config.count !== config.sampling.count) {
390
+ throw invalidSnapshot("Gradient exact sampling must retain its automatic count.", path);
391
+ }
392
+ }
304
393
  if (config.representation === undefined) continue;
305
394
  if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
306
395
  if (config.representation === "symbols") {
307
396
  if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
308
- requireObject(config.sampling, path);
309
- const sample = config.sampling;
310
- exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
311
- if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
312
- if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
313
- validateGeneratedItemLimit(sample.count, "Color legend sample count");
314
- const scale = program.resolvedScales[config.scale];
315
- if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
316
- if (sample.mode === "values") {
317
- requireArray(sample.values, path);
318
- if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
319
- }
397
+ validateStoredColorLegendSampling(program, config, path);
320
398
  requireObject(config.symbol, path);
321
399
  validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
322
400
  if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
@@ -326,7 +404,7 @@ export function validateProgramState(program, path) {
326
404
  if (!["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"].includes(config.position) || !["horizontal", "vertical"].includes(config.direction) || config.layout !== "edge" || !["left", "center", "right"].includes(config.align) || !["top", "left"].includes(config.titlePosition) || config.columns !== undefined && (!Number.isInteger(config.columns) || config.columns < 1)) throw invalidSnapshot("Invalid sampled color item layout.", path);
327
405
  validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
328
406
  validateNonNegativeFinite(config.offset, "Stored sampled color offset");
329
- } else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
407
+ } else if (["symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
330
408
  }
331
409
 
332
410
  const regressionFits = program.materializationConfigs.regressionFits ?? {};
@@ -336,6 +414,18 @@ export function validateProgramState(program, path) {
336
414
  validateRegressionFitStatistics(cache, dataset, resources.get("data").get(dataset?.source));
337
415
  }
338
416
  for (const dataset of semantic.datasets) {
417
+ if(dataset.transform?.[0]?.type==="lookup"){
418
+ orderedDatasetAncestors(semantic.datasets,dataset.id);
419
+ const transform=dataset.transform[0],source=resources.get("data").get(dataset.source),right=resources.get("data").get(transform.lookup.data);
420
+ if(source===undefined||right===undefined)throw invalidSnapshot("Lookup requires both existing input datasets.",path);
421
+ const missingSchemas = [source, right, dataset].some(input => input.schema === undefined);
422
+ if (!allowMissingLookupSchemas || !missingSchemas) {
423
+ const schema=deriveTransformSchema(source.schema,transform,dataset.values,
424
+ datasetSchemaContext(source,transform,dataset.id,id=>resources.get("data").get(id)));
425
+ if(!sameLookupValue(dataset.schema,schema))throw invalidSnapshot("Lookup schema does not match both inputs.",path);
426
+ }
427
+ if(dataset.values!==undefined&&!sameLookupValue(dataset.values,deriveLookupRows(source.values,right.values,transform)))throw invalidSnapshot("Lookup values do not match both input datasets.",path);
428
+ }
339
429
  if (dataset.transform?.[0]?.type !== "regressionStatistics") continue;
340
430
  const fit = resources.get("data").get(dataset.source);
341
431
  requireRegressionStatisticsSource(fit);
@@ -378,7 +468,13 @@ export function validateProgramState(program, path) {
378
468
  }
379
469
  }
380
470
  for (const id of Object.keys(program.markConfigs)) {
471
+ validateRegressionPlacement(program.markConfigs[id].regression?.placement);
472
+ requestedMarkClip(program.markConfigs[id]);
473
+ validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
381
474
  if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
475
+ if (resources.get("mark").get(id).mark.type === "point" && program.markConfigs[id].shape !== undefined) {
476
+ validatePointShape(program.markConfigs[id].shape);
477
+ }
382
478
  if (resources.get("mark").get(id).mark.type === "area") {
383
479
  validateAreaPaintConfig(program.markConfigs[id]);
384
480
  }
@@ -7,6 +7,7 @@ import { decodeValue, encodeValue, encodeSharedValue, invalidSnapshot } from "./
7
7
  import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
8
8
  import {
9
9
  deriveTransformSchema,
10
+ datasetSchemaContext,
10
11
  inferDatasetSchema,
11
12
  validateDatasetSchema
12
13
  } from "./grammar/datasetSchema.js";
@@ -63,7 +64,7 @@ function restoreState(state, extensions, path = "payload", { requireSchemas = fa
63
64
  { requireSchemas }
64
65
  )]));
65
66
  const program = new ChartProgram({ ...state, children });
66
- validateProgramState(program, path);
67
+ validateProgramState(program, path, { allowMissingLookupSchemas: !requireSchemas });
67
68
  if (requireSchemas) {
68
69
  for (const dataset of program.semanticSpec.datasets) {
69
70
  if (dataset.schema === undefined) {
@@ -96,7 +97,8 @@ function migrateStateSchemas(state) {
96
97
  sourceSchema,
97
98
  dataset.transform?.[0] ?? {},
98
99
  dataset.values,
99
- { sourceId: dataset.source, ownerId: dataset.id, sourceTransform: byId.get(dataset.source)?.transform?.[0] }
100
+ datasetSchemaContext({...byId.get(dataset.source),schema:sourceSchema},dataset.transform?.[0]??{},dataset.id,
101
+ input=>({...byId.get(input),schema:schemaFor(input)}))
100
102
  );
101
103
  }
102
104
  active.delete(id);
@@ -1,4 +1,5 @@
1
1
  import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
2
+ import { validateGraphicClip } from "../../grammar/schemas/graphicClip.js";
2
3
  import { prepareIsolatedGlyphs, drawIsolatedGlyph } from "./isolated.js";
3
4
  import { resolveCanvasSolidPaint } from "./fill.js";
4
5
  import { annotateError } from "../../core/diagnostics.js";
@@ -158,6 +159,16 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
158
159
  enterNestedCanvas(context, id, object);
159
160
  return;
160
161
  }
162
+ if (object.clip !== undefined) {
163
+ validateGraphicClip(object.clip, object.type);
164
+ requireContextMethod(context, "rect", `Graphic "${id}" clip`);
165
+ requireContextMethod(context, "clip", `Graphic "${id}" clip`);
166
+ context.save();
167
+ context.beginPath();
168
+ const {x, y, width, height} = object.clip;
169
+ context.rect(x, y, width, height);
170
+ context.clip();
171
+ }
161
172
  if (object.type === "collection") {
162
173
  context.save();
163
174
  return;
@@ -179,6 +190,7 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
179
190
  ) {
180
191
  context.restore();
181
192
  }
193
+ if (object.clip !== undefined) context.restore();
182
194
  }
183
195
  });
184
196
  }
@@ -221,6 +233,10 @@ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, c
221
233
  ), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
222
234
  }
223
235
  preflightCanvasGraphicSpec(target, pixelRatio);
236
+ if (Object.values(graphicSpec.objects).some(object => object.clip !== undefined)) {
237
+ requireContextMethod(context, "rect", "Graphic clip");
238
+ requireContextMethod(context, "clip", "Graphic clip");
239
+ }
224
240
  const isolated = prepareIsolatedGlyphs(target, context, {pixelRatio, createCanvas});
225
241
 
226
242
  context.canvas.width = physicalWidth;
@@ -1,4 +1,5 @@
1
1
  import { resolvePrimitiveGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
2
+ import { graphicClipBounds, intersectClipBounds, validateGraphicClip } from "../../grammar/schemas/graphicClip.js";
2
3
  import { walkGraphicTreeEvents } from "../../grammar/schemas/graphicTree.js";
3
4
  import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
4
5
 
@@ -124,10 +125,19 @@ export function prepareIsolatedGlyphs(target, context, { pixelRatio = 1, createC
124
125
  };
125
126
  frames.push({offset, clip});
126
127
  }
128
+ if (object.clip !== undefined) {
129
+ const parent = frames.at(-1);
130
+ validateGraphicClip(object.clip, object.type);
131
+ frames.push({offset: parent.offset, clip: intersectClipBounds(parent.clip,
132
+ graphicClipBounds(object.clip, ...parent.offset))});
133
+ }
127
134
  if (!object.items) plan(id, object, object.type);
128
135
  },
129
136
  item({id, object, owner}) { plan(id, object, object.type ?? owner.type); },
130
- exit({object}) { if (object.type === "canvas") frames.pop(); }
137
+ exit({object}) {
138
+ if (object.clip !== undefined) frames.pop();
139
+ if (object.type === "canvas") frames.pop();
140
+ }
131
141
  });
132
142
  return {plans, surface, pixelRatio};
133
143
  }
@@ -1,4 +1,5 @@
1
1
  import { resolveLinearGradientCoordinates } from "../../grammar/paint.js";
2
+ import { validateGraphicClip } from "../../grammar/schemas/graphicClip.js";
2
3
  import { resolvePathCommandBounds } from
3
4
  "../../grammar/schemas/graphicBounds.js";
4
5
  import { walkGraphicTreeEvents } from
@@ -143,6 +144,12 @@ export function preflightCanvasGraphicSpec(target, scale = 1) {
143
144
  offset = id === target.canvasId ? undefined : parent;
144
145
  }
145
146
  preflightGraphic(id, object, scale, object.type, offset);
147
+ if (object.clip !== undefined) {
148
+ const clip = validateGraphicClip(object.clip, object.type);
149
+ requireProperties(clip, ["x", "y", "width", "height"], `Graphic "${id}" Canvas clip`, scale, offset);
150
+ requireCanvasNativeValue(clip.x + clip.width, `Graphic "${id}" Canvas clip`, scale, offset?.[0]);
151
+ requireCanvasNativeValue(clip.y + clip.height, `Graphic "${id}" Canvas clip`, scale, offset?.[1]);
152
+ }
146
153
  },
147
154
  item({ id, object, owner }) {
148
155
  preflightGraphic(
@@ -0,0 +1,66 @@
1
+ import { isOwned } from '../../core/immutable.js';
2
+ import { render } from './index.js';
3
+
4
+ const compiled = new WeakMap();
5
+ // Translate only concrete position properties. Path/text child coordinates keep
6
+ // their authored relationship; gradients retain their local bounding-box units.
7
+ function position(properties, dx, dy) {
8
+ if (!dx && !dy) return properties;
9
+ const result = { ...properties };
10
+ for (const key of ['x', 'x1', 'x2']) if (key in result) result[key] += dx;
11
+ for (const key of ['y', 'y1', 'y2']) if (key in result) result[key] += dy;
12
+ if (result.commands) result.commands = result.commands.map(command => position(command, dx, dy));
13
+ return result;
14
+ }
15
+ function compile(scene) {
16
+ if (scene?.version !== 2 || ![scene.width, scene.height].every(n => Number.isFinite(n) && n > 0) || !Array.isArray(scene.nodes)) {
17
+ throw new TypeError('renderTransitionScene requires a version 2 Canvas scene.');
18
+ }
19
+ const objects = { canvas: { type: 'canvas', properties: { width: scene.width, height: scene.height }, children: [] } };
20
+ const keys = new Set();
21
+ let index = 0, activeClips = [];
22
+ const groups = [objects.canvas], origins = [[0, 0]];
23
+ const validClip = clip => Array.isArray(clip) && clip.length === 4 &&
24
+ clip.every(Number.isFinite) && clip[2] >= 0 && clip[3] >= 0;
25
+ for (const node of scene.nodes) {
26
+ const clips = node.clips ?? [node.clip];
27
+ if (typeof node.key !== 'string' || keys.has(node.key) || !['circle','rect','line','path','text'].includes(node.type) ||
28
+ node.offset?.length !== 2 || !node.offset.every(Number.isFinite) || !validClip(node.clip) ||
29
+ !Array.isArray(clips) || !clips.every(validClip) || !node.properties || typeof node.properties !== 'object') {
30
+ throw new TypeError('Canvas scene nodes require unique keys, concrete properties, offsets and clips.');
31
+ }
32
+ keys.add(node.key);
33
+ let common = 0;
34
+ while (common < Math.min(clips.length, activeClips.length) &&
35
+ clips[common].every((value, i) => value === activeClips[common][i])) common++;
36
+ groups.length = common + 1; origins.length = common + 1;
37
+ // Retain shared concrete clip ancestors across adjacent nodes. Rebuilding
38
+ // a fractional ancestor between its children can change raster coverage.
39
+ for (let i = common; i < clips.length; i++) {
40
+ const clip = clips[i], origin = origins.at(-1), parent = groups.at(-1);
41
+ const id = `clip-${index++}`;
42
+ const nested = objects[id] = { type: 'canvas', properties: {
43
+ x: clip[0] - origin[0], y: clip[1] - origin[1], width: clip[2], height: clip[3]
44
+ }, children: [] };
45
+ parent.children.push(id); groups.push(nested); origins.push([clip[0], clip[1]]);
46
+ }
47
+ activeClips = clips;
48
+ const group = groups.at(-1), origin = origins.at(-1);
49
+ const id = `node-${index++}`;
50
+ objects[id] = { type: node.type, properties: position(node.properties,
51
+ node.offset[0] - origin[0], node.offset[1] - origin[1]) };
52
+ group.children.push(id);
53
+ }
54
+ return { graphicSpec: { objects, order: ['canvas'] } };
55
+ }
56
+ /** Paint concrete Canvas scenes with the ordinary renderer; no semantics or DOM. */
57
+ export function renderTransitionScene(scene, context, options) {
58
+ // Only immutable producer scenes are cacheable. Intermediate animation frames
59
+ // may be reused/mutated by a caller and must always reflect their current data.
60
+ let program = isOwned(scene) ? compiled.get(scene) : undefined;
61
+ if (!program) {
62
+ program = compile(scene);
63
+ if (isOwned(scene)) compiled.set(scene, program);
64
+ }
65
+ return render(program, context, options);
66
+ }
@@ -1,6 +1,6 @@
1
1
  import { resolveCanvasSolidPaint } from "./fill.js";
2
2
  import { requireFiniteProperty, requireStringProperty } from "./validation.js";
3
- import { validateConcreteGraphicProperties } from
3
+ import { validateConcreteGraphicProperties, validateConcreteGraphicValue } from
4
4
  "../../grammar/schemas/concreteGraphic.js";
5
5
  import { normalizeRendererFontWeight } from "../text.js";
6
6
 
@@ -13,7 +13,8 @@ function drawText(context, child, collectionId) {
13
13
  const fontSize = requireFiniteProperty(properties, "fontSize", graphicId);
14
14
  const rotation = properties.rotation ?? 0;
15
15
  const opacity = properties.opacity ?? 1;
16
- const text = requireStringProperty(properties, "text", graphicId);
16
+ const text = properties.text;
17
+ validateConcreteGraphicValue("text", "text", text);
17
18
  const fill = requireStringProperty(properties, "fill", graphicId);
18
19
  const fontFamily = requireStringProperty(
19
20
  properties,