ggaction 0.0.12 → 0.0.13

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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -1,161 +1,47 @@
1
- import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
- import {
3
- BAR_ORIENTATIONS,
4
- resolveBarChannels,
5
- resolveBarColorLayout
6
- } from "../../grammar/bars/policy.js";
7
- import {
8
- DEFAULT_SERIES_BASELINE,
9
- layoutSeriesPartition
10
- } from "../../grammar/seriesLayout.js";
11
- import {
12
- isDiscretePositionScaleType,
13
- mapContinuousScaleValues,
14
- mapOrdinalPositionValues
15
- } from "../../grammar/scales/index.js";
1
+ import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
2
+ import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
3
+ import { mapContinuousScaleValues, mapOrdinalPositionValues } from "../../grammar/scales/index.js";
16
4
  import { mapScaleConsumerValues } from "../scales/map.js";
17
- import {
18
- DEFAULT_BAR_FILL,
19
- resolveBarAppearance
20
- } from "./resolve.js";
21
- import { deriveGroupedRectangles } from "./grouped.js";
5
+ import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
22
6
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
7
+ import { finiteMidpoint } from "../../grammar/numeric.js";
23
8
 
24
9
  export function deriveAggregateRectangles(required, resolved, widthConfig) {
25
10
  const { dataset, layer } = required;
26
- const layout = resolveBarColorLayout(layer);
27
- if (layout === "group") {
28
- return deriveGroupedRectangles(required, resolved, widthConfig);
29
- }
30
-
31
- const xScale = resolved.resolvedScales[required.xEncoding.scale];
32
- const yScale = resolved.resolvedScales[required.yEncoding.scale];
33
- const channels = resolveBarChannels(layer);
34
- const vertical = channels.orientation === BAR_ORIENTATIONS.vertical;
35
- const categoryScale = vertical ? xScale : yScale;
36
- const measureScale = vertical ? yScale : xScale;
37
- const colorEncoding = layer.encoding?.color;
38
- const colorScale = resolved.resolvedScales[colorEncoding?.scale];
39
- if (colorEncoding !== undefined && colorScale === undefined) {
40
- throw new Error(
41
- `Bar mark "${layer.id}" requires a resolved color scale.`
42
- );
43
- }
44
-
45
- const seriesDomain = colorScale?.domain ?? [undefined];
46
- const seriesIndex = new Map(
47
- seriesDomain.map((value, index) => [value, index])
48
- );
49
- const colors = new Map(seriesDomain.map((value, index) => [
50
- value,
51
- colorScale?.range[index % colorScale.range.length] ?? DEFAULT_BAR_FILL
52
- ]));
53
- const cells = deriveBarAggregates(dataset.values, layer).values;
54
- const discreteCategory = categoryScale.type === "ordinal" ||
55
- isDiscretePositionScaleType(categoryScale.type);
56
- const categoryDomain = discreteCategory
57
- ? categoryScale.domain
58
- : [...new Set(cells.map(cell => cell[channels.category]))]
59
- .sort((left, right) => left - right);
60
- const categoryIndex = new Map(
61
- categoryDomain.map((value, index) => [value, index])
62
- );
63
- const cellMap = new Map(cells.map(cell => [
64
- JSON.stringify([cell[channels.category], cell.color]),
65
- cell
66
- ]));
67
- const thickness = resolveBarWidth(
68
- widthConfig,
69
- Math.abs(categoryScale.bandwidth ?? categoryScale.step)
70
- );
71
- const baseline = DEFAULT_SERIES_BASELINE;
72
- const continuousColor = colorEncoding?.fieldType === "quantitative";
73
- if (continuousColor) {
74
- const mappedColors = mapScaleConsumerValues(
75
- cells.map(cell => cell.color), colorScale, "color"
76
- );
77
- const cellByCategory = new Map(
78
- cells.map((cell, index) => [cell[channels.category], { cell, index }])
79
- );
80
- const existing = resolved.graphicSpec.objects[layer.id].items;
81
- const config = resolved.markConfigs[layer.id] ?? {};
82
- const appearance = config.barAppearance ?? {};
83
- return categoryDomain.flatMap(categoryValue => {
84
- const entry = cellByCategory.get(categoryValue);
85
- if (entry === undefined) return [];
86
- const { cell, index } = entry;
87
- const categoryCenter = discreteCategory
88
- ? mapOrdinalPositionValues([categoryValue], categoryScale)[0]
89
- : mapContinuousScaleValues(
90
- [categoryValue],
91
- categoryScale
92
- )[0];
93
- const [segment] = layoutSeriesPartition(
94
- [cell[channels.measure]],
95
- layout,
96
- { baseline }
97
- );
98
- if (segment === undefined) return [];
99
- const [start, end] = mapContinuousScaleValues(
100
- [segment.start, segment.end],
101
- measureScale
102
- );
103
- return [{
104
- x: vertical ? categoryCenter - thickness / 2 : Math.min(start, end),
105
- y: vertical ? Math.min(start, end) : categoryCenter - thickness / 2,
106
- width: vertical ? thickness : Math.abs(start - end),
107
- height: vertical ? Math.abs(start - end) : thickness,
108
- fill: mappedColors[index],
109
- ...resolveBarAppearance(config, existing[index]?.properties)
110
- }];
111
- });
112
- }
113
- const segments = [];
114
-
115
- for (const category of categoryDomain) {
116
- const partition = seriesDomain.map(color =>
117
- cellMap.get(JSON.stringify([category, color]))?.[channels.measure] ?? 0
118
- );
119
- for (const segment of layoutSeriesPartition(partition, layout, { baseline })) {
120
- const color = seriesDomain[segment.index];
121
- const cell = cellMap.get(JSON.stringify([category, color]));
122
- if (cell === undefined) continue;
123
- segments.push({ category, color, ...segment });
124
- }
125
- }
126
-
127
- const existing = resolved.graphicSpec.objects[layer.id].items;
11
+ const channels = resolveBarChannels(layer), vertical = channels.orientation === "vertical";
12
+ const categoryScale = resolved.resolvedScales[layer.encoding[channels.category].scale];
13
+ const measureScale = resolved.resolvedScales[layer.encoding[channels.measure].scale];
14
+ const color = layer.encoding.color, colorScale = resolved.resolvedScales[color?.scale];
15
+ const grouped = resolveBarColorLayout(layer) === "group";
16
+ const offsetScale = resolved.resolvedScales[layer.encoding[resolveBarOffsetChannel(layer)]?.scale];
17
+ if (grouped && offsetScale === undefined) throw new Error("Grouped bar requires a resolved offset scale.");
18
+ const temporal = categoryScale.type === "time";
19
+ const segments = deriveSeriesBarCells(dataset.values, layer, temporal ? undefined : categoryScale.domain,
20
+ grouped ? offsetScale.domain : layer.encoding.group === undefined && color?.fieldType !== "quantitative" ? colorScale?.domain : undefined);
21
+ const fills = color === undefined ? undefined : mapScaleConsumerValues(segments.map(segment => segment.cell.color), colorScale, "color");
128
22
  const config = resolved.markConfigs[layer.id] ?? {};
129
- const appearance = config.barAppearance ?? {};
130
- return segments.map((segment, index) => {
131
- const category = categoryIndex.get(segment.category);
132
- const color = seriesIndex.get(segment.color);
133
- if (category === undefined || color === undefined) {
134
- throw new Error("Bar value is outside a resolved ordinal domain.");
23
+ const existing = resolved.graphicSpec.objects[layer.id].items;
24
+ const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
25
+ const indices = new Map(categoryScale.domain.map((value, index) => [value, index]));
26
+ return segments.map(({ cell, start, end, seriesIndex }, index) => {
27
+ const categoryValue = cell[channels.category];
28
+ const categoryCenter = (temporal ? mapContinuousScaleValues : mapOrdinalPositionValues)([categoryValue], categoryScale)[0];
29
+ let position = !grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
30
+ ? (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step + categoryScale.bandwidth / 2 - width / 2
31
+ : categoryCenter - width / 2;
32
+ if (grouped) {
33
+ const direction = Math.sign(categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]) || 1;
34
+ const offsetCenter = offsetScale.start + seriesIndex * offsetScale.step + (Math.sign(offsetScale.step) || 1) * offsetScale.bandwidth / 2;
35
+ if (vertical && direction > 0 && offsetScale.step > 0) {
36
+ const categoryStart = temporal ? categoryCenter - categoryScale.bandwidth / 2
37
+ : (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step;
38
+ position = categoryStart + offsetScale.start + seriesIndex * offsetScale.step + (offsetScale.bandwidth - width) / 2;
39
+ } else position = categoryCenter + direction * (offsetCenter - finiteMidpoint(...offsetScale.range)) - width / 2;
135
40
  }
136
- const categoryCenter = discreteCategory
137
- ? mapOrdinalPositionValues([segment.category], categoryScale)[0]
138
- : mapContinuousScaleValues(
139
- [segment.category],
140
- categoryScale
141
- )[0];
142
- const [start, end] = mapContinuousScaleValues(
143
- [segment.start, segment.end],
144
- measureScale
145
- );
146
- return {
147
- x: vertical
148
- ? categoryCenter - thickness / 2
149
- : Math.min(start, end),
150
- y: vertical
151
- ? Math.min(start, end)
152
- : categoryCenter - thickness / 2,
153
- width: vertical ? thickness : Math.abs(start - end),
154
- height: vertical ? Math.abs(start - end) : thickness,
155
- fill: segment.color === undefined
156
- ? appearance.fill ?? config.fill ?? colors.get(segment.color)
157
- : colors.get(segment.color),
158
- ...resolveBarAppearance(config, existing[index]?.properties)
159
- };
41
+ const [a, b] = mapContinuousScaleValues([start, end], measureScale);
42
+ return { x: vertical ? position : Math.min(a, b), y: vertical ? Math.min(a, b) : position,
43
+ width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
44
+ fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
45
+ ...resolveBarAppearance(config, existing[index]?.properties) };
160
46
  });
161
47
  }
@@ -1,3 +1,4 @@
1
+ import { readSeriesIdentity } from "../../grammar/pathSeries.js";
1
2
  import {
2
3
  countHistogramBins,
3
4
  findHistogramBinIndex,
@@ -34,6 +35,29 @@ export function deriveHistogramSegments({
34
35
  const colorEncoding = layer.encoding?.color;
35
36
  const layout = resolveBarColorLayout(layer);
36
37
 
38
+ if (layer.encoding?.group !== undefined) {
39
+ const identity = readSeriesIdentity(dataset.values, layer);
40
+ const cellRows = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
41
+ const index = new Map(identity.domain.map((value, i) => [value, i]));
42
+ xValues.forEach((value, i) => { const bin = findHistogramBinIndex(value, bins.boundaries);
43
+ if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(dataset.values[i]); });
44
+ return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(rows => rows.length), layout).map(segment => {
45
+ const members = cells[segment.index];
46
+ let color, colorValue;
47
+ if (colorEncoding !== undefined) {
48
+ const values = readNominalField(members, colorEncoding.field);
49
+ if (new Set(values).size !== 1) throw new Error("Histogram color requires one value within each bin/series cell.");
50
+ colorValue = values[0];
51
+ const scale = resolvedScales[colorEncoding.scale];
52
+ color = mapOrdinalValues([colorValue], scale.domain, scale.range)[0];
53
+ }
54
+ return { bin, start: bins.boundaries[bin], end: bins.boundaries[bin + 1],
55
+ category: segment.index, categoryCount: identity.domain.length,
56
+ stackStart: segment.start, stackEnd: segment.end, members,
57
+ ...(color === undefined ? {} : { color, colorValue }) };
58
+ }));
59
+ }
60
+
37
61
  if (colorEncoding?.scale === undefined) {
38
62
  return countHistogramBins(xValues, bins.boundaries).flatMap((count, bin) =>
39
63
  layoutSeriesPartition([count], layout).map(segment => ({
@@ -0,0 +1,40 @@
1
+ export function resolveCategorySlotOffset(program, layer) {
2
+ const policy = program.markConfigs[layer.id]?.categorySlotOffset;
3
+ if (policy === undefined) return undefined;
4
+ if (!["x", "y"].includes(policy.channel)) {
5
+ throw new Error(`Category slot offset for "${layer.id}" requires channel x or y.`);
6
+ }
7
+ if (!Number.isFinite(policy.band) || policy.band < -0.5 || policy.band > 0.5) {
8
+ throw new RangeError(`Category slot offset for "${layer.id}" must be within [-0.5, 0.5] band.`);
9
+ }
10
+ const encoding = layer.encoding?.[policy.channel];
11
+ if (!["nominal", "ordinal"].includes(encoding?.fieldType)) {
12
+ throw new Error(`Category slot offset for "${layer.id}" requires a categorical position.`);
13
+ }
14
+ const scale = program.resolvedScales[encoding.scale];
15
+ if (scale?.type !== "band" || !Number.isFinite(scale.bandwidth) || scale.bandwidth <= 0) {
16
+ throw new Error(`Category slot offset for "${layer.id}" requires a positive band scale.`);
17
+ }
18
+ return { channel: policy.channel, pixels: scale.bandwidth * policy.band };
19
+ }
20
+
21
+ export function offsetCategoryPositions(program, layer, positions) {
22
+ const offset = resolveCategorySlotOffset(program, layer);
23
+ if (offset === undefined || offset.pixels === 0) return positions;
24
+ const shift = values => values === undefined
25
+ ? undefined
26
+ : values.map(value => Number.isFinite(value) ? value + offset.pixels : value);
27
+ return {
28
+ ...positions,
29
+ [offset.channel]: shift(positions[offset.channel])
30
+ };
31
+ }
32
+
33
+ export function offsetCategoryRectangles(program, layer, rectangles) {
34
+ const offset = resolveCategorySlotOffset(program, layer);
35
+ if (offset === undefined || offset.pixels === 0) return rectangles;
36
+ return rectangles.map(rectangle => ({
37
+ ...rectangle,
38
+ [offset.channel]: rectangle[offset.channel] + offset.pixels
39
+ }));
40
+ }
@@ -32,7 +32,7 @@ export function namespaceGraphicId(namespace, id) {
32
32
  const encode = value => Array.from(value, character =>
33
33
  character.codePointAt(0).toString(16).padStart(6, "0")
34
34
  ).join("");
35
- return `g${encode(namespace)}_${encode(id)}`;
35
+ return `g${encode(namespace)}_${id}`;
36
36
  }
37
37
 
38
38
  function rewriteItem(item, namespace) {
@@ -1,12 +1,16 @@
1
+ import { withGuideLayoutTransaction } from "./guides/layout.js";
1
2
  import {
2
3
  canDeferScaleConsumerApplication,
4
+ getLayerScaleIds,
3
5
  getExistingMarkRematerializationStep,
4
6
  getMarkMaterializationStep,
5
- getSourceDependentMarkSteps
7
+ getScaleConsumerMarkSteps,
8
+ getSourceDependentMarkSteps,
9
+ reusePlannedMarkScales
6
10
  } from "./marks/index.js";
7
- import { requireLayer } from "../selectors/layers.js";
11
+ import { findLayer, requireLayer } from "../selectors/layers.js";
8
12
  import {
9
- applyMaterializationPlan,
13
+ applyMaterializationPlan as executeMaterializationPlan,
10
14
  buildMaterializationPlan
11
15
  } from "./planner.js";
12
16
  import { hasMaterializedLegend } from "./legends.js";
@@ -16,13 +20,6 @@ import {
16
20
  } from "./scaleGuideDependencies.js";
17
21
  import { planLayoutRematerialization } from "./layout.js";
18
22
 
19
- function layerScaleIds(layer) {
20
- return [
21
- ...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
22
- ...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
23
- ].filter(scale => scale !== undefined);
24
- }
25
-
26
23
  export function planCanvasRematerialization(program) {
27
24
  const marks = [];
28
25
  for (const layer of program.semanticSpec.layers) {
@@ -38,7 +35,7 @@ export function planCanvasRematerialization(program) {
38
35
  if (needsCanvasScaleRematerialization(program, scale)) {
39
36
  const deferredConsumers = program.semanticSpec.layers.filter(layer =>
40
37
  canDeferScaleConsumerApplication(layer) &&
41
- layerScaleIds(layer).includes(scale.id)
38
+ getLayerScaleIds(layer).includes(scale.id)
42
39
  );
43
40
  const canDeferMarks = deferredConsumers.length === 0 ||
44
41
  deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
@@ -66,7 +63,7 @@ export function planCanvasRematerialization(program) {
66
63
 
67
64
  export function planLayerDataRematerialization(program, id) {
68
65
  const layer = requireLayer(program, id);
69
- const scaleIds = [...new Set(layerScaleIds(layer))];
66
+ const scaleIds = [...new Set(getLayerScaleIds(layer))];
70
67
  const markStep = getMarkMaterializationStep(program, layer);
71
68
  const scales = scaleIds.map(scale => ({
72
69
  op: "rematerializeScale",
@@ -76,15 +73,19 @@ export function planLayerDataRematerialization(program, id) {
76
73
  ...(markStep === undefined ? {} : { marks: false })
77
74
  }
78
75
  }));
79
- const marks = [
76
+ const directMarks = [
80
77
  ...(markStep === undefined ? [] : [markStep]),
81
- ...getSourceDependentMarkSteps(program, id)
78
+ ...getScaleConsumerMarkSteps(program, scaleIds)
79
+ ];
80
+ const marks = [
81
+ ...directMarks,
82
+ ...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
82
83
  ];
83
84
  if (scales.length > 0 || marks.length > 0) {
84
85
  const guides = scaleIds.flatMap(scale =>
85
86
  planScaleGuideRematerialization(program, scale)
86
87
  );
87
- return buildMaterializationPlan({ scales, marks, guides });
88
+ return buildMaterializationPlan({ scales, marks: reusePlannedMarkScales(program, marks, scaleIds), guides });
88
89
  }
89
90
  const existingStep = getExistingMarkRematerializationStep(program, layer);
90
91
  return buildMaterializationPlan({
@@ -99,5 +100,45 @@ export function applyLayerDataRematerialization(program, id) {
99
100
  );
100
101
  }
101
102
 
102
- export { applyMaterializationPlan } from "./planner.js";
103
+ export function applyLayerEmptyDataView(program, id) {
104
+ requireLayer(program, id);
105
+ const targets = [
106
+ id,
107
+ ...getSourceDependentMarkSteps(program, id).map(step => step.args.id)
108
+ ];
109
+ let next = program;
110
+ for (const target of new Set(targets)) {
111
+ const graphic = next.graphicSpec.objects[target];
112
+ if (graphic === undefined) continue;
113
+ next = graphic.type === "collection"
114
+ ? next.editGraphics({ target, property: "items", value: [] })
115
+ : next.editGraphics({ target, property: "length", value: 0 });
116
+ }
117
+ return next;
118
+ }
119
+
120
+ export function applyDetachedScaleRematerialization(program, previousLayers) {
121
+ const retained = new Set(program.semanticSpec.layers.flatMap(getLayerScaleIds));
122
+ const scaleIds = [...new Set(previousLayers.flatMap(layer => {
123
+ const current = findLayer(program, layer.id);
124
+ const currentIds = current === undefined ? [] : getLayerScaleIds(current);
125
+ return getLayerScaleIds(layer).filter(id => !currentIds.includes(id) && retained.has(id));
126
+ }))];
127
+ if (scaleIds.length === 0) return program;
128
+ const directMarks = getScaleConsumerMarkSteps(program, scaleIds);
129
+ return applyMaterializationPlan(program, buildMaterializationPlan({
130
+ scales: scaleIds.map(id => ({
131
+ op: "rematerializeScale", args: { id, guides: false, marks: false }
132
+ })),
133
+ marks: [
134
+ ...directMarks,
135
+ ...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
136
+ ],
137
+ guides: scaleIds.flatMap(id => planScaleGuideRematerialization(program, id))
138
+ }));
139
+ }
140
+
141
+ export function applyMaterializationPlan(program, plan) {
142
+ return withGuideLayoutTransaction(program, next => executeMaterializationPlan(next, plan));
143
+ }
103
144
  export { planScaleGuideRematerialization };
@@ -25,7 +25,7 @@ export function planEncodingRematerialization(program, {
25
25
  );
26
26
  const marks = [
27
27
  ...directMarks,
28
- ...getSourceDependentMarkSteps(program, target)
28
+ ...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
29
29
  ];
30
30
 
31
31
  const guides = [];
@@ -162,10 +162,17 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
162
162
  }
163
163
  const title = titleLayout(program);
164
164
  const spec = program.compositionSpec;
165
+ const gridCells = new Map(
166
+ (spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
167
+ );
165
168
  const layout = resolveFacetLayout({
166
169
  children: spec.children.map((id, index) => ({
167
170
  ...compositionChildDescriptor(id, program.children[id]),
168
- value: spec.facet.values[index]
171
+ value: spec.facet.values[index],
172
+ ...(gridCells.has(id) ? {
173
+ row: gridCells.get(id).row,
174
+ column: gridCells.get(id).column
175
+ } : {})
169
176
  })),
170
177
  columns: spec.columns,
171
178
  gap: spec.gap,
@@ -0,0 +1,57 @@
1
+ import { assertGuideCollisionBlocks } from "../../layout/guideCollisions.js";
2
+ import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
3
+ import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
4
+
5
+ // Only domain-owned guides participate. Extension primitives may deliberately
6
+ // overlap, and a combined categorical/size legend is one occupied group.
7
+ export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
8
+ const blocks = [];
9
+ const append = (id, kind, position, ids) => {
10
+ if (position === undefined) return;
11
+ const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
12
+ const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
13
+ if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
14
+ };
15
+ append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
16
+ for (const channel of ["x", "y"]) {
17
+ for (const id of axisGraphicIds(channel)) {
18
+ const component = id.slice(`${channel}Axis`.length).toLowerCase();
19
+ append(`${channel}-axis ${component}`, "axis", guideConfigs.axis?.[channel]?.[component]?.position, [id]);
20
+ }
21
+ }
22
+ const policies = legendResourcePolicies();
23
+ const configs = guideConfigs.legend ?? {};
24
+ const categorical = policies.find(policy => policy.family === "categorical" && configs[policy.kind] !== undefined);
25
+ const combined = categorical !== undefined && configs.size !== undefined &&
26
+ configs[categorical.kind].target === configs.size.target;
27
+ for (const policy of policies) {
28
+ const config = configs[policy.kind];
29
+ if (config === undefined || combined && policy.kind === "size") continue;
30
+ const ids = combined && policy === categorical
31
+ ? [...policy.graphicIds, ...policies.find(item => item.kind === "size").graphicIds]
32
+ : policy.graphicIds;
33
+ append(`${policy.kind}${combined && policy === categorical ? "+size" : ""} legend`, "legend", config.position, ids);
34
+ }
35
+ return blocks;
36
+ }
37
+
38
+
39
+ // Defer cross-guide checks while sibling materializers still carry old
40
+ // coordinates. The scope is transient and never survives a returned program.
41
+ export function withGuideLayoutTransaction(program, apply) {
42
+ if (program.context.deferGuideLayoutValidation === true) return apply(program);
43
+ const result = apply(program._withContext({ deferGuideLayoutValidation: true }));
44
+ const { deferGuideLayoutValidation: _deferred, ...context } = result.context;
45
+ if (Object.hasOwn(program.context, "deferGuideLayoutValidation")) {
46
+ context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
47
+ }
48
+ const next = result._clone({ context });
49
+ assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
50
+ return next;
51
+ }
52
+
53
+ export function withGuideLayoutValidation(implementation) {
54
+ return function (args) {
55
+ return withGuideLayoutTransaction(this, program => implementation.call(program, args));
56
+ };
57
+ }
@@ -27,7 +27,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
27
27
  family: "size",
28
28
  rematerializeOp: "rematerializeSizeLegend",
29
29
  graphicIds: Object.freeze([
30
- "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
30
+ "sizeLegendBackground", "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
31
31
  ])
32
32
  }),
33
33
  gradient: Object.freeze({
@@ -62,7 +62,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
62
62
  family: "strokeWidth",
63
63
  rematerializeOp: "rematerializeStrokeWidthLegend",
64
64
  graphicIds: Object.freeze([
65
- "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
65
+ "strokeWidthLegendBackground", "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
66
66
  "strokeWidthLegendTitle"
67
67
  ])
68
68
  })
@@ -1,5 +1,11 @@
1
+ import { findLayer } from "../selectors/layers.js";
1
2
  import { LEGEND_CONFIG_KINDS } from "../core/vocabulary.js";
2
3
 
4
+ export function isOpacityLegendLayer(layer) {
5
+ return ["point", "line"].includes(layer.mark?.type) &&
6
+ layer.encoding?.opacity?.scale !== undefined;
7
+ }
8
+
3
9
  export function hasMaterializedLegend(program) {
4
10
  return LEGEND_CONFIG_KINDS.some(kind => {
5
11
  const config = program.guideConfigs?.legend?.[kind];
@@ -15,6 +21,9 @@ export function materializedLegendUsesScale(program, id) {
15
21
  return LEGEND_CONFIG_KINDS.some(kind => {
16
22
  const config = program.guideConfigs?.legend?.[kind];
17
23
  if (config === undefined) return false;
18
- return config.scale === id || config.scales?.includes(id);
24
+ const order = program.semanticSpec.guides.legend?.[kind]?.order;
25
+ const linkedScale = order?.channel === undefined ? undefined
26
+ : findLayer(program, config.target)?.encoding?.[order.channel]?.scale;
27
+ return config.scale === id || config.scales?.includes(id) || linkedScale === id;
19
28
  });
20
29
  }
@@ -145,9 +145,9 @@ export function canMaterializeBar(program, layer) {
145
145
  ) {
146
146
  const offsetChannel = resolveBarOffsetChannel(layer);
147
147
  return (
148
- layer.encoding?.color?.field !== undefined &&
149
- layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
150
- layer.encoding[offsetChannel].scale !== undefined
148
+ (layer.encoding?.group !== undefined || (layer.encoding?.color?.field !== undefined &&
149
+ layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field)) &&
150
+ layer.encoding[offsetChannel]?.scale !== undefined
151
151
  );
152
152
  }
153
153
  return true;
@@ -163,14 +163,29 @@ export function canMaterializeRule(program, layer) {
163
163
  );
164
164
  }
165
165
 
166
+ const TEXT_SOURCE_CAPABILITIES = Object.freeze({
167
+ point: canMaterializePoint,
168
+ line: canMaterializeLine,
169
+ bar: canMaterializeBar,
170
+ rule: canMaterializeRule,
171
+ rect: (program, layer) => canMaterializeRect(program, layer) ||
172
+ program.markConfigs[layer.id]?.gradientPlot?.materialized === true,
173
+ arc: canMaterializeArc
174
+ });
175
+
176
+ export function isTextSource(layer) {
177
+ return layer?.data !== undefined &&
178
+ Object.hasOwn(TEXT_SOURCE_CAPABILITIES, layer.mark?.type);
179
+ }
180
+
166
181
  export function canMaterializeText(program, layer) {
167
182
  if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
168
183
  return false;
169
184
  }
170
185
  if (layer.source !== undefined) {
171
186
  const source = findLayer(program, layer.source);
172
- return source !== undefined &&
173
- ["point", "bar", "rule", "rect", "arc"].includes(source.mark?.type) &&
187
+ return isTextSource(source) &&
188
+ TEXT_SOURCE_CAPABILITIES[source.mark.type](program, source) &&
174
189
  Array.isArray(program.graphicSpec.objects[source.id]?.items);
175
190
  }
176
191
  return hasCartesianPositionScales(layer);
@@ -0,0 +1,52 @@
1
+ import { findUpstreamTransform } from "../dataProvenance.js";
2
+ import { readPathSeriesFields, validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
3
+
4
+ export function validatePathGroupAppearance(program, layer, dataset) {
5
+ if (layer.mark.type === "area" && findUpstreamTransform(program, dataset, "density")) {
6
+ // Density placement owns a category/split grain beyond the visible group field.
7
+ readPathSeriesFields(dataset.values, layer);
8
+ return;
9
+ }
10
+ validatePathSeriesAppearance(dataset.values, layer);
11
+ }
12
+
13
+ export function assertPathGroupCompatible(program, layer, dataset, group) {
14
+ if (layer.encoding?.parallel !== undefined) {
15
+ throw new Error("Parallel paths have row identity and do not support encodeGroup.");
16
+ }
17
+ const field = group?.field;
18
+ for (const [type, editor] of [
19
+ ["density", "editDensity"],
20
+ ["horizon", "editHorizon"],
21
+ ["regression", "editRegression"],
22
+ ["interval", "editErrorBand"]
23
+ ]) {
24
+ const transform = findUpstreamTransform(program, dataset, type);
25
+ if (transform === undefined) continue;
26
+ const fields = type === "horizon" ? [transform.as.group]
27
+ : type === "interval"
28
+ ? transform.groupBy.filter(value =>
29
+ value !== layer.encoding?.x?.field && value !== layer.encoding?.y?.field
30
+ )
31
+ : [transform.groupBy];
32
+ if (group?.fields !== undefined || group === undefined ||
33
+ field === undefined || !fields.includes(field)) {
34
+ throw new Error(
35
+ `${type === "density" ? "Density area" : type + " path"} mark "${layer.id}" must group by ${fields.length === 1 && fields[0] !== undefined ? JSON.stringify(fields[0]) : "its owned field"}; use ${editor}.`
36
+ );
37
+ }
38
+ }
39
+ if (layer.layout?.mode !== undefined) {
40
+ if (group === undefined && !["overlay"].includes(layer.layout.mode)) throw new Error("Remove active series layout before removing its group.");
41
+ return;
42
+ }
43
+ if (layer.mark?.type === "area" && (
44
+ layer.encoding?.y?.stack === "center" ||
45
+ ![undefined, "overlay"].includes(layer.encoding?.color?.layout)
46
+ )) {
47
+ if (group?.fields !== undefined ||
48
+ (layer.encoding?.color !== undefined && field !== layer.encoding.color.field)) {
49
+ throw new Error("Area layout requires one group field matching its color field.");
50
+ }
51
+ }
52
+ }