ggaction 0.0.21 → 0.0.22

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 (253) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/jitter.js +2 -2
  165. package/src/grammar/legendSpacing.js +45 -0
  166. package/src/grammar/lineMissing.js +90 -0
  167. package/src/grammar/lineSeries.js +1 -376
  168. package/src/grammar/lookup.js +97 -0
  169. package/src/grammar/markFilter.js +4 -2
  170. package/src/grammar/markLabels.js +8 -3
  171. package/src/grammar/numeric.js +1 -1
  172. package/src/grammar/pathCommands.js +1 -1
  173. package/src/grammar/pathSeries.js +1 -109
  174. package/src/grammar/pointPolygon.js +81 -0
  175. package/src/grammar/pointShapes.js +15 -15
  176. package/src/grammar/rects.js +1 -1
  177. package/src/grammar/regression/derive.js +1 -195
  178. package/src/grammar/scales/appearance.js +1 -1
  179. package/src/grammar/scales/color.js +1 -1
  180. package/src/grammar/scales/colorConsumers.js +1 -63
  181. package/src/grammar/scales/continuous.js +58 -1
  182. package/src/grammar/scales/definition.js +1 -1
  183. package/src/grammar/scales/ordinal.js +5 -2
  184. package/src/grammar/scales/policies.js +3 -3
  185. package/src/grammar/schemas/graphicBounds.js +40 -12
  186. package/src/grammar/schemas/graphicClip.js +23 -0
  187. package/src/grammar/schemas/graphicTree.js +16 -0
  188. package/src/grammar/statisticalReference.js +22 -0
  189. package/src/grammar/text.js +65 -1
  190. package/src/grammar/ticks.js +92 -1
  191. package/src/grammar/transformTopology.js +3 -1
  192. package/src/grammar/transforms.js +24 -0
  193. package/src/inspection.js +1 -495
  194. package/src/inspectionDescriptors.js +1 -1
  195. package/src/layout/canvas.js +1 -1
  196. package/src/layout/facetContentFlow.js +101 -0
  197. package/src/layout/facetHeaderTitles.js +1 -0
  198. package/src/layout/facetTopLane.js +29 -0
  199. package/src/layout/facets.js +1 -1
  200. package/src/layout/legendGroup.js +1 -1
  201. package/src/layout/legendItems.js +26 -10
  202. package/src/layout/legendSpacing.js +66 -0
  203. package/src/layout/plotDrawOrder.js +17 -0
  204. package/src/materialization/bars/histogram.js +8 -2
  205. package/src/materialization/bars/resolve.js +1 -1
  206. package/src/materialization/composition.js +1 -0
  207. package/src/materialization/compositionSnapshot.js +22 -2
  208. package/src/materialization/dataProvenance.js +1 -1
  209. package/src/materialization/facetGuides/layout.js +1 -1
  210. package/src/materialization/facetGuides/placement.js +23 -11
  211. package/src/materialization/facetGuides/preparation.js +43 -24
  212. package/src/materialization/facetHeaders.js +1 -1
  213. package/src/materialization/facetTopLane.js +25 -0
  214. package/src/materialization/facets.js +1 -1
  215. package/src/materialization/graphicHierarchy.js +5 -0
  216. package/src/materialization/graphicTranslation.js +2 -0
  217. package/src/materialization/guides/layout.js +1 -1
  218. package/src/materialization/guides/resources.js +1 -1
  219. package/src/materialization/legends.js +1 -1
  220. package/src/materialization/markClip.js +26 -0
  221. package/src/materialization/marks/capabilities.js +3 -2
  222. package/src/materialization/marks/grouping.js +7 -5
  223. package/src/materialization/marks/index.js +4 -2
  224. package/src/materialization/marks/policies.js +4 -0
  225. package/src/materialization/plotDrawOrder.js +44 -0
  226. package/src/materialization/rect.js +11 -3
  227. package/src/materialization/scaleGuideDependencies.js +13 -5
  228. package/src/materialization/selection/items/common.js +1 -1
  229. package/src/materialization/selection/items/path.js +1 -0
  230. package/src/materialization/selection/policies/index.js +2 -1
  231. package/src/materialization/text.js +10 -5
  232. package/src/mcp/cli.js +0 -0
  233. package/src/persistence/validation.js +108 -18
  234. package/src/persistence.js +4 -2
  235. package/src/renderers/canvas/index.js +16 -0
  236. package/src/renderers/canvas/isolated.js +11 -1
  237. package/src/renderers/canvas/native.js +7 -0
  238. package/src/renderers/canvas/scene.js +66 -0
  239. package/src/renderers/canvas/text.js +3 -2
  240. package/src/renderers/svg.js +1 -617
  241. package/src/renderers/svgSerialization.js +643 -0
  242. package/src/renderers/transitionScene.js +67 -0
  243. package/src/scene.js +89 -0
  244. package/src/selectors/axes.js +1 -1
  245. package/src/selectors/datasets.js +1 -0
  246. package/src/selectors/grids.js +9 -0
  247. package/src/selectors/markClip.js +28 -0
  248. package/src/selectors/raincloud.js +7 -0
  249. package/src/version.js +1 -1
  250. package/types/basic.d.ts +7 -0
  251. package/types/index.d.ts +8 -0
  252. package/types/program.d.ts +270 -60
  253. package/types/scene.d.ts +35 -0
@@ -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,12 @@
1
+ import { validateGraphicClip } from "../grammar/schemas/graphicClip.js";
2
+ import { validatePointShape } from "../grammar/pointShapes.js";
3
+ import {deriveLookupRows,sameLookupValue} from "../grammar/lookup.js";
4
+ import {deriveTransformSchema,datasetSchemaContext} from "../grammar/datasetSchema.js";
5
+ import {orderedDatasetAncestors} from "../grammar/datasetDependencies.js";
6
+ import {validateLegendSpacing} from "../grammar/legendSpacing.js";
7
+ import { requestedMarkClip, validateMarkClipCoordinate } from "../selectors/markClip.js";
1
8
  import { validateAuthoredFacetLegend } from "../grammar/facets/legendIntent.js";
9
+ import { validateFacetHeaderRotations } from "../grammar/facetHeaders.js";
2
10
  import { AXIS_RESOURCE_CHANNELS } from "../core/vocabulary.js";
3
11
  import { cartesianAxisOwners, assertCartesianAxisSideAvailable } from "../selectors/axes.js";
4
12
  import { axisGraphicIds } from "../selectors/axes.js";
@@ -20,16 +28,19 @@ import { validateGraphicItemLimit } from "../core/validation.js";
20
28
  import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
21
29
  import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
22
30
  import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
31
+ import { validateTickSelectionRounding, axisTickSelection } from "../grammar/ticks.js";
23
32
  import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
24
33
  import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
25
34
  import { validateRegressionFitStatistics, requireRegressionStatisticsSource, regressionStatisticsRows } from "../grammar/regression/statistics.js";
26
35
  import { resolveGraphicItemCapacity } from "../selectors/graphicCapacity.js";
27
36
  import { isContinuousColorScaleType } from "../grammar/scales/types.js";
37
+ import { validateTickColorChannels } from "../grammar/scales/colorConsumers.js";
28
38
  import { normalizeColorInterpolationOptions } from "../grammar/scales/color.js";
29
39
  import { normalizeTransformParameters } from "../grammar/scales/transformed.js";
30
40
  import { validateLegendGroupState } from "../grammar/legendGroups.js";
31
41
  import { normalizeGradientDirection, validateLegendGradient } from "../grammar/legendGradient.js";
32
42
  import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
43
+ import { resolveTextOffsetDataset } from "../grammar/text.js";
33
44
  import { isPlainObject } from "../core/immutable.js";
34
45
  import { validateUserId } from "../core/identifiers.js";
35
46
  import { collectAllResourceReferences } from "../core/resourceReferences.js";
@@ -48,6 +59,21 @@ export const STATE_KEYS = Object.freeze([
48
59
  "children", "compositionSpec", "context", "trace", "actionStack"
49
60
  ]);
50
61
 
62
+ function validateStoredColorLegendSampling(program, config, path) {
63
+ requireObject(config.sampling, path);
64
+ const sample = config.sampling;
65
+ exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
66
+ if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
67
+ if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
68
+ validateGeneratedItemLimit(sample.count, "Color legend sample count");
69
+ const scale = program.resolvedScales[config.scale];
70
+ if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
71
+ if (sample.mode === "values") {
72
+ requireArray(sample.values, path);
73
+ 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);
74
+ }
75
+ }
76
+
51
77
  export function requireObject(value, path) {
52
78
  if (!isPlainObject(value)) throw invalidSnapshot(`${path} must be a plain object.`, path);
53
79
  }
@@ -79,9 +105,10 @@ export function validateGraphic(graphic, path = "payload.graphicSpec") {
79
105
  if (!id.length) throw invalidSnapshot("Graphic IDs must be non-empty.", path);
80
106
  requireObject(object, `${path}.objects.${id}`);
81
107
  validateGraphicType(object.type);
82
- if (Object.keys(object).some(key => !["type", "properties", "items", "children"].includes(key))) {
108
+ if (Object.keys(object).some(key => !["type", "properties", "items", "children", "clip"].includes(key))) {
83
109
  throw invalidSnapshot(`Unknown graphic member at ${id}.`, path);
84
110
  }
111
+ if (object.clip !== undefined) validateGraphicClip(object.clip, object.type);
85
112
  if (object.properties !== undefined) validateConcreteGraphicProperties(object.type, object.properties);
86
113
  if (object.items !== undefined) {
87
114
  if (object.type === "canvas" || object.properties !== undefined) throw invalidSnapshot(`Invalid item owner "${id}".`, path);
@@ -143,7 +170,7 @@ function validateSemanticProperties(program, value, property) {
143
170
  validateStoredSemanticValue(program, parsed, value);
144
171
  }
145
172
 
146
- export function validateProgramState(program, path) {
173
+ export function validateProgramState(program, path, { allowMissingLookupSchemas = false } = {}) {
147
174
  const semantic = program.semanticSpec;
148
175
  exactKeys(semantic, ["datasets", "layers", "scales", "coordinates", "guides", "title"], `${path}.semanticSpec`);
149
176
  const resources = new Map();
@@ -161,6 +188,12 @@ export function validateProgramState(program, path) {
161
188
  }
162
189
  resources.set(referenceKind, entries);
163
190
  for (const resource of entries.values()) {
191
+ if (kind === "layer") validateTickColorChannels(resource);
192
+ if (kind === "layer" && ["dx", "dy"].some(channel => resource.encoding?.[channel] !== undefined)) {
193
+ const dataset = semantic.datasets.find(item => item.id === resource.data);
194
+ if (dataset === undefined) throw new Error("Numeric text offsets require an existing dataset.");
195
+ resolveTextOffsetDataset(resource, dataset);
196
+ }
164
197
  if (kind === "scale") {
165
198
  normalizeColorInterpolationOptions(resource.interpolate, resource);
166
199
  if (resource.type === "pow") normalizeTransformParameters("pow", {
@@ -181,6 +214,13 @@ export function validateProgramState(program, path) {
181
214
  if (program.compositionSpec?.type === "facet" && isPlainObject(program.compositionSpec.facet.guides.legend)) {
182
215
  validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
183
216
  }
217
+ if (program.compositionSpec?.type === "facet") {
218
+ const headers = program.materializationConfigs.facets?.[program.compositionSpec.id]?.headers;
219
+ if (headers !== undefined) validateFacetHeaderRotations(headers);
220
+ if (headers?.row?.title !== undefined && program.compositionSpec.facet.grid === undefined) {
221
+ throw new TypeError("Facet row field title requires a row-column facet grid.");
222
+ }
223
+ }
184
224
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
185
225
  for (const config of [
186
226
  ...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
@@ -189,17 +229,54 @@ export function validateProgramState(program, path) {
189
229
  if (config?.strokeDash !== undefined) validateGuideDash(config.strokeDash);
190
230
  if (config?.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
191
231
  }
192
- for (const owner of cartesianAxisOwners(program)) {
232
+ const cartesianOwners = cartesianAxisOwners(program);
233
+ for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
234
+ for (const config of [axis?.ticks, axis?.labels]) {
235
+ if (config === undefined || !Object.hasOwn(config, "rounding")) continue;
236
+ if (!cartesianOwners.some(owner => owner.id === id)) {
237
+ throw invalidSnapshot("Tick rounding requires a Cartesian axis owner.", path);
238
+ }
239
+ validateTickSelectionRounding(config, program.resolvedScales[config.scale]?.type);
240
+ }
241
+ }
242
+ for (const owner of cartesianOwners) {
193
243
  const channel = owner.id;
194
244
  const axis = owner.config;
245
+ if (axis?.ticks && axis?.labels &&
246
+ (Object.hasOwn(axis.ticks, "rounding") || Object.hasOwn(axis.labels, "rounding")) &&
247
+ JSON.stringify(axisTickSelection(axis.ticks)) !== JSON.stringify(axisTickSelection(axis.labels))) {
248
+ throw invalidSnapshot("Stored tick rounding conflicts with axis labels.", path);
249
+ }
195
250
  if (axis?.frame !== undefined) {
196
- exactKeys(axis.frame, ["offset"], `${path}.guides.axis.${channel}.frame`);
197
- validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
251
+ validateOptionObject(axis.frame, ["offset", "underlay"], `Stored axis "${channel}" frame`, { allowEmpty: false });
252
+ if (Object.hasOwn(axis.frame, "offset")) validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
253
+ if (Object.hasOwn(axis.frame, "underlay") && axis.frame.underlay !== true) {
254
+ throw new Error("Stored axis underlay must be canonical true intent.");
255
+ }
198
256
  }
199
257
  if (axis?.labels !== undefined) validateAxisTextPlacement(axis.labels);
200
258
  if (axis?.title !== undefined) validateAxisTextPlacement(axis.title, { title: true });
201
259
  }
260
+ for (const config of Object.values(program.guideConfigs.grid ?? {})) {
261
+ if (!Object.hasOwn(config, "rounding")) continue;
262
+ validateTickSelectionRounding(config, program.resolvedScales[config.scale]?.type);
263
+ const owners = cartesianOwners.map(owner => owner.config?.ticks)
264
+ .filter(ticks => ticks?.scale === config.scale);
265
+ if (config.inferredValues !== true || config.rounding !== "nearest" || owners.length === 0 ||
266
+ owners.some(ticks => JSON.stringify(axisTickSelection(ticks)) !== JSON.stringify(axisTickSelection(config)))) {
267
+ throw invalidSnapshot("Stored grid rounding requires its matching inferred tick policy.", path);
268
+ }
269
+ }
270
+ const border = program.materializationConfigs.canvas?.plotBorder;
271
+ if (isPlainObject(border) && Object.hasOwn(border, "underlay") && border.underlay !== true) {
272
+ throw new Error("Stored plot border underlay must be canonical true intent.");
273
+ }
202
274
  for (const id of Object.keys(program.guideConfigs.axis ?? {})) {
275
+ const config = program.guideConfigs.axis[id];
276
+ if (config?.frame !== undefined && Object.hasOwn(config.frame, "underlay") &&
277
+ !cartesianOwners.some(owner => owner.id === id)) {
278
+ throw new Error("Stored axis underlay requires a Cartesian axis owner.");
279
+ }
203
280
  if (!AXIS_RESOURCE_CHANNELS.includes(id) && !Object.hasOwn(program.semanticSpec.guides.axis ?? {}, id)) {
204
281
  throw invalidSnapshot("Named axis configs require their semantic owner.", path);
205
282
  }
@@ -242,6 +319,7 @@ export function validateProgramState(program, path) {
242
319
  if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
243
320
  for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
244
321
  validateLegendClip(kind,config);
322
+ validateLegendSpacing(kind,config);
245
323
  if (localLegendPosition(config) !== undefined) {
246
324
  const descriptor = describeLegendBlock(program, kind, config);
247
325
  for (const [key, override] of Object.entries(config.blockOverrides)) {
@@ -301,22 +379,17 @@ export function validateProgramState(program, path) {
301
379
  if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
302
380
  validateLegendGradient(config.gradient, "Stored legend gradient");
303
381
  }
382
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols" && config.sampling !== undefined) {
383
+ validateStoredColorLegendSampling(program, config, path);
384
+ if (config.sampling.mode !== "values" || config.count !== config.sampling.count) {
385
+ throw invalidSnapshot("Gradient exact sampling must retain its automatic count.", path);
386
+ }
387
+ }
304
388
  if (config.representation === undefined) continue;
305
389
  if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
306
390
  if (config.representation === "symbols") {
307
391
  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
- }
392
+ validateStoredColorLegendSampling(program, config, path);
320
393
  requireObject(config.symbol, path);
321
394
  validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
322
395
  if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
@@ -326,7 +399,7 @@ export function validateProgramState(program, path) {
326
399
  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
400
  validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
328
401
  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);
402
+ } else if (["symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
330
403
  }
331
404
 
332
405
  const regressionFits = program.materializationConfigs.regressionFits ?? {};
@@ -336,6 +409,18 @@ export function validateProgramState(program, path) {
336
409
  validateRegressionFitStatistics(cache, dataset, resources.get("data").get(dataset?.source));
337
410
  }
338
411
  for (const dataset of semantic.datasets) {
412
+ if(dataset.transform?.[0]?.type==="lookup"){
413
+ orderedDatasetAncestors(semantic.datasets,dataset.id);
414
+ const transform=dataset.transform[0],source=resources.get("data").get(dataset.source),right=resources.get("data").get(transform.lookup.data);
415
+ if(source===undefined||right===undefined)throw invalidSnapshot("Lookup requires both existing input datasets.",path);
416
+ const missingSchemas = [source, right, dataset].some(input => input.schema === undefined);
417
+ if (!allowMissingLookupSchemas || !missingSchemas) {
418
+ const schema=deriveTransformSchema(source.schema,transform,dataset.values,
419
+ datasetSchemaContext(source,transform,dataset.id,id=>resources.get("data").get(id)));
420
+ if(!sameLookupValue(dataset.schema,schema))throw invalidSnapshot("Lookup schema does not match both inputs.",path);
421
+ }
422
+ if(dataset.values!==undefined&&!sameLookupValue(dataset.values,deriveLookupRows(source.values,right.values,transform)))throw invalidSnapshot("Lookup values do not match both input datasets.",path);
423
+ }
339
424
  if (dataset.transform?.[0]?.type !== "regressionStatistics") continue;
340
425
  const fit = resources.get("data").get(dataset.source);
341
426
  requireRegressionStatisticsSource(fit);
@@ -378,7 +463,12 @@ export function validateProgramState(program, path) {
378
463
  }
379
464
  }
380
465
  for (const id of Object.keys(program.markConfigs)) {
466
+ requestedMarkClip(program.markConfigs[id]);
467
+ validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
381
468
  if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
469
+ if (resources.get("mark").get(id).mark.type === "point" && program.markConfigs[id].shape !== undefined) {
470
+ validatePointShape(program.markConfigs[id].shape);
471
+ }
382
472
  if (resources.get("mark").get(id).mark.type === "area") {
383
473
  validateAreaPaintConfig(program.markConfigs[id]);
384
474
  }
@@ -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,