ggaction 0.0.11 → 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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/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 +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  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 +79 -24
  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 +153 -11
  83. package/src/actions/errorBars/edit.js +365 -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 +138 -14
  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 +60 -4
  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/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -17,12 +17,30 @@ function requireArcLayer(layer) {
17
17
  throw new Error("Arc derivation requires a semantic arc mark.");
18
18
  }
19
19
  const theta = layer.encoding?.theta;
20
- if (theta === undefined || !["nominal", "ordinal"].includes(theta.fieldType)) {
21
- throw new Error(`Arc mark "${layer.id}" requires categorical theta.`);
20
+ if (
21
+ theta === undefined ||
22
+ !["quantitative", "nominal", "ordinal"].includes(theta.fieldType)
23
+ ) {
24
+ throw new Error(
25
+ `Arc mark "${layer.id}" requires quantitative or categorical theta.`
26
+ );
22
27
  }
23
28
  return theta;
24
29
  }
25
30
 
31
+ function requireContinuousThetaScale(scale, label) {
32
+ if (
33
+ scale?.type !== "linear" ||
34
+ !Array.isArray(scale.range) ||
35
+ scale.range.length !== 2 ||
36
+ !scale.range.every(Number.isFinite) ||
37
+ scale.range[0] === scale.range[1]
38
+ ) {
39
+ throw new Error(`${label} requires a resolved linear scale.`);
40
+ }
41
+ return scale;
42
+ }
43
+
26
44
  function requireBandScale(scale, label) {
27
45
  if (
28
46
  scale?.type !== "band" ||
@@ -45,24 +63,90 @@ function colorValues(rows, encoding) {
45
63
  : readNominalField(rows, encoding.field);
46
64
  }
47
65
 
48
- export function readArcThetaWeights(rows, field, layerId = "arc") {
49
- if (!Array.isArray(rows)) throw new TypeError("Arc theta weights require rows.");
66
+ function readNonNegativeArcThetaValues(rows, field, layerId, role) {
67
+ if (!Array.isArray(rows)) throw new TypeError(`Arc theta ${role}s require rows.`);
50
68
  if (typeof field !== "string" || field.length === 0) {
51
- throw new TypeError("Arc theta weight must be a non-empty string.");
69
+ throw new TypeError(`Arc theta ${role} must be a non-empty string.`);
52
70
  }
53
- const weights = rows.map((row, index) => {
71
+ const values = rows.map((row, index) => {
54
72
  const value = row?.[field];
55
73
  if (!Number.isFinite(value) || value < 0) {
56
74
  throw new TypeError(
57
- `Arc theta weight field "${field}" must contain non-negative finite numbers at row ${index}.`
75
+ `Arc theta ${role} field "${field}" must contain non-negative finite numbers at row ${index}.`
58
76
  );
59
77
  }
60
78
  return value;
61
79
  });
62
- if (weights.every(value => value === 0)) {
63
- throw new Error(`Arc mark "${layerId}" theta weights must have a positive total.`);
80
+ if (values.every(value => value === 0)) {
81
+ throw new Error(`Arc mark "${layerId}" theta ${role}s must have a positive total.`);
64
82
  }
65
- return cloneAndFreeze(weights);
83
+ return cloneAndFreeze(values);
84
+ }
85
+
86
+ export function readArcThetaWeights(rows, field, layerId = "arc") {
87
+ return readNonNegativeArcThetaValues(rows, field, layerId, "weight");
88
+ }
89
+
90
+ export function readArcThetaValues(rows, field, layerId = "arc") {
91
+ return readNonNegativeArcThetaValues(rows, field, layerId, "value");
92
+ }
93
+
94
+ function resolveProportionalRanges(values, range, labelAt) {
95
+ const total = values.reduce((sum, value) => sum + value, 0);
96
+ const normalizedTotal = Number.isFinite(total)
97
+ ? undefined
98
+ : normalizedFiniteSum(values, "Arc theta values");
99
+ if (total === 0 || normalizedTotal?.total <= 0) {
100
+ throw new Error("Arc theta values must have a positive total.");
101
+ }
102
+ const span = range[1] - range[0];
103
+ let cursor = range[0];
104
+ let cumulative = 0;
105
+ let cumulativeCorrection = 0;
106
+ return values.map((value, index) => {
107
+ const normalizedValue = normalizedTotal === undefined
108
+ ? value
109
+ : value / normalizedTotal.scale;
110
+ const next = cumulative + normalizedValue;
111
+ if (normalizedTotal !== undefined) {
112
+ cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
113
+ ? cumulative - next + normalizedValue
114
+ : normalizedValue - next + cumulative;
115
+ }
116
+ cumulative = next;
117
+ const directEnd = cursor + value / total * span;
118
+ const endTheta = index === values.length - 1
119
+ ? range[1]
120
+ : normalizedTotal === undefined && Number.isFinite(directEnd)
121
+ ? directEnd
122
+ : interpolateNumber(
123
+ range[0],
124
+ range[1],
125
+ (cumulative + cumulativeCorrection) /
126
+ (normalizedTotal?.total ?? total)
127
+ );
128
+ if (!Number.isFinite(endTheta) || endTheta === cursor) {
129
+ throw new RangeError(
130
+ `Arc theta ${labelAt(index)} cannot resolve a distinct finite sector.`
131
+ );
132
+ }
133
+ const result = { startTheta: cursor, endTheta };
134
+ cursor = endTheta;
135
+ return result;
136
+ });
137
+ }
138
+
139
+ function resolveProportionalRadii(layer, frame, innerRadiusRatio) {
140
+ if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
141
+ throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
142
+ }
143
+ return {
144
+ innerRadius: requireFiniteResult(
145
+ frame.availableRadius * innerRadiusRatio,
146
+ `Arc mark "${layer.id}" inner radius`
147
+ ),
148
+ outerRadius: frame.availableRadius
149
+ };
66
150
  }
67
151
 
68
152
  function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
@@ -87,14 +171,7 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
87
171
  weight: weights[index]
88
172
  });
89
173
  }
90
- if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
91
- throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
92
- }
93
- const innerRadius = requireFiniteResult(
94
- frame.availableRadius * innerRadiusRatio,
95
- `Arc mark "${layer.id}" inner radius`
96
- );
97
- let cursor = thetaScale.range[0];
174
+ const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
98
175
  const sectors = [];
99
176
  const positiveValues = thetaScale.domain.filter(value =>
100
177
  groups.get(value).some(item => item.weight > 0)
@@ -103,48 +180,15 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
103
180
  groups.get(value).map(item => item.weight),
104
181
  `Arc theta group "${String(value)}" aggregate`
105
182
  ));
106
- const total = aggregateValues.reduce((sum, value) => sum + value, 0);
107
- const normalizedTotal = Number.isFinite(total)
108
- ? undefined
109
- : normalizedFiniteSum(
110
- aggregateValues,
111
- `Arc mark "${layer.id}" theta aggregates`
112
- );
113
- if (total === 0 || normalizedTotal?.total <= 0) {
114
- throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
115
- }
116
- const span = thetaScale.range[1] - thetaScale.range[0];
117
- let cumulative = 0;
118
- let cumulativeCorrection = 0;
183
+ const ranges = resolveProportionalRanges(
184
+ aggregateValues,
185
+ thetaScale.range,
186
+ index => `group "${String(positiveValues[index])}"`
187
+ );
119
188
  for (const [positiveIndex, value] of positiveValues.entries()) {
120
189
  const group = groups.get(value);
121
190
  const aggregateValue = aggregateValues[positiveIndex];
122
- const normalizedValue = normalizedTotal === undefined
123
- ? aggregateValue
124
- : aggregateValue / normalizedTotal.scale;
125
- const next = cumulative + normalizedValue;
126
- if (normalizedTotal !== undefined) {
127
- cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
128
- ? cumulative - next + normalizedValue
129
- : normalizedValue - next + cumulative;
130
- }
131
- cumulative = next;
132
- const directEnd = cursor + aggregateValue / total * span;
133
- const endTheta = positiveIndex === positiveValues.length - 1
134
- ? thetaScale.range[1]
135
- : normalizedTotal === undefined && Number.isFinite(directEnd)
136
- ? directEnd
137
- : interpolateNumber(
138
- thetaScale.range[0],
139
- thetaScale.range[1],
140
- (cumulative + cumulativeCorrection) /
141
- (normalizedTotal?.total ?? total)
142
- );
143
- if (!Number.isFinite(endTheta) || endTheta === cursor) {
144
- throw new RangeError(
145
- `Arc theta group "${String(value)}" cannot resolve a distinct finite sector.`
146
- );
147
- }
191
+ const { startTheta, endTheta } = ranges[positiveIndex];
148
192
  const distinctColors = [...new Set(group.map(item => item.color))];
149
193
  if (distinctColors.length > 1) {
150
194
  throw new Error(
@@ -157,17 +201,101 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
157
201
  count: group.length,
158
202
  aggregateValue,
159
203
  color: distinctColors[0],
160
- startTheta: cursor,
204
+ startTheta,
161
205
  endTheta,
162
- innerRadius,
163
- outerRadius: frame.availableRadius,
206
+ ...radii,
164
207
  sourceIndices: group.map(item => item.sourceIndex)
165
208
  });
166
- cursor = endTheta;
167
209
  }
168
210
  return sectors;
169
211
  }
170
212
 
213
+ function quantitativeSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
214
+ const theta = layer.encoding.theta;
215
+ if (layer.encoding?.radius !== undefined) {
216
+ throw new Error(
217
+ `Arc mark "${layer.id}" cannot combine quantitative theta with radius.`
218
+ );
219
+ }
220
+ const values = readArcThetaValues(rows, theta.field, layer.id);
221
+ const colors = colorValues(rows, layer.encoding?.color);
222
+ const positiveIndices = values
223
+ .map((value, index) => value > 0 ? index : undefined)
224
+ .filter(index => index !== undefined);
225
+ const positiveValues = positiveIndices.map(index => values[index]);
226
+ const ranges = resolveProportionalRanges(
227
+ positiveValues,
228
+ thetaScale.range,
229
+ index => `row ${positiveIndices[index]}`
230
+ );
231
+ const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
232
+ return positiveIndices.map((sourceIndex, index) => ({
233
+ key: `${String(values[sourceIndex])}:${sourceIndex}`,
234
+ theta: values[sourceIndex],
235
+ count: 1,
236
+ aggregateValue: values[sourceIndex],
237
+ color: colors[sourceIndex],
238
+ ...ranges[index],
239
+ ...radii,
240
+ sourceIndices: [sourceIndex]
241
+ }));
242
+ }
243
+
244
+ export function deriveMeasuredArcValues(rows, layer) {
245
+ const theta = requireArcLayer(layer);
246
+ const radius = layer.encoding?.radius;
247
+ if (!["nominal", "ordinal"].includes(theta.fieldType) || theta.aggregate !== undefined ||
248
+ !["count", "sum"].includes(radius?.aggregate) || radius.fieldType !== "quantitative") {
249
+ throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");
250
+ }
251
+ if (radius.aggregate === "count" && Object.hasOwn(radius, "field")) {
252
+ throw new Error("Measured count radius does not accept a field.");
253
+ }
254
+ const categories = readNominalField(rows, theta.field);
255
+ const measures = radius.aggregate === "count" ? rows.map(() => 1)
256
+ : readQuantitativeField(rows, radius.field);
257
+ if (measures.some(value => value < 0)) {
258
+ throw new RangeError("Measured Arc radius inputs must be non-negative.");
259
+ }
260
+ const colors = colorValues(rows, layer.encoding?.color);
261
+ const groups = new Map();
262
+ for (const [index, category] of categories.entries()) {
263
+ const group = groups.get(category) ?? { key: category, sourceIndices: [], values: [], colors: [] };
264
+ group.sourceIndices.push(index);
265
+ group.values.push(measures[index]);
266
+ group.colors.push(colors[index]);
267
+ groups.set(category, group);
268
+ }
269
+ const result = [...groups.values()].map(group => {
270
+ const distinctColors = [...new Set(group.colors)];
271
+ if (distinctColors.length > 1) {
272
+ throw new Error(`Measured Arc category "${group.key}" must resolve to one color value.`);
273
+ }
274
+ return { key: group.key, theta: group.key, count: group.sourceIndices.length,
275
+ radius: stableFiniteSum(group.values, "Measured Arc radius aggregate"),
276
+ color: distinctColors[0], sourceIndices: group.sourceIndices };
277
+ });
278
+ if (!result.some(group => group.radius > 0)) {
279
+ throw new Error("Measured Arc radius requires at least one positive category aggregate.");
280
+ }
281
+ return cloneAndFreeze(result);
282
+ }
283
+
284
+ function measuredRadialSectors(rows, layer, thetaScale, radiusScale) {
285
+ const items = deriveMeasuredArcValues(rows, layer);
286
+ const centers = mapOrdinalPositionValues(items.map(item => item.theta), thetaScale);
287
+ const radii = mapContinuousScaleValues(items.map(item => item.radius), radiusScale);
288
+ const halfBand = (Math.sign(thetaScale.step) || 1) * thetaScale.bandwidth / 2;
289
+ const byCategory = new Map(items.map((item, index) => [item.theta, {
290
+ ...item, startTheta: centers[index] - halfBand, endTheta: centers[index] + halfBand,
291
+ innerRadius: radiusScale.range[0], outerRadius: radii[index]
292
+ }]));
293
+ return thetaScale.domain.flatMap(category => {
294
+ const item = byCategory.get(category);
295
+ return item === undefined || item.radius === 0 ? [] : [item];
296
+ });
297
+ }
298
+
171
299
  function radialSectors(rows, layer, thetaScale, radiusScale) {
172
300
  const theta = layer.encoding.theta;
173
301
  const radius = layer.encoding?.radius;
@@ -223,7 +351,6 @@ export function deriveArcSectors(rows, layer, {
223
351
  } = {}) {
224
352
  if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
225
353
  const theta = requireArcLayer(layer);
226
- requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`);
227
354
  if (
228
355
  !Number.isFinite(innerRadiusRatio) ||
229
356
  innerRadiusRatio < 0 ||
@@ -231,8 +358,29 @@ export function deriveArcSectors(rows, layer, {
231
358
  ) {
232
359
  throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
233
360
  }
234
- const sectors = ["count", "sum"].includes(theta.aggregate)
235
- ? proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
236
- : radialSectors(rows, layer, thetaScale, radiusScale);
361
+ const sectors = radiusScale?.radialMapping !== undefined
362
+ ? measuredRadialSectors(rows, layer, requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`), radiusScale)
363
+ : theta.fieldType === "quantitative"
364
+ ? quantitativeSectors(
365
+ rows,
366
+ layer,
367
+ requireContinuousThetaScale(thetaScale, `Arc mark "${layer.id}" theta`),
368
+ frame,
369
+ innerRadiusRatio
370
+ )
371
+ : ["count", "sum"].includes(theta.aggregate)
372
+ ? proportionalSectors(
373
+ rows,
374
+ layer,
375
+ requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
376
+ frame,
377
+ innerRadiusRatio
378
+ )
379
+ : radialSectors(
380
+ rows,
381
+ layer,
382
+ requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
383
+ radiusScale
384
+ );
237
385
  return cloneAndFreeze({ sectors });
238
386
  }
@@ -0,0 +1,46 @@
1
+ import { validateKeys, validateNonEmptyString } from "../core/validation.js";
2
+ import { isPlainObject } from "../core/immutable.js";
3
+ import { readQuantitativeField } from "./scales/fields.js";
4
+
5
+ export function validateAreaMissing(value = "error") {
6
+ if (!["error", "break"].includes(value)) {
7
+ throw new Error('Area missing must be "error" or "break".');
8
+ }
9
+ return value;
10
+ }
11
+
12
+ export function normalizeAreaBound(bound) {
13
+ if (typeof bound === "string") return { field: validateNonEmptyString(bound, "Area endpoint field") };
14
+ if (!isPlainObject(bound)) throw new TypeError("Area bound must be a field or datum object.");
15
+ validateKeys(bound, ["datum"], "Area bound");
16
+ if (!Number.isFinite(bound.datum)) throw new TypeError("Area datum must be finite.");
17
+ return { datum: bound.datum };
18
+ }
19
+
20
+ export function readAreaEndpoint(rows, encoding, missing = "error") {
21
+ if (Object.hasOwn(encoding, "datum")) {
22
+ if (Object.hasOwn(encoding, "field") || !Number.isFinite(encoding.datum) || encoding.fieldType !== "quantitative") {
23
+ throw new TypeError("Area endpoint requires exactly one finite quantitative datum or field.");
24
+ }
25
+ return rows.map(() => encoding.datum);
26
+ }
27
+ if (missing !== "break") return readQuantitativeField(rows, encoding.field);
28
+ validateNonEmptyString(encoding.field, "Area endpoint field");
29
+ if (rows.length > 0 && !rows.some(row => Object.hasOwn(row, encoding.field))) {
30
+ throw new Error(`Area endpoint field "${encoding.field}" does not exist.`);
31
+ }
32
+ return rows.map((row, index) => {
33
+ const value = row[encoding.field];
34
+ if (value == null) return null;
35
+ if (!Number.isFinite(value)) {
36
+ throw new TypeError(`Field "${encoding.field}" must contain a finite number at row ${index}.`);
37
+ }
38
+ return value;
39
+ });
40
+ }
41
+
42
+ export function validateAreaEndpointPair(primary, secondary) {
43
+ if (primary?.datum !== undefined && secondary?.datum !== undefined) {
44
+ throw new Error("Area range requires at least one field endpoint.");
45
+ }
46
+ }
@@ -1,15 +1,11 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
- import {
3
- readNominalField,
4
- readQuantitativeField,
5
- readTemporalField
6
- } from "./scales/index.js";
7
- import {
8
- layoutSeriesPartition,
9
- validateColorLayout
10
- } from "./seriesLayout.js";
2
+ import { readNominalField, readQuantitativeField, readTemporalField } from "./scales/index.js";
3
+ import { layoutSeriesPartition, validateColorLayout } from "./seriesLayout.js";
11
4
  import { stableOrderPathValues } from "./pathOrder.js";
12
5
  import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
6
+ import { validatePathSeriesAppearance } from "./pathSeries.js";
7
+ import { readAreaEndpoint, validateAreaEndpointPair } from "./areaEndpoints.js";
8
+ import { validateGeneratedItemLimit } from "../core/validation.js";
13
9
 
14
10
  function indexSegments(segments) {
15
11
  const indexed = [];
@@ -21,7 +17,7 @@ export function deriveAreaSeries(rows, layer) {
21
17
  if (layer?.mark?.type !== "area") {
22
18
  throw new Error("Area series derivation requires a semantic area mark.");
23
19
  }
24
- const { x, y, x2, y2, group, color } = layer.encoding ?? {};
20
+ const { x, y, x2, y2 } = layer.encoding ?? {};
25
21
  const vertical =
26
22
  ["quantitative", "temporal"].includes(x?.fieldType) &&
27
23
  y?.fieldType === "quantitative" &&
@@ -37,26 +33,19 @@ export function deriveAreaSeries(rows, layer) {
37
33
  `Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or temporal independent position.`
38
34
  );
39
35
  }
40
- if (group !== undefined && group.fieldType !== "nominal") {
41
- throw new Error(`Area group encoding on mark "${layer.id}" must be nominal.`);
42
- }
36
+ const grouping = validatePathSeriesAppearance(rows, layer);
43
37
  const orientation = vertical ? "vertical" : "horizontal";
44
38
  const independent = vertical ? x : y;
45
39
  const independentValues = independent.fieldType === "temporal"
46
- ? readTemporalField(rows, independent.field)
40
+ ? readTemporalField(rows, independent.field, independent.temporalUnit)
47
41
  : readQuantitativeField(rows, independent.field);
48
- const lower = vertical
49
- ? readQuantitativeField(rows, y.field)
50
- : readQuantitativeField(rows, x.field);
51
- const upper = vertical
52
- ? readQuantitativeField(rows, y2.field)
53
- : readQuantitativeField(rows, x2.field);
54
- const groupValues = group === undefined
55
- ? rows.map(() => undefined)
56
- : readNominalField(rows, group.field);
57
- const colorValues = color === undefined
58
- ? rows.map(() => undefined)
59
- : readNominalField(rows, color.field);
42
+ if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
43
+ const primary = vertical ? y : x;
44
+ const secondary = vertical ? y2 : x2;
45
+ validateAreaEndpointPair(primary, secondary);
46
+ const missing = layer.mark.missing ?? "error";
47
+ const lower = readAreaEndpoint(rows, primary, missing);
48
+ const upper = readAreaEndpoint(rows, secondary, missing);
60
49
  const pathOrder = layer.encoding?.pathOrder;
61
50
  const orderValues = pathOrder === undefined
62
51
  ? undefined
@@ -64,32 +53,24 @@ export function deriveAreaSeries(rows, layer) {
64
53
  const groups = new Map();
65
54
 
66
55
  for (let index = 0; index < rows.length; index += 1) {
67
- const key = groupValues[index];
56
+ const tuple = grouping.map(field => rows[index][field]);
57
+ const key = JSON.stringify(tuple);
68
58
  const series = groups.get(key) ?? {
69
- key: {
70
- ...(group === undefined ? {} : { [group.field]: key }),
71
- ...(color === undefined ? {} : { [color.field]: colorValues[index] })
72
- },
59
+ key: Object.fromEntries(grouping.map((field, i) => [field, tuple[i]])),
73
60
  values: []
74
61
  };
75
- if (
76
- color !== undefined &&
77
- !Object.is(series.key[color.field], colorValues[index])
78
- ) {
79
- throw new Error(
80
- `Area series "${String(key)}" must have one color value.`
81
- );
82
- }
83
62
  series.values.push(vertical
84
63
  ? {
85
64
  x: independentValues[index],
86
65
  y: lower[index],
87
66
  y2: upper[index],
67
+ ...(missing === "break" ? { sourceIndex: index } : {}),
88
68
  ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
89
69
  }
90
70
  : {
91
71
  x: lower[index],
92
72
  x2: upper[index],
73
+ ...(missing === "break" ? { sourceIndex: index } : {}),
93
74
  y: independentValues[index],
94
75
  ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
95
76
  });
@@ -98,7 +79,8 @@ export function deriveAreaSeries(rows, layer) {
98
79
  if (groups.size === 0) {
99
80
  throw new Error(`Area mark "${layer.id}" has no values.`);
100
81
  }
101
- const series = [...groups.values()].map(item => {
82
+ layoutRawAreaGroups(groups, { layer, vertical, primary, secondary });
83
+ const series = [...groups.values()].flatMap(item => {
102
84
  const key = vertical ? "x" : "y";
103
85
  const values = pathOrder === undefined
104
86
  ? item.values.sort((left, right) => left[key] - right[key])
@@ -107,6 +89,22 @@ export function deriveAreaSeries(rows, layer) {
107
89
  item.values.map(value => value.pathOrder),
108
90
  pathOrder.order
109
91
  );
92
+ if (missing === "break") {
93
+ const segments = [];
94
+ let segment = [];
95
+ const emit = () => {
96
+ if (segment.length >= 2) segments.push({ key: item.key,
97
+ sourceIndices: segment.map(value => value.sourceIndex),
98
+ values: segment.map(({ sourceIndex, ...value }) => value) });
99
+ segment = [];
100
+ };
101
+ for (const value of values) {
102
+ if ((vertical ? value.y == null || value.y2 == null : value.x == null || value.x2 == null)) emit();
103
+ else segment.push(value);
104
+ }
105
+ emit();
106
+ return segments;
107
+ }
110
108
  if (values.length < 2) {
111
109
  throw new Error(
112
110
  `Area series on mark "${layer.id}" requires at least two points.`
@@ -114,6 +112,7 @@ export function deriveAreaSeries(rows, layer) {
114
112
  }
115
113
  return { key: item.key, values };
116
114
  });
115
+ if (series.length === 0) throw new Error(`Area mark "${layer.id}" has no valid segment with at least two points.`);
117
116
  return cloneAndFreeze({
118
117
  orientation,
119
118
  xValues: series.flatMap(item => item.values.flatMap(value =>
@@ -151,7 +150,7 @@ export function deriveCenteredAreaSeries(rows, layer) {
151
150
  );
152
151
  }
153
152
  const xValues = x.fieldType === "temporal"
154
- ? readTemporalField(rows, x.field)
153
+ ? readTemporalField(rows, x.field, x.temporalUnit)
155
154
  : readQuantitativeField(rows, x.field);
156
155
  const yValues = readQuantitativeField(rows, y.field);
157
156
  const groups = readNominalField(rows, group.field);
@@ -359,3 +358,50 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
359
358
  }))
360
359
  });
361
360
  }
361
+
362
+ export function layoutRawAreaGroups(groups, { layer, vertical, primary, secondary }) {
363
+ const mode = layer.layout?.mode ?? "overlay";
364
+ validateColorLayout(mode);
365
+ if (mode === "overlay") return;
366
+ if (mode === "group" || (mode === "center" && !vertical)) {
367
+ throw new Error(`Area layout "${mode}" is incompatible with this orientation.`);
368
+ }
369
+ const primaryDatum = Object.hasOwn(primary, "datum");
370
+ const secondaryDatum = Object.hasOwn(secondary, "datum");
371
+ if (primaryDatum === secondaryDatum || (primaryDatum ? primary.datum : secondary.datum) !== 0) {
372
+ throw new Error("Stacked area requires one value field and one zero datum endpoint.");
373
+ }
374
+ const position = vertical ? "x" : "y";
375
+ const lower = vertical ? "y" : "x", upper = `${lower}2`;
376
+ const measure = primaryDatum ? upper : lower;
377
+ const series = [...groups.values()];
378
+ const indices = series.map(item => {
379
+ const index = new Map();
380
+ for (const value of item.values) {
381
+ if (index.has(value[position])) throw new Error("Area layout requires unique group/position rows.");
382
+ index.set(value[position], value);
383
+ }
384
+ return index;
385
+ });
386
+ const positions = [...new Set(series.flatMap(item => item.values.map(value => value[position])))].sort((a, b) => a - b);
387
+ validateGeneratedItemLimit(positions.length * series.length, "Area layout cell count");
388
+ if (indices.some(index => index.size !== positions.length || positions.some(value => !index.has(value)))) {
389
+ throw new Error("Area layout requires an aligned row for every group and position.");
390
+ }
391
+ for (const position of positions) {
392
+ const points = indices.map(index => index.get(position));
393
+ if (points.some(point => point[measure] == null)) {
394
+ for (const point of points) { point[lower] = null; point[upper] = null; }
395
+ continue;
396
+ }
397
+ const segments = layoutSeriesPartition(points.map(point => point[measure]), mode);
398
+ const byIndex = new Map(segments.map(segment => [segment.index, segment]));
399
+ let endpoint = mode === "center" ? segments[0]?.start ?? 0 : 0;
400
+ points.forEach((point, index) => {
401
+ const segment = byIndex.get(index);
402
+ point[lower] = segment?.start ?? (mode === "diverging" ? 0 : endpoint);
403
+ point[upper] = segment?.end ?? (mode === "diverging" ? 0 : endpoint);
404
+ if (segment !== undefined && mode !== "diverging") endpoint = segment.end;
405
+ });
406
+ }
407
+ }
@@ -1,15 +1,9 @@
1
+ import { readSeriesIdentity } from "../pathSeries.js";
1
2
  import { cloneAndFreeze } from "../../core/immutable.js";
2
3
  import { readNominalField, readTemporalField } from "../scales/index.js";
3
- import {
4
- aggregateRows,
5
- validateAggregateFieldType,
6
- validateAggregateFieldValues,
7
- } from "../aggregate.js";
8
- import {
9
- BAR_GRAINS,
10
- resolveBarChannels,
11
- resolveBarGrain
12
- } from "./policy.js";
4
+ import { aggregateRows, validateAggregateFieldType, validateAggregateFieldValues } from "../aggregate.js";
5
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain, resolveBarColorLayout } from "./policy.js";
6
+ import { layoutSeriesPartition } from "../seriesLayout.js";
13
7
 
14
8
  function requireAggregateBarEncoding(layer) {
15
9
  if (layer?.mark?.type !== "bar") {
@@ -33,7 +27,7 @@ export function deriveBarAggregates(rows, layer) {
33
27
  const { channels, category, measure } = requireAggregateBarEncoding(layer);
34
28
  validateAggregateFieldValues(rows, measure.field, measure.fieldType);
35
29
  const categoryValues = category.fieldType === "temporal"
36
- ? readTemporalField(rows, category.field)
30
+ ? readTemporalField(rows, category.field, category.temporalUnit)
37
31
  : readNominalField(rows, category.field);
38
32
  const color = layer.encoding?.color;
39
33
  let colorValues;
@@ -52,17 +46,23 @@ export function deriveBarAggregates(rows, layer) {
52
46
  );
53
47
  }
54
48
  }
49
+ const identity = layer.encoding?.group === undefined ? undefined : readSeriesIdentity(rows, layer);
55
50
  const groups = new Map();
56
51
 
57
52
  for (let index = 0; index < rows.length; index += 1) {
58
53
  const categoryValue = categoryValues[index];
59
54
  const colorValue = colorValues?.[index];
60
- const key = JSON.stringify([categoryValue, colorValue]);
55
+ const seriesValue = identity?.values[index];
56
+ const key = JSON.stringify([categoryValue, identity === undefined ? colorValue : seriesValue]);
61
57
  const group = groups.get(key) ?? {
62
58
  category: categoryValue,
59
+ ...(identity === undefined ? {} : { series: seriesValue }),
63
60
  ...(colorValues === undefined ? {} : { color: colorValue }),
64
61
  rows: []
65
62
  };
63
+ if (identity !== undefined && colorValues !== undefined && group.color !== colorValue) {
64
+ throw new Error("Bar color requires one categorical value within each aggregate cell.");
65
+ }
66
66
  group.rows.push(rows[index]);
67
67
  groups.set(key, group);
68
68
  }
@@ -82,6 +82,7 @@ export function deriveBarAggregates(rows, layer) {
82
82
  x: channels.category === "x" ? group.category : value,
83
83
  y: channels.category === "y" ? group.category : value,
84
84
  ...(color === undefined ? {} : { color: aggregateColor }),
85
+ ...(identity === undefined ? {} : { series: group.series }),
85
86
  count: group.rows.length
86
87
  }];
87
88
  });
@@ -96,3 +97,23 @@ export function deriveBarAggregates(rows, layer) {
96
97
  values
97
98
  });
98
99
  }
100
+
101
+ export function deriveSeriesBarCells(rows, layer, categoryDomain, seriesDomain) {
102
+ const channels = resolveBarChannels(layer);
103
+ const values = deriveBarAggregates(rows, layer).values;
104
+ const identity = readSeriesIdentity(rows, layer);
105
+ const categories = categoryDomain ?? [...new Set(values.map(cell => cell[channels.category]))].sort((a, b) => a - b);
106
+ const seriesKey = cell => layer.encoding.group !== undefined ? cell.series : layer.encoding.color?.fieldType === "quantitative" ? undefined : cell.color;
107
+ const series = seriesDomain ?? (layer.encoding.group !== undefined ? identity.domain : [...new Set(values.map(seriesKey))]);
108
+ const cells = new Map(values.map(cell => [JSON.stringify([cell[channels.category], seriesKey(cell)]), cell]));
109
+ const layout = resolveBarColorLayout(layer);
110
+ return categories.flatMap(category => {
111
+ const partition = series.map(key => cells.get(JSON.stringify([category, key])));
112
+ const segments = layout === "group"
113
+ ? partition.flatMap((cell, index) => cell === undefined ? [] : [{ index, start: 0, end: cell[channels.measure] }])
114
+ : layoutSeriesPartition(partition.map(cell => cell?.[channels.measure] ?? 0), layout);
115
+ return segments.flatMap(segment => partition[segment.index] === undefined ? [] : [{
116
+ cell: partition[segment.index], start: segment.start, end: segment.end, seriesIndex: segment.index
117
+ }]);
118
+ });
119
+ }