ggaction 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -160,7 +160,7 @@ function resolveContinuousScale({
160
160
  }) {
161
161
  return {
162
162
  type: isSequentialColor
163
- ? "sequential"
163
+ ? scale.type
164
164
  : isOrdinalAppearance
165
165
  ? validateOrdinalScaleType(scale.type)
166
166
  : scale.type === "time"
@@ -200,6 +200,8 @@ function resolveContinuousScale({
200
200
  }
201
201
  : {}),
202
202
  ...(isSequentialColor ? { interpolate: scale.interpolate ?? "rgb" } : {}),
203
+ ...(scale.whitePoint === undefined ? {} : {whitePoint: scale.whitePoint}),
204
+ ...(scale.gamma === undefined ? {} : {gamma: scale.gamma}),
203
205
  ...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {})
204
206
  };
205
207
  }
@@ -377,7 +379,7 @@ export function resolveScaleMaterialization(options) {
377
379
  validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
378
380
  if (channel === "shape" && domain.length > range.length) {
379
381
  throw new Error(
380
- `Shape scale "${id}" requires at least one distinct shape per domain value.`
382
+ `Shape scale "${id}" requires at least one shape entry per domain value.`
381
383
  );
382
384
  }
383
385
 
@@ -440,7 +442,9 @@ export function resolveScaleMaterialization(options) {
440
442
  const temporalBarBand = resolveTemporalBarBand(
441
443
  valuesByConsumer,
442
444
  domain,
443
- range
445
+ range,
446
+ markConfigs,
447
+ Array.isArray(scale.range)
444
448
  );
445
449
  if (temporalBarBand !== undefined) {
446
450
  resolvedScale = {
@@ -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,7 +5,7 @@ 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";
@@ -274,12 +274,14 @@ function concreteTextItem(config, position, text, { offsets = true } = {}) {
274
274
  if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
275
275
  return undefined;
276
276
  }
277
+ const lines = resolveTextLines(text, config);
277
278
  return {
278
279
  type: "text",
279
280
  properties: {
280
281
  x: x + (offsets ? config.dx : 0),
281
282
  y: y + (offsets ? config.dy : 0),
282
283
  text,
284
+ ...(lines === undefined ? {} : { lines }),
283
285
  fill: config.fill,
284
286
  opacity: config.opacity,
285
287
  fontSize: config.fontSize,
@@ -310,6 +312,11 @@ function relativeLuminance(color) {
310
312
  }
311
313
 
312
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
+ }
313
320
  if (
314
321
  !["arc", "rect"].includes(source.mark.type) ||
315
322
  config.fillExplicit === true
@@ -383,6 +390,7 @@ function resolveSourceTextItems(program, layer, config) {
383
390
  geometry: sourcePlacementGeometry(program, source, item),
384
391
  text: {
385
392
  text,
393
+ lines: resolveTextLines(text, resolvedConfig),
386
394
  fontSize: resolvedConfig.fontSize,
387
395
  fontFamily: resolvedConfig.fontFamily,
388
396
  fontWeight: resolvedConfig.fontWeight,
@@ -396,7 +404,7 @@ function resolveSourceTextItems(program, layer, config) {
396
404
  if (!resolved.visible) return [];
397
405
  if (
398
406
  resolved.anchor.startsWith("outside") &&
399
- config.fillExplicit !== true
407
+ config.fillExplicit !== true && !config.inheritColor
400
408
  ) {
401
409
  resolvedConfig = config;
402
410
  }
@@ -420,7 +428,7 @@ function resolveRowTextItems(program, layer, config) {
420
428
  throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
421
429
  }
422
430
  const dataset = applyItemMissingPolicy(layer, sourceDataset);
423
- const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
431
+ const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
424
432
  const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
425
433
  ? dataset.values.length
426
434
  : 1;
@@ -454,9 +462,10 @@ function resolveRowTextItems(program, layer, config) {
454
462
  const rows = length === 1 && dataset.values.length === 0
455
463
  ? [undefined]
456
464
  : dataset.values.slice(0, length);
465
+ const fills = resolveRowEncodingValues(program, layer, dataset, "color");
457
466
  return rows.flatMap((row, index) => {
458
467
  const concrete = concreteItem(
459
- config,
468
+ fills === undefined ? config : { ...config, fill: fills[index] },
460
469
  { x: x[index], y: y[index] },
461
470
  contentValue(layer.encoding.text, row),
462
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
+ }
@@ -1,3 +1,5 @@
1
+ import { cartesianAxisOwners, axisTargetArgs } from "../selectors/axes.js";
2
+ import { axisGraphicIds } from "./guides/resources.js";
1
3
  import { applyMaterializationPlan, buildMaterializationPlan } from "./planner.js";
2
4
  import { withGuideLayoutTransaction } from "./guides/layout.js";
3
5
 
@@ -13,11 +15,17 @@ export function rematerializeTypography(program) {
13
15
  .map(layer => ({ op: "rematerializeTextMark", args: { id: layer.id } }));
14
16
  const guides = [];
15
17
  for (const [id, op] of [
16
- ["xAxisLabels", "editXAxisLabels"], ["yAxisLabels", "editYAxisLabels"],
17
- ["xAxisTitle", "editXAxisTitle"], ["yAxisTitle", "editYAxisTitle"],
18
18
  ["thetaAxisLabels", "editThetaAxisLabels"], ["radialAxisLabels", "editRadialAxisLabels"],
19
19
  ["thetaAxisTitle", "editThetaAxisTitle"], ["radialAxisTitle", "editRadialAxisTitle"]
20
20
  ]) if (exists(id, op)) guides.push({ op });
21
+ for (const owner of cartesianAxisOwners(program)) {
22
+ for (const [index, component] of [[2, "Labels"], [3, "Title"]]) {
23
+ const op = `edit${owner.channel.toUpperCase()}Axis${component}`;
24
+ if (exists(axisGraphicIds(owner.channel, owner.id)[index], op)) {
25
+ guides.push({ op, ...(owner.named ? { args: axisTargetArgs(owner.id, owner.channel) } : {}) });
26
+ }
27
+ }
28
+ }
21
29
  const parallel = program.guideConfigs.axis?.parallel?.axes;
22
30
  if (parallel !== undefined && typeof program.rematerializeParallelAxes === "function") {
23
31
  guides.push({ op: "rematerializeParallelAxes", args: { target: parallel.target } });
@@ -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,34 @@
1
+ import { validateAuthoredFacetLegend } from "../grammar/facets/legendIntent.js";
2
+ import { AXIS_RESOURCE_CHANNELS } from "../core/vocabulary.js";
3
+ import { cartesianAxisOwners, assertCartesianAxisSideAvailable } from "../selectors/axes.js";
4
+ import { axisGraphicIds } from "../selectors/axes.js";
5
+
6
+
7
+
8
+ import {findLayer} from '../selectors/layers.js';
9
+ import {findCoordinate} from '../selectors/coordinates.js';
10
+ import {resolveColorSizeMapping,normalizeColorSizeSymbol} from '../grammar/colorSizeLegend.js';
11
+ import{validateLegendClip}from"../grammar/legendClip.js";
12
+ import {validateLegendLayerOpacity}from"../grammar/legendOpacity.js";
13
+ import { isSizeShapeChannels, resolveSizeShapeMapping } from "../grammar/legendCombinations.js";
14
+ import { describeLegendBlock } from "../grammar/legendBlocks.js";
15
+ import { localLegendPosition } from "../grammar/legendGroups.js";
16
+ import { normalizeLegendBorderDash } from "../grammar/legendBorder.js";
17
+ import { normalizeNumericDisplayLabelMap } from "../grammar/displayLabels.js";
18
+ import { isContinuousSizeScaleType } from "../grammar/scales/size.js";
19
+ import { validateGraphicItemLimit } from "../core/validation.js";
20
+ import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
21
+ import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
22
+ import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
23
+ import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
24
+ import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
25
+ import { validateRegressionFitStatistics, requireRegressionStatisticsSource, regressionStatisticsRows } from "../grammar/regression/statistics.js";
26
+ import { resolveGraphicItemCapacity } from "../selectors/graphicCapacity.js";
27
+ import { isContinuousColorScaleType } from "../grammar/scales/types.js";
28
+ import { normalizeColorInterpolationOptions } from "../grammar/scales/color.js";
29
+ import { normalizeTransformParameters } from "../grammar/scales/transformed.js";
30
+ import { validateLegendGroupState } from "../grammar/legendGroups.js";
31
+ import { normalizeGradientDirection, validateLegendGradient } from "../grammar/legendGradient.js";
1
32
  import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
2
33
  import { isPlainObject } from "../core/immutable.js";
3
34
  import { validateUserId } from "../core/identifiers.js";
@@ -130,6 +161,12 @@ export function validateProgramState(program, path) {
130
161
  }
131
162
  resources.set(referenceKind, entries);
132
163
  for (const resource of entries.values()) {
164
+ if (kind === "scale") {
165
+ normalizeColorInterpolationOptions(resource.interpolate, resource);
166
+ if (resource.type === "pow") normalizeTransformParameters("pow", {
167
+ ...(resource.exponent === undefined ? {} : {exponent:resource.exponent})
168
+ });
169
+ }
133
170
  for (const [key, value] of Object.entries(resource)) {
134
171
  if (key !== "id") validateSemanticProperties(program, value, `${kind}[${resource.id}].${key}`);
135
172
  }
@@ -141,7 +178,178 @@ export function validateProgramState(program, path) {
141
178
  }
142
179
  for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
143
180
  for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
181
+ if (program.compositionSpec?.type === "facet" && isPlainObject(program.compositionSpec.facet.guides.legend)) {
182
+ validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
183
+ }
144
184
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
185
+ for (const config of [
186
+ ...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
187
+ ...["horizontal", "vertical"].map(direction => program.guideConfigs.grid?.[direction])
188
+ ]) {
189
+ if (config?.strokeDash !== undefined) validateGuideDash(config.strokeDash);
190
+ if (config?.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
191
+ }
192
+ for (const owner of cartesianAxisOwners(program)) {
193
+ const channel = owner.id;
194
+ const axis = owner.config;
195
+ if (axis?.frame !== undefined) {
196
+ exactKeys(axis.frame, ["offset"], `${path}.guides.axis.${channel}.frame`);
197
+ validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
198
+ }
199
+ if (axis?.labels !== undefined) validateAxisTextPlacement(axis.labels);
200
+ if (axis?.title !== undefined) validateAxisTextPlacement(axis.title, { title: true });
201
+ }
202
+ for (const id of Object.keys(program.guideConfigs.axis ?? {})) {
203
+ if (!AXIS_RESOURCE_CHANNELS.includes(id) && !Object.hasOwn(program.semanticSpec.guides.axis ?? {}, id)) {
204
+ throw invalidSnapshot("Named axis configs require their semantic owner.", path);
205
+ }
206
+ }
207
+ for (const [id, semanticAxis] of Object.entries(program.semanticSpec.guides.axis ?? {})) {
208
+ if (AXIS_RESOURCE_CHANNELS.includes(id)) continue;
209
+ validateUserId(id, "Axis id");
210
+ validateKeys(semanticAxis, ["channel", "position", "scale", "coordinate", "title"], "Stored named axis");
211
+ if (!["x", "y"].includes(semanticAxis.channel) ||
212
+ !(semanticAxis.channel === "x" ? ["bottom", "top"] : ["left", "right"]).includes(semanticAxis.position)) {
213
+ throw invalidSnapshot("Named axes require a Cartesian channel and compatible physical side.", path);
214
+ }
215
+ validateUserId(semanticAxis.scale, "Axis scale id");
216
+ validateUserId(semanticAxis.coordinate, "Axis coordinate id");
217
+ if (resources.get("coordinate").get(semanticAxis.coordinate)?.type !== "cartesian") {
218
+ throw invalidSnapshot("Named Cartesian axes require a Cartesian coordinate.", path);
219
+ }
220
+ const config = program.guideConfigs.axis?.[id];
221
+ requireObject(config, `${path}.guides.axis.${id}`);
222
+ validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title"], "Stored named axis config");
223
+ const components = ["line", "ticks", "labels", "title"];
224
+ if (!components.some(component => config[component] !== undefined)) {
225
+ throw invalidSnapshot("Named axes require at least one retained component.", path);
226
+ }
227
+ for (const [index, component] of components.entries()) {
228
+ const stored = config[component];
229
+ const graphic = program.graphicSpec.objects[axisGraphicIds(semanticAxis.channel, id)[index]];
230
+ if ((program.compositionSpec === undefined && (stored !== undefined) !== (graphic !== undefined)) ||
231
+ (stored !== undefined && (stored.position !== semanticAxis.position ||
232
+ stored.scale !== undefined && stored.scale !== semanticAxis.scale))) {
233
+ throw invalidSnapshot("Named axis components must share their owner's side and scale.", path);
234
+ }
235
+ }
236
+ if ((semanticAxis.title !== undefined) !== (config.title !== undefined)) {
237
+ throw invalidSnapshot("Named axis title meaning requires its retained title component.", path);
238
+ }
239
+ assertCartesianAxisSideAvailable(program, { id, channel: semanticAxis.channel,
240
+ coordinate: semanticAxis.coordinate, position: semanticAxis.position });
241
+ }
242
+ if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
243
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
244
+ validateLegendClip(kind,config);
245
+ if (localLegendPosition(config) !== undefined) {
246
+ const descriptor = describeLegendBlock(program, kind, config);
247
+ for (const [key, override] of Object.entries(config.blockOverrides)) {
248
+ if (override.position !== undefined && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
249
+ }
250
+ }
251
+ for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
252
+ if(kind==='colorSize') {
253
+ if(config.combine!=='color-size')throw invalidSnapshot('Color-size legend requires its combined owner.',path);
254
+ const layer=findLayer(program,config.target);
255
+ const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
256
+ for(const key of ['target','field','fieldType','colorScale','sizeScale','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
257
+ const guide=semantic.guides.legend?.colorSize;
258
+ if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
259
+ requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
260
+ if(config.symbol.type!=='point'||!Object.hasOwn(config.symbol,'strokeWidth')||!Object.hasOwn(config.symbol,'opacity'))throw invalidSnapshot('Color-size symbol requires point type.',path);
261
+ const sample=config.sampling;requireObject(sample,path);
262
+ exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
263
+ if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
264
+ validateGeneratedItemLimit(sample.count,'Color-size samples');
265
+ if(sample.mode==='values'){
266
+ requireArray(sample.values,path);
267
+ if(!sample.values.length||sample.values.length>100||sample.values.some((v,i)=>!Number.isFinite(v)||i>0&&v<=sample.values[i-1]||v<Math.min(...mapping.domain)||v>Math.max(...mapping.domain)||[mapping.colorScale,mapping.sizeScale].some(id=>program.resolvedScales[id].type==='log'&&v<=0)))throw invalidSnapshot('Invalid exact color-size samples.',path);
268
+ }
269
+ } else if (config.combine !== undefined) {
270
+ if (config.combine !== "size-shape" || kind !== "series" || !isSizeShapeChannels(config.channels)) throw invalidSnapshot("Invalid combined legend owner.", path);
271
+ const layer = semantic.layers.find(layer=>layer.id===config.target);
272
+ const definition = resolveSizeShapeMapping({layer,scales:program.resolvedScales,
273
+ order:semantic.guides.legend?.series?.order,rows:resolveDatasetReference(program,layer.data).values});
274
+ if (!Array.isArray(config.sizeAreas) || config.sizeAreas.length !== definition.sizeAreas.length ||
275
+ config.sizeAreas.some((area,index)=>!Object.is(area,definition.sizeAreas[index]))) throw invalidSnapshot("Stale combined legend size mapping.", path);
276
+ }
277
+ if (config.gradientDirection !== undefined) {
278
+ if (!["gradient", "strokeGradient"].includes(kind)) throw invalidSnapshot("Gradient direction requires a gradient owner.", path);
279
+ normalizeGradientDirection(config.gradientDirection, "Stored gradient direction");
280
+ }
281
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols" && !["top", "bottom"].includes(config.titlePosition)) {
282
+ throw invalidSnapshot("Gradient titlePosition requires top or bottom.", path);
283
+ }
284
+ if (config.border && Object.hasOwn(config.border, "strokeDash")) {
285
+ normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
286
+ }
287
+ if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
288
+ for (const [key, override] of Object.entries(config.blockOverrides)) {
289
+ if (override.labelMap === undefined) continue;
290
+ if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
291
+ throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
292
+ }
293
+ normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
294
+ }
295
+ }
296
+ if (config.titleStyle !== undefined) {
297
+ validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
298
+ validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
299
+ }
300
+ if (config.inferredOffset !== undefined && typeof config.inferredOffset !== "boolean") throw invalidSnapshot("Invalid legend offset inference.", path);
301
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
302
+ validateLegendGradient(config.gradient, "Stored legend gradient");
303
+ }
304
+ if (config.representation === undefined) continue;
305
+ if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
306
+ if (config.representation === "symbols") {
307
+ 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
+ }
320
+ requireObject(config.symbol, path);
321
+ validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
322
+ if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
323
+ validatePositiveFinite(config.symbol.radius, "Stored sampled color radius");
324
+ validateNonNegativeFinite(config.symbol.strokeWidth, "Stored sampled color strokeWidth");
325
+ if (config.symbol.stroke !== undefined) validateNonEmptyString(config.symbol.stroke, "Stored sampled color stroke");
326
+ 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
+ validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
328
+ 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);
330
+ }
331
+
332
+ const regressionFits = program.materializationConfigs.regressionFits ?? {};
333
+ requireObject(regressionFits, `${path}.materializationConfigs.regressionFits`);
334
+ for (const [id, cache] of Object.entries(regressionFits)) {
335
+ const dataset = resources.get("data").get(id);
336
+ validateRegressionFitStatistics(cache, dataset, resources.get("data").get(dataset?.source));
337
+ }
338
+ for (const dataset of semantic.datasets) {
339
+ if (dataset.transform?.[0]?.type !== "regressionStatistics") continue;
340
+ const fit = resources.get("data").get(dataset.source);
341
+ requireRegressionStatisticsSource(fit);
342
+ if (regressionFits[fit.id] === undefined) throw invalidSnapshot("Regression statistics requires its linear fit cache.", path);
343
+ if (dataset.values !== undefined) {
344
+ const expected = regressionStatisticsRows(regressionFits[fit.id], fit);
345
+ if (dataset.values.length !== expected.length || dataset.values.some((row, index) =>
346
+ !isPlainObject(row) || Object.keys(row).length !== Object.keys(expected[index]).length ||
347
+ Object.entries(expected[index]).some(([key, value]) => !Object.hasOwn(row, key) || !Object.is(row[key], value)))) {
348
+ throw invalidSnapshot("Regression statistics values do not match their linear fit cache.", path);
349
+ }
350
+ }
351
+ }
352
+
145
353
  const selections = program.materializationConfigs.selections ?? {};
146
354
  requireObject(selections, `${path}.materializationConfigs.selections`);
147
355
  for (const [id, selection] of Object.entries(selections)) {
@@ -161,12 +369,19 @@ export function validateProgramState(program, path) {
161
369
  requireObject(scale, `${path}.resolvedScales.${id}`);
162
370
  requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
163
371
  requireArray(scale.range, `${path}.resolvedScales.${id}.range`);
372
+ normalizeColorInterpolationOptions(scale.interpolate, scale);
373
+ if (scale.type === "pow") normalizeTransformParameters("pow", {
374
+ ...(scale.exponent === undefined ? {} : {exponent:scale.exponent})
375
+ });
164
376
  if (!resources.get("scale").has(id) || scale.type !== resources.get("scale").get(id).type) {
165
377
  throw invalidSnapshot(`Resolved scale "${id}" has no matching semantic owner/type.`, path);
166
378
  }
167
379
  }
168
380
  for (const id of Object.keys(program.markConfigs)) {
169
381
  if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
382
+ if (resources.get("mark").get(id).mark.type === "area") {
383
+ validateAreaPaintConfig(program.markConfigs[id]);
384
+ }
170
385
  }
171
386
  for (const kind of ["data", "mark"]) {
172
387
  const visited = new Set();
@@ -182,4 +397,9 @@ export function validateProgramState(program, path) {
182
397
  for (const resource of resources.get(kind).values()) visit(resource);
183
398
  }
184
399
  validateGraphic(program.graphicSpec, `${path}.graphicSpec`);
400
+ for (const [id, graphic] of Object.entries(program.graphicSpec.objects)) {
401
+ if (graphic.items !== undefined) validateGraphicItemLimit(graphic.items.length, resolveGraphicItemCapacity(program, {
402
+ id, label: "Graphic collection length"
403
+ }));
404
+ }
185
405
  }