ggaction 0.0.16 → 0.0.18

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 (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +10 -4
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -7,14 +7,19 @@ import {
7
7
  interpolateNumber,
8
8
  inverseLerp
9
9
  } from "../../../grammar/numeric.js";
10
- export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
10
+ export function resolveTemporalBarBand(valuesByConsumer, domain, range, markConfigs = {}, preserveRange = false) {
11
11
  const temporalBars = valuesByConsumer.filter(({ consumer }) => {
12
12
  const channels = resolveBarChannels(consumer.layer);
13
- return resolveBarGrain(consumer.layer) === BAR_GRAINS.aggregate &&
13
+ return [BAR_GRAINS.aggregate, BAR_GRAINS.ranged].includes(resolveBarGrain(consumer.layer)) &&
14
14
  channels?.category === consumer.channel &&
15
15
  consumer.encoding.fieldType === "temporal";
16
16
  });
17
17
  if (temporalBars.length === 0) return undefined;
18
+ const intervalBars = temporalBars.filter(({consumer}) => markConfigs[consumer.layer.id]?.barWidth?.interval !== undefined);
19
+ if (intervalBars.length > 0) {
20
+ if (intervalBars.length !== temporalBars.length) throw new Error("Temporal interval and centered bar widths cannot share a position scale.");
21
+ return undefined;
22
+ }
18
23
  const temporalFields = new Set(
19
24
  temporalBars.map(({ consumer }) => consumer.encoding.field)
20
25
  );
@@ -31,6 +36,11 @@ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
31
36
  "A temporal bar position scale requires compatible bar, line, rule, or text consumers of one field."
32
37
  );
33
38
  }
39
+ // Explicit pixel widths do not need an inferred half-slot inset. Keep one
40
+ // common range when a shared scale also has band-width bar consumers.
41
+ preserveRange ||= temporalBars.every(({ consumer }) =>
42
+ markConfigs[consumer.layer.id]?.barWidth?.pixels !== undefined
43
+ );
34
44
  const values = temporalBars.flatMap(item => item.values);
35
45
  const ordered = [...new Set(values)].sort((left, right) => left - right);
36
46
  if (ordered.length < 2) {
@@ -59,7 +69,7 @@ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
59
69
  interpolateNumber(range[0], range[1], bandwidthFraction) - range[0]
60
70
  )
61
71
  : directBandwidth;
62
- const resolvedRange = stable
72
+ const resolvedRange = preserveRange ? [...range] : stable
63
73
  ? [
64
74
  interpolateNumber(range[0], range[1], bandwidthFraction / 2),
65
75
  interpolateNumber(range[0], range[1], 1 - bandwidthFraction / 2)
@@ -79,10 +89,12 @@ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
79
89
  });
80
90
  let bandwidth = Infinity;
81
91
  for (let index = 1; index < positions.length; index += 1) {
82
- bandwidth = Math.min(
83
- bandwidth,
84
- Math.abs(positions[index] - positions[index - 1])
85
- );
92
+ const gap = Math.abs(positions[index] - positions[index - 1]);
93
+ // Keep a finite conservative slot when an explicitly requested span itself
94
+ // exceeds the representable difference of two finite endpoints.
95
+ bandwidth = Math.min(bandwidth,
96
+ preserveRange && !Number.isFinite(gap) ? estimatedBandwidth : gap);
97
+
86
98
  }
87
99
  if (![estimatedBandwidth, bandwidth, ...resolvedRange].every(Number.isFinite)) {
88
100
  throw new RangeError("Temporal bar position exceeds the finite numeric range.");
@@ -27,11 +27,13 @@ export function resolveSeriesLayoutDomain({
27
27
  seriesLayouts[index] === undefined &&
28
28
  !isSourceOwnedText(consumer.layer)
29
29
  );
30
+ const layout = activeLayouts[0].layout;
30
31
  const compatibleDirect = directConsumers.every(({ consumer }) =>
31
32
  layoutConsumers.every(({ consumer: layoutConsumer }) => {
32
33
  const aggregate = layoutConsumer.encoding.aggregate;
33
34
  const directEncoding = consumer.encoding;
34
35
  return directEncoding.fieldType === "quantitative" && (
36
+ layout === "overlay" ||
35
37
  Object.hasOwn(directEncoding, "datum") ||
36
38
  (
37
39
  aggregate !== "count" &&
@@ -45,7 +47,6 @@ export function resolveSeriesLayoutDomain({
45
47
  `Series layout scale "${id}" cannot be shared with another policy.`
46
48
  );
47
49
  }
48
- const layout = activeLayouts[0].layout;
49
50
  const values = [
50
51
  ...activeLayouts.flatMap(item => item.values),
51
52
  ...directConsumers.flatMap(item => item.values)
@@ -35,6 +35,36 @@ import { resolveOffsetScalePolicy } from "./policies/offset.js";
35
35
  import { resolveBinnedPositionDomain } from "./policies/binnedPosition.js";
36
36
  import { resolveSeriesLayoutDomain } from "./policies/series.js";
37
37
  import { OFFSET_POSITION_CHANNELS } from "../../core/vocabulary.js";
38
+ import { annotateError } from "../../core/diagnostics.js";
39
+ import { cloneAndFreeze } from "../../core/immutable.js";
40
+ import { comparableDataIdentity } from "../revisionIdentity.js";
41
+
42
+ const bindingSignatures = new WeakMap();
43
+
44
+ function scaleBindingSignature(channel, consumers, { comparable = false } = {}) {
45
+ return consumers.map(consumer => ({
46
+ channel: consumer.channel ?? channel,
47
+ coordinate: consumer.layer.coordinate,
48
+ data: comparable
49
+ ? comparableDataIdentity(consumer.layer.data)
50
+ : consumer.layer.data,
51
+ field: consumer.encoding.field,
52
+ datum: consumer.encoding.datum,
53
+ fieldType: consumer.encoding.fieldType,
54
+ temporalUnit: consumer.encoding.temporalUnit,
55
+ aggregate: consumer.encoding.aggregate,
56
+ bin: consumer.encoding.bin
57
+ })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
58
+ }
59
+
60
+ export function rememberResolvedScaleBinding(resolvedScale, channel, consumers) {
61
+ if (resolvedScale !== undefined) {
62
+ bindingSignatures.set(
63
+ resolvedScale,
64
+ cloneAndFreeze(scaleBindingSignature(channel, consumers))
65
+ );
66
+ }
67
+ }
38
68
 
39
69
  function resolveDefaultDomain({
40
70
  scale,
@@ -130,7 +160,7 @@ function resolveContinuousScale({
130
160
  }) {
131
161
  return {
132
162
  type: isSequentialColor
133
- ? "sequential"
163
+ ? scale.type
134
164
  : isOrdinalAppearance
135
165
  ? validateOrdinalScaleType(scale.type)
136
166
  : scale.type === "time"
@@ -206,12 +236,22 @@ export function resolveScaleMaterialization(options) {
206
236
  .flatMap(item => item.values)
207
237
  .filter(value => value !== undefined);
208
238
  const preserved = resolvedScales[id]?.domain;
239
+ const bindingSignature = scaleBindingSignature(channel, consumers);
240
+ const comparableBindingSignature = scaleBindingSignature(channel, consumers, {
241
+ comparable: true
242
+ });
243
+ const priorSignature = resolvedScales[id] === undefined
244
+ ? undefined
245
+ : bindingSignatures.get(resolvedScales[id]);
246
+ const samePreservedMeaning = priorSignature !== undefined &&
247
+ JSON.stringify(priorSignature) === JSON.stringify(comparableBindingSignature);
209
248
  if (
210
249
  scale.domain === "auto" &&
211
250
  Array.isArray(preserved) &&
212
- consumers.some(({ layer }) =>
251
+ resolvedScales[id]?.type === scale.type &&
252
+ (consumers.some(({ layer }) =>
213
253
  markConfigs?.[layer.id]?.markFilter?.empty === true
214
- )
254
+ ) || (scale.emptyDomain === "preserve" && allValues.length === 0 && samePreservedMeaning))
215
255
  ) {
216
256
  return resolveScaleMaterialization({
217
257
  ...options,
@@ -222,6 +262,27 @@ export function resolveScaleMaterialization(options) {
222
262
  }))
223
263
  });
224
264
  }
265
+ if (
266
+ scale.domain === "auto" && allValues.length === 0 &&
267
+ scale.emptyDomain === "preserve" && Array.isArray(preserved) && !samePreservedMeaning
268
+ ) {
269
+ throw annotateError(new Error(`Scale "${id}" requires an explicit domain because its binding changed.`), {
270
+ code: "incompatible-resource",
271
+ operation: "rematerializeScale",
272
+ optionPath: "domain",
273
+ resourceId: id,
274
+ reason: "domain-required"
275
+ });
276
+ }
277
+ if (scale.domain === "auto" && allValues.length === 0) {
278
+ throw annotateError(new Error(`Scale "${id}" requires an explicit domain for empty data.`), {
279
+ code: "incompatible-resource",
280
+ operation: "rematerializeScale",
281
+ optionPath: "domain",
282
+ resourceId: id,
283
+ reason: "domain-required"
284
+ });
285
+ }
225
286
  if (channel === "size") {
226
287
  return resolveSizeScale({ ...scale, values: allValues });
227
288
  }
@@ -316,7 +377,7 @@ export function resolveScaleMaterialization(options) {
316
377
  validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
317
378
  if (channel === "shape" && domain.length > range.length) {
318
379
  throw new Error(
319
- `Shape scale "${id}" requires at least one distinct shape per domain value.`
380
+ `Shape scale "${id}" requires at least one shape entry per domain value.`
320
381
  );
321
382
  }
322
383
 
@@ -379,7 +440,9 @@ export function resolveScaleMaterialization(options) {
379
440
  const temporalBarBand = resolveTemporalBarBand(
380
441
  valuesByConsumer,
381
442
  domain,
382
- range
443
+ range,
444
+ markConfigs,
445
+ Array.isArray(scale.range)
383
446
  );
384
447
  if (temporalBarBand !== undefined) {
385
448
  resolvedScale = {
@@ -389,7 +452,9 @@ export function resolveScaleMaterialization(options) {
389
452
  };
390
453
  }
391
454
  }
392
- return scale.reverse === true
455
+ const finalScale = cloneAndFreeze(scale.reverse === true
393
456
  ? reverseResolvedScale(resolvedScale)
394
- : resolvedScale;
457
+ : resolvedScale);
458
+ bindingSignatures.set(finalScale, cloneAndFreeze(bindingSignature));
459
+ return finalScale;
395
460
  }
@@ -16,7 +16,7 @@ export function resolveArcItems(program, layer, dataset) {
16
16
  thetaScale,
17
17
  ...(radiusScale === undefined ? {} : { radiusScale }),
18
18
  frame,
19
- innerRadiusRatio: program.markConfigs[layer.id]?.innerRadius ?? 0
19
+ innerRadius: program.markConfigs[layer.id]?.innerRadius ?? 0
20
20
  });
21
21
  const graphic = program.graphicSpec.objects[layer.id];
22
22
  const stroke = layer.encoding?.stroke;
@@ -1,3 +1,4 @@
1
+ import { hasRectRadius } from "../../../grammar/roundedRect.js";
1
2
  import { deriveSeriesBarCells } from "../../../grammar/bars/aggregate.js";
2
3
  import { readSeriesIdentity } from "../../../grammar/pathSeries.js";
3
4
  import {
@@ -128,9 +129,12 @@ function histogramDefinitions(program, layer, dataset) {
128
129
  }
129
130
 
130
131
  function rangedDefinitions(layer, dataset) {
132
+ const measure = resolveBarGrain(layer) === BAR_GRAINS.centered ? resolveBarChannels(layer).measure : undefined;
131
133
  return dataset.values.map(row => ({
132
134
  fields: ownFields(row),
133
- channels: channelMapFromRow(row, layer),
135
+ channels: { ...channelMapFromRow(row, layer),
136
+ ...(measure !== undefined && layer.encoding[`${measure}2`] === undefined
137
+ ? { [measure]: 0, [`${measure}2`]: row[layer.encoding[measure].field] } : {}) },
134
138
  members: [row]
135
139
  }));
136
140
  }
@@ -205,16 +209,13 @@ export function resolveBarItems(program, layer, dataset, selectionGrain) {
205
209
  ? histogramDefinitions(program, layer, dataset)
206
210
  : grain === BAR_GRAINS.aggregate
207
211
  ? aggregateCellDefinitions(program, layer, dataset)
208
- : grain === BAR_GRAINS.ranged
212
+ : [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(grain)
209
213
  ? rangedDefinitions(layer, dataset)
210
214
  : undefined;
211
215
  if (definitions === undefined) {
212
216
  throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
213
217
  }
214
- const graphicTypes = Object.hasOwn(
215
- program.markConfigs[layer.id]?.barAppearance ?? {},
216
- "cornerRadius"
217
- ) ? ["rect", "path"] : "rect";
218
+ const graphicTypes = hasRectRadius(program.markConfigs[layer.id]?.barAppearance) ? ["rect", "path"] : "rect";
218
219
  if (selectionGrain === "stack") {
219
220
  return finalizeItems(
220
221
  program,
@@ -59,15 +59,16 @@ export function resolveLineItems(program, layer, dataset) {
59
59
  const derived = deriveLineSeries(
60
60
  dataset.values,
61
61
  layer,
62
- x?.bin === undefined
63
- ? undefined
64
- : {
62
+ {
63
+ xDomain: program.resolvedScales[x?.scale]?.domain,
64
+ ...(x?.bin === undefined ? {} : {
65
65
  xBinBoundaries: resolveLineBins(
66
66
  dataset.values,
67
67
  layer,
68
68
  requireSemanticScale(program, x.scale)
69
69
  ).boundaries
70
- }
70
+ })
71
+ }
71
72
  );
72
73
  return finalizeItems(
73
74
  program,
@@ -82,8 +83,8 @@ export function resolveAreaItems(program, layer, dataset) {
82
83
  const transform = findUpstreamTransform(program, dataset, "density");
83
84
  const derived = transform === undefined
84
85
  ? layer.encoding?.y?.stack === "center"
85
- ? deriveCenteredAreaSeries(dataset.values, layer)
86
- : deriveAreaSeries(dataset.values, layer)
86
+ ? deriveCenteredAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
87
+ : deriveAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
87
88
  : deriveDensityAreaSeries(dataset.values, layer, transform);
88
89
  return finalizeItems(
89
90
  program,
@@ -1,5 +1,5 @@
1
1
  import { rectUsesFields } from "../../../grammar/rects.js";
2
- import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
2
+ import { hasRectRadius, readRectItemGeometry } from "../../../grammar/roundedRect.js";
3
3
  import { resolveRectRows } from "../../rect.js";
4
4
  import {
5
5
  channelMapFromRow,
@@ -40,7 +40,7 @@ function resolveGradientPlotItems(program, layer, dataset) {
40
40
 
41
41
  export function resolveRectItems(program, layer, dataset) {
42
42
  const graphic = program.graphicSpec.objects[layer.id];
43
- const rounded = Object.hasOwn(program.markConfigs[layer.id] ?? {}, "cornerRadius");
43
+ const rounded = hasRectRadius(program.markConfigs[layer.id]);
44
44
  const compatibleCollection = rounded && graphic?.type === "collection" &&
45
45
  graphic.items?.every(item => ["rect", "path"].includes(item.type));
46
46
  if (
@@ -5,10 +5,11 @@ import {
5
5
  resolveMarkLabelPlacement
6
6
  } from "../layout/labels.js";
7
7
  import { resolveMarkLabelValues } from "../grammar/markLabels.js";
8
- import { formatTextValue } from "../grammar/text.js";
8
+ import { formatTextValue, resolveTextLines } 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";
12
13
  import { findCoordinate } from "../selectors/coordinates.js";
13
14
  import { findLayer } from "../selectors/layers.js";
14
15
  import { unionConcreteGraphicBounds } from
@@ -273,12 +274,14 @@ function concreteTextItem(config, position, text, { offsets = true } = {}) {
273
274
  if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
274
275
  return undefined;
275
276
  }
277
+ const lines = resolveTextLines(text, config);
276
278
  return {
277
279
  type: "text",
278
280
  properties: {
279
281
  x: x + (offsets ? config.dx : 0),
280
282
  y: y + (offsets ? config.dy : 0),
281
283
  text,
284
+ ...(lines === undefined ? {} : { lines }),
282
285
  fill: config.fill,
283
286
  opacity: config.opacity,
284
287
  fontSize: config.fontSize,
@@ -309,6 +312,11 @@ function relativeLuminance(color) {
309
312
  }
310
313
 
311
314
  function sourceTextConfig(config, source, item) {
315
+ if (config.inheritColor && config.fillExplicit !== true) {
316
+ const fill = item.properties[config.inheritColor];
317
+ if (typeof fill !== "string") throw new Error(`Source mark "${source.id}" has no resolved ${config.inheritColor} color.`);
318
+ return { ...config, fill };
319
+ }
312
320
  if (
313
321
  !["arc", "rect"].includes(source.mark.type) ||
314
322
  config.fillExplicit === true
@@ -382,6 +390,7 @@ function resolveSourceTextItems(program, layer, config) {
382
390
  geometry: sourcePlacementGeometry(program, source, item),
383
391
  text: {
384
392
  text,
393
+ lines: resolveTextLines(text, resolvedConfig),
385
394
  fontSize: resolvedConfig.fontSize,
386
395
  fontFamily: resolvedConfig.fontFamily,
387
396
  fontWeight: resolvedConfig.fontWeight,
@@ -395,7 +404,7 @@ function resolveSourceTextItems(program, layer, config) {
395
404
  if (!resolved.visible) return [];
396
405
  if (
397
406
  resolved.anchor.startsWith("outside") &&
398
- config.fillExplicit !== true
407
+ config.fillExplicit !== true && !config.inheritColor
399
408
  ) {
400
409
  resolvedConfig = config;
401
410
  }
@@ -414,11 +423,12 @@ function resolveSourceTextItems(program, layer, config) {
414
423
  }
415
424
 
416
425
  function resolveRowTextItems(program, layer, config) {
417
- const dataset = findDataset(program, layer.data);
418
- if (dataset === undefined) {
426
+ const sourceDataset = findDataset(program, layer.data);
427
+ if (sourceDataset === undefined) {
419
428
  throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
420
429
  }
421
- const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
430
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
431
+ const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
422
432
  const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
423
433
  ? dataset.values.length
424
434
  : 1;
@@ -452,9 +462,10 @@ function resolveRowTextItems(program, layer, config) {
452
462
  const rows = length === 1 && dataset.values.length === 0
453
463
  ? [undefined]
454
464
  : dataset.values.slice(0, length);
465
+ const fills = resolveRowEncodingValues(program, layer, dataset, "color");
455
466
  return rows.flatMap((row, index) => {
456
467
  const concrete = concreteItem(
457
- config,
468
+ fills === undefined ? config : { ...config, fill: fills[index] },
458
469
  { x: x[index], y: y[index] },
459
470
  contentValue(layer.encoding.text, row),
460
471
  layer.encoding.text.format
@@ -0,0 +1,15 @@
1
+ import { freezeOwned } from "../core/immutable.js";
2
+
3
+ // Reconcile optional derived line geometry without recreating its text graphic.
4
+ // The owning wrapped domain materializer records the transition.
5
+ export function materializeTextLines(program, id, lines) {
6
+ if (lines !== undefined) return program.editGraphics({ target: id, property: "lines", value: lines });
7
+ const graphic = program.graphicSpec.objects[id];
8
+ if (!Object.hasOwn(graphic.properties, "lines")) return program;
9
+ const { lines: _lines, ...properties } = graphic.properties;
10
+ return program._clone({ graphicSpec: freezeOwned({ ...program.graphicSpec,
11
+ objects: freezeOwned({ ...program.graphicSpec.objects,
12
+ [id]: freezeOwned({ ...graphic, properties: freezeOwned(properties) })
13
+ })
14
+ }) });
15
+ }
@@ -5,7 +5,7 @@ export function invalidSnapshot(message, optionPath) {
5
5
  return annotateError(new TypeError(message), { code: "unsupported-format", optionPath });
6
6
  }
7
7
 
8
- export function encodeValue(value, path = "payload", ancestors = new WeakSet()) {
8
+ export function encodeValue(value, path = "payload", ancestors = new WeakSet(), shared) {
9
9
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
10
10
  if (value === undefined) return ["undefined"];
11
11
  if (typeof value === "bigint") return ["bigint", value.toString()];
@@ -17,6 +17,7 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
17
17
  if (ancestors.has(value)) throw invalidSnapshot(`Cannot serialize a cycle at ${path}.`, path);
18
18
  if (!Array.isArray(value) && !isPlainObject(value)) throw invalidSnapshot(`Cannot serialize a class instance at ${path}.`, path);
19
19
  if (Object.getOwnPropertySymbols(value).length) throw invalidSnapshot(`Cannot serialize symbol keys at ${path}.`, path);
20
+ if (shared?.seen.has(value)) return shared.seen.get(value);
20
21
  const keys = Object.getOwnPropertyNames(value);
21
22
  if (Array.isArray(value) ? keys.some(key => key !== "length" && !(/^(0|[1-9]\d*)$/.test(key) && Number(key) < value.length)) :
22
23
  keys.some(key => !Object.getOwnPropertyDescriptor(value, key).enumerable)) {
@@ -25,18 +26,48 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
25
26
  ancestors.add(value);
26
27
  const result = Array.isArray(value)
27
28
  ? ["array", Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)
28
- ? encodeValue(value[index], `${path}[${index}]`, ancestors) : ["hole"])]
29
- : ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors)])];
29
+ ? encodeValue(value[index], `${path}[${index}]`, ancestors, shared) : ["hole"])]
30
+ : ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors, shared)])];
30
31
  ancestors.delete(value);
32
+ if (shared !== undefined && Array.isArray(value) && value.length >= 32) {
33
+ const key = JSON.stringify(result);
34
+ let index = shared.keys.get(key);
35
+ if (index === undefined) {
36
+ index = shared.arrays.length;
37
+ shared.keys.set(key, index);
38
+ shared.arrays.push(result);
39
+ }
40
+ const reference = ["reference", index];
41
+ shared.seen.set(value, reference);
42
+ return reference;
43
+ }
31
44
  return result;
32
45
  }
33
46
 
34
- export function decodeValue(value, path = "payload") {
47
+ export function encodeSharedValue(value) {
48
+ const shared = { arrays: [], keys: new Map(), seen: new WeakMap() };
49
+ const root = encodeValue(value, "payload", new WeakSet(), shared);
50
+ return ["shared", shared.arrays, root];
51
+ }
52
+
53
+ export function decodeValue(value, path = "payload", references) {
35
54
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
36
55
  if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
37
56
  const invalid = () => { throw invalidSnapshot(`Invalid encoded value at ${path}.`, path); };
38
57
  if (!Array.isArray(value)) return invalid();
39
58
  const [tag, payload] = value;
59
+ if (tag === "shared" && references === undefined && value.length === 3 && Array.isArray(payload)) {
60
+ const arrays = [];
61
+ for (const [index, encoded] of payload.entries()) {
62
+ if (!Array.isArray(encoded) || encoded[0] !== "array") return invalid();
63
+ arrays.push(decodeValue(encoded, `${path}.arrays[${index}]`, arrays));
64
+ }
65
+ return decodeValue(value[2], path, arrays);
66
+ }
67
+ if (tag === "reference" && references !== undefined && value.length === 2 &&
68
+ Number.isInteger(payload) && payload >= 0 && payload < references.length) {
69
+ return references[payload];
70
+ }
40
71
  if (tag === "undefined" && value.length === 1) return undefined;
41
72
  if (value.length !== 2) return invalid();
42
73
  if (tag === "bigint" && typeof payload === "string" && /^(?:0|-?[1-9]\d*)$/.test(payload)) return BigInt(payload);
@@ -47,7 +78,7 @@ export function decodeValue(value, path = "payload") {
47
78
  const result = new Array(payload.length);
48
79
  payload.forEach((child, index) => {
49
80
  if (Array.isArray(child) && child.length === 1 && child[0] === "hole") return;
50
- result[index] = decodeValue(child, `${path}[${index}]`);
81
+ result[index] = decodeValue(child, `${path}[${index}]`, references ?? []);
51
82
  });
52
83
  return result;
53
84
  }
@@ -56,7 +87,7 @@ export function decodeValue(value, path = "payload") {
56
87
  for (const entry of payload) {
57
88
  if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || Object.hasOwn(result, entry[0])) return invalid();
58
89
  Object.defineProperty(result, entry[0], {
59
- value: decodeValue(entry[1], `${path}.${entry[0]}`), enumerable: true, writable: true, configurable: true
90
+ value: decodeValue(entry[1], `${path}.${entry[0]}`, references ?? []), enumerable: true, writable: true, configurable: true
60
91
  });
61
92
  }
62
93
  return result;
@@ -1,3 +1,9 @@
1
+ import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
2
+ import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
3
+ import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
4
+ import { isContinuousColorScaleType } from "../grammar/scales/types.js";
5
+ import { validateLegendGroupState } from "../grammar/legendGroups.js";
6
+ import { validateLegendGradient } from "../grammar/legendGradient.js";
1
7
  import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
2
8
  import { isPlainObject } from "../core/immutable.js";
3
9
  import { validateUserId } from "../core/identifiers.js";
@@ -142,6 +148,44 @@ export function validateProgramState(program, path) {
142
148
  for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
143
149
  for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
144
150
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
151
+ if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
152
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
153
+ if (config.titleStyle !== undefined) {
154
+ validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
155
+ validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
156
+ }
157
+ if (config.inferredOffset !== undefined && typeof config.inferredOffset !== "boolean") throw invalidSnapshot("Invalid legend offset inference.", path);
158
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
159
+ validateLegendGradient(config.gradient, "Stored legend gradient");
160
+ }
161
+ if (config.representation === undefined) continue;
162
+ if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
163
+ if (config.representation === "symbols") {
164
+ if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
165
+ requireObject(config.sampling, path);
166
+ const sample = config.sampling;
167
+ exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
168
+ if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
169
+ if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
170
+ validateGeneratedItemLimit(sample.count, "Color legend sample count");
171
+ const scale = program.resolvedScales[config.scale];
172
+ if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
173
+ if (sample.mode === "values") {
174
+ requireArray(sample.values, path);
175
+ 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);
176
+ }
177
+ requireObject(config.symbol, path);
178
+ validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
179
+ if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
180
+ validatePositiveFinite(config.symbol.radius, "Stored sampled color radius");
181
+ validateNonNegativeFinite(config.symbol.strokeWidth, "Stored sampled color strokeWidth");
182
+ if (config.symbol.stroke !== undefined) validateNonEmptyString(config.symbol.stroke, "Stored sampled color stroke");
183
+ 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);
184
+ validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
185
+ validateNonNegativeFinite(config.offset, "Stored sampled color offset");
186
+ } else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
187
+ }
188
+
145
189
  const selections = program.materializationConfigs.selections ?? {};
146
190
  requireObject(selections, `${path}.materializationConfigs.selections`);
147
191
  for (const [id, selection] of Object.entries(selections)) {
@@ -167,6 +211,9 @@ export function validateProgramState(program, path) {
167
211
  }
168
212
  for (const id of Object.keys(program.markConfigs)) {
169
213
  if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
214
+ if (resources.get("mark").get(id).mark.type === "area") {
215
+ validateAreaPaintConfig(program.markConfigs[id]);
216
+ }
170
217
  }
171
218
  for (const kind of ["data", "mark"]) {
172
219
  const visited = new Set();