ggaction 0.0.10 → 0.0.12

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 (224) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -3
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/intent-taxonomy.json +1 -183
  7. package/knowledge/intent-taxonomy.schema.json +1 -0
  8. package/knowledge/mcp-resources.json +1 -95
  9. package/knowledge/mcp-resources.schema.json +1 -0
  10. package/knowledge/task-packet.schema.json +1 -159
  11. package/knowledge/task-resolver.js +1 -1230
  12. package/package.json +18 -2
  13. package/src/actions/boxPlots/edit.js +45 -298
  14. package/src/actions/boxPlots/options.js +46 -77
  15. package/src/actions/boxPlots/resolve.js +1 -1
  16. package/src/actions/charts/bar.js +3 -0
  17. package/src/actions/charts/heatmap.js +73 -83
  18. package/src/actions/charts/index.js +1 -1
  19. package/src/actions/charts/parallel.js +35 -1
  20. package/src/actions/charts/shared.js +20 -0
  21. package/src/actions/composition/actions.js +46 -49
  22. package/src/actions/data/bin2d.js +105 -2
  23. package/src/actions/data/box.js +17 -17
  24. package/src/actions/data/density.js +42 -78
  25. package/src/actions/data/gradientProfile.js +22 -52
  26. package/src/actions/data/horizon.js +16 -41
  27. package/src/actions/data/index.js +10 -10
  28. package/src/actions/data/interval.js +16 -38
  29. package/src/actions/data/intervalEdit.js +55 -0
  30. package/src/actions/data/regression.js +7 -17
  31. package/src/actions/data/shared.js +47 -0
  32. package/src/actions/data/timeUnit.js +14 -38
  33. package/src/actions/data/window.js +14 -38
  34. package/src/actions/distributions/revision.js +154 -34
  35. package/src/actions/encodings/angle.js +9 -10
  36. package/src/actions/encodings/appearance.js +17 -51
  37. package/src/actions/encodings/density/resolve.js +3 -1
  38. package/src/actions/encodings/density.js +1 -15
  39. package/src/actions/encodings/horizon/resolve.js +13 -3
  40. package/src/actions/encodings/horizon.js +8 -7
  41. package/src/actions/encodings/index.js +9 -9
  42. package/src/actions/encodings/offset.js +41 -45
  43. package/src/actions/encodings/parallel.js +9 -10
  44. package/src/actions/encodings/pathOrder.js +6 -13
  45. package/src/actions/encodings/position/apply.js +30 -0
  46. package/src/actions/encodings/position/policies/arc.js +23 -2
  47. package/src/actions/encodings/position/policies/index.js +4 -4
  48. package/src/actions/encodings/position/policies/line.js +47 -17
  49. package/src/actions/encodings/position/resolve.js +36 -7
  50. package/src/actions/encodings/ranged.js +18 -23
  51. package/src/actions/encodings/remove.js +15 -25
  52. package/src/actions/encodings/ruleAppearance.js +9 -24
  53. package/src/actions/encodings/shared.js +37 -0
  54. package/src/actions/encodings/strokeDash.js +8 -23
  55. package/src/actions/encodings/text.js +9 -10
  56. package/src/actions/errorBands/create.js +105 -39
  57. package/src/actions/errorBands/edit.js +89 -93
  58. package/src/actions/errorBands/index.js +1 -2
  59. package/src/actions/errorBars/create.js +212 -30
  60. package/src/actions/errorBars/edit.js +91 -48
  61. package/src/actions/errorBars/index.js +1 -2
  62. package/src/actions/facets/actions.js +45 -69
  63. package/src/actions/facets/derive.js +38 -53
  64. package/src/actions/facets/replay.js +0 -1
  65. package/src/actions/gradientPlots/components.js +59 -48
  66. package/src/actions/gradientPlots/edit.js +35 -216
  67. package/src/actions/gradientPlots/index.js +11 -10
  68. package/src/actions/gradientPlots/materialize.js +21 -35
  69. package/src/actions/gradientPlots/options.js +5 -7
  70. package/src/actions/gradientPlots/paint.js +9 -6
  71. package/src/actions/gradientPlots/resolve.js +1 -1
  72. package/src/actions/guides/axes/index.js +3 -3
  73. package/src/actions/guides/axes/labels.js +68 -26
  74. package/src/actions/guides/axes/lines.js +22 -2
  75. package/src/actions/guides/axes/policy.js +28 -8
  76. package/src/actions/guides/axes/ticks.js +27 -9
  77. package/src/actions/guides/axes/titles.js +113 -65
  78. package/src/actions/guides/grids/resolve.js +14 -0
  79. package/src/actions/guides/guides.js +2 -4
  80. package/src/actions/guides/index.js +2 -2
  81. package/src/actions/guides/legends/categorical/actions.js +44 -68
  82. package/src/actions/guides/legends/categorical/components.js +6 -1
  83. package/src/actions/guides/legends/categorical/index.js +2 -2
  84. package/src/actions/guides/legends/categorical/layout.js +148 -175
  85. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  86. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  87. package/src/actions/guides/legends/continuous/common.js +117 -66
  88. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  89. package/src/actions/guides/legends/continuous/interval.js +97 -76
  90. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  91. package/src/actions/guides/legends/edit.js +14 -1
  92. package/src/actions/guides/legends/index.js +1 -1
  93. package/src/actions/guides/legends/lane.js +2 -1
  94. package/src/actions/guides/legends/remove.js +8 -0
  95. package/src/actions/guides/legends/size.js +36 -43
  96. package/src/actions/guides/legends/strokeWidth.js +20 -26
  97. package/src/actions/guides/polar/axes/labels.js +17 -1
  98. package/src/actions/guides/polar/axes/shared.js +7 -1
  99. package/src/actions/guides/polar/axes/titles.js +19 -2
  100. package/src/actions/guides/polar/grids.js +4 -0
  101. package/src/actions/guides/polar/resolve.js +6 -0
  102. package/src/actions/index.js +13 -13
  103. package/src/actions/intervals/resolve.js +87 -117
  104. package/src/actions/marks/arc/actions.js +31 -36
  105. package/src/actions/marks/area/actions.js +23 -31
  106. package/src/actions/marks/index.js +6 -6
  107. package/src/actions/marks/line/actions.js +33 -52
  108. package/src/actions/marks/line/materialize.js +7 -5
  109. package/src/actions/marks/point/materialize.js +38 -16
  110. package/src/actions/marks/remove.js +21 -0
  111. package/src/actions/marks/rule/actions.js +79 -54
  112. package/src/actions/marks/shared.js +8 -4
  113. package/src/actions/marks/text/actions.js +14 -6
  114. package/src/actions/marks/text/layout.js +21 -45
  115. package/src/actions/marks/tick/actions.js +22 -21
  116. package/src/actions/primitives/createGraphics.js +4 -0
  117. package/src/actions/primitives/editGraphics.js +4 -0
  118. package/src/actions/primitives/index.js +3 -3
  119. package/src/actions/primitives/semantic.js +1 -1
  120. package/src/actions/primitives/semanticValidation/index.js +2 -2
  121. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  122. package/src/actions/regression/edit.js +3 -43
  123. package/src/actions/scales/consumers/families.js +24 -4
  124. package/src/actions/scales/consumers/index.js +1 -1
  125. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  126. package/src/actions/scales/definitions.js +100 -103
  127. package/src/actions/scales/materialize.js +5 -0
  128. package/src/actions/selection/actions.js +79 -121
  129. package/src/actions/titles/actions.js +48 -94
  130. package/src/actions/titles/resolve.js +14 -27
  131. package/src/actions/violinPlots/create.js +37 -44
  132. package/src/core/textMetrics.js +74 -37
  133. package/src/core/validation.js +18 -0
  134. package/src/core/vocabulary.js +2 -2
  135. package/src/grammar/aggregate.js +48 -28
  136. package/src/grammar/arcs.js +170 -32
  137. package/src/grammar/areaSeries.js +14 -16
  138. package/src/grammar/bin2d.js +37 -23
  139. package/src/grammar/boxPlot.js +14 -13
  140. package/src/grammar/categoricalDensity.js +14 -14
  141. package/src/grammar/categoryOrder.js +8 -14
  142. package/src/grammar/curveCommands.js +87 -20
  143. package/src/grammar/density.js +162 -141
  144. package/src/grammar/facets/index.js +14 -0
  145. package/src/grammar/facets/scales.js +7 -4
  146. package/src/grammar/gradientProfile.js +12 -15
  147. package/src/grammar/histogram.js +145 -83
  148. package/src/grammar/horizon.js +60 -23
  149. package/src/grammar/interval.js +14 -13
  150. package/src/grammar/lineSeries.js +91 -61
  151. package/src/grammar/numeric.js +290 -0
  152. package/src/grammar/paint.js +3 -4
  153. package/src/grammar/palettes.js +21 -32
  154. package/src/grammar/pathCommands.js +6 -0
  155. package/src/grammar/polarPaths.js +5 -5
  156. package/src/grammar/positionCompatibility.js +1 -1
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/rules.js +2 -0
  161. package/src/grammar/scales/color.js +35 -26
  162. package/src/grammar/scales/continuous.js +71 -24
  163. package/src/grammar/scales/definition.js +1 -1
  164. package/src/grammar/scales/discretized.js +42 -12
  165. package/src/grammar/scales/fields.js +21 -4
  166. package/src/grammar/scales/ordinal.js +139 -60
  167. package/src/grammar/scales/temporal.js +40 -11
  168. package/src/grammar/scales/transformed.js +199 -52
  169. package/src/grammar/schemas/graphicBounds.js +270 -79
  170. package/src/grammar/seriesLayout.js +50 -18
  171. package/src/grammar/ticks.js +123 -53
  172. package/src/grammar/timeUnit.js +18 -8
  173. package/src/grammar/transforms.js +4 -1
  174. package/src/grammar/window.js +70 -57
  175. package/src/index.js +1 -1
  176. package/src/layout/canvas.js +4 -8
  177. package/src/layout/composition.js +39 -40
  178. package/src/layout/facets.js +55 -30
  179. package/src/layout/labels.js +91 -15
  180. package/src/layout/legend.js +22 -20
  181. package/src/layout/legendLane.js +54 -40
  182. package/src/layout/title.js +7 -7
  183. package/src/materialization/bars/aggregate.js +14 -40
  184. package/src/materialization/bars/grouped.js +7 -18
  185. package/src/materialization/bars/histogram.js +2 -19
  186. package/src/materialization/bars/ranged.js +20 -15
  187. package/src/materialization/bars/resolve.js +21 -0
  188. package/src/materialization/facetGuides/legacyCategorical.js +9 -2
  189. package/src/materialization/facetGuides/placement.js +49 -19
  190. package/src/materialization/facetGuides/preparation.js +35 -6
  191. package/src/materialization/facets.js +42 -28
  192. package/src/materialization/guides/resources.js +2 -1
  193. package/src/materialization/marks/capabilities.js +15 -4
  194. package/src/materialization/marks/index.js +29 -6
  195. package/src/materialization/rowEncoding.js +57 -1
  196. package/src/materialization/scales/policies/bar.js +61 -102
  197. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  198. package/src/materialization/scales/policies/offset.js +54 -0
  199. package/src/materialization/scales/policies/series.js +13 -7
  200. package/src/materialization/scales/resolve.js +6 -9
  201. package/src/materialization/selection/items/arc.js +16 -2
  202. package/src/materialization/selection/items/bar.js +4 -2
  203. package/src/materialization/selection/items/common.js +21 -1
  204. package/src/materialization/selection/items/path.js +19 -2
  205. package/src/materialization/selection/items/point.js +4 -16
  206. package/src/materialization/selection/items/tick.js +13 -21
  207. package/src/materialization/selection/point.js +11 -4
  208. package/src/materialization/selection/styles.js +19 -27
  209. package/src/materialization/text.js +4 -1
  210. package/src/mcp/adapter.js +8 -3
  211. package/src/renderers/canvas/fill.js +2 -2
  212. package/src/renderers/canvas/index.js +35 -2
  213. package/src/renderers/canvas/native.js +160 -0
  214. package/src/renderers/pdf.js +14 -0
  215. package/src/renderers/svg.js +37 -4
  216. package/types/index.d.ts +3 -0
  217. package/types/program.d.ts +523 -131
  218. package/types/svg.d.ts +1 -0
  219. package/src/actions/errorBands/components.js +0 -81
  220. package/src/actions/errorBands/options.js +0 -39
  221. package/src/actions/errorBands/resolve.js +0 -28
  222. package/src/actions/errorBars/components.js +0 -62
  223. package/src/actions/errorBars/options.js +0 -58
  224. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,4 +1,12 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ interpolateNumber,
4
+ numericExtent,
5
+ requireFiniteResult,
6
+ stableFiniteDeviation,
7
+ stableFiniteMean,
8
+ stableFiniteSum
9
+ } from "./numeric.js";
2
10
 
3
11
  export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
4
12
  "count", "sum", "mean", "median", "min", "max",
@@ -8,8 +16,8 @@ export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
8
16
  ]);
9
17
 
10
18
  const SCALAR_OPERATIONS = new Set(SCALAR_AGGREGATE_OPERATIONS);
11
- const NOMINAL_OPERATIONS = new Set(["count", "distinct", "valid", "missing"]);
12
- const PARAMETERIZED_OPERATIONS = new Set(["quantile", "first", "last"]);
19
+ const NOMINAL_OPERATIONS = ["count", "distinct", "valid", "missing"];
20
+ const PARAMETERIZED_OPERATIONS = ["quantile", "first", "last"];
13
21
 
14
22
  function nonEmptyString(value, label) {
15
23
  if (typeof value !== "string" || value.length === 0) {
@@ -22,7 +30,7 @@ export function isScalarAggregate(value) {
22
30
  }
23
31
 
24
32
  export function isParameterizedAggregate(value) {
25
- return isPlainObject(value) && PARAMETERIZED_OPERATIONS.has(value.op);
33
+ return isPlainObject(value) && PARAMETERIZED_OPERATIONS.includes(value.op);
26
34
  }
27
35
 
28
36
  export function isAggregate(value) {
@@ -95,7 +103,7 @@ export function validateScalarAggregateFieldType(operation, fieldType) {
95
103
  throw new Error("Scalar aggregate calculation requires a scalar operation.");
96
104
  }
97
105
  if (fieldType === "quantitative") return fieldType;
98
- if (fieldType === "nominal" && NOMINAL_OPERATIONS.has(operation)) {
106
+ if (fieldType === "nominal" && NOMINAL_OPERATIONS.includes(operation)) {
99
107
  return fieldType;
100
108
  }
101
109
  throw new Error(
@@ -139,8 +147,11 @@ function quantile(values, probability) {
139
147
  const position = (ordered.length - 1) * probability;
140
148
  const lower = Math.floor(position);
141
149
  const upper = Math.ceil(position);
142
- return ordered[lower] +
143
- (ordered[upper] - ordered[lower]) * (position - lower);
150
+ return interpolateNumber(
151
+ ordered[lower],
152
+ ordered[upper],
153
+ position - lower
154
+ );
144
155
  }
145
156
 
146
157
  function comparableOrderKey(value) {
@@ -179,15 +190,6 @@ function aggregateOrderedRows(rows, field, aggregate) {
179
190
  : candidates.at(-1).value;
180
191
  }
181
192
 
182
- function moments(values) {
183
- const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
184
- const squared = values.reduce(
185
- (sum, value) => sum + (value - mean) ** 2,
186
- 0
187
- );
188
- return { mean, squared };
189
- }
190
-
191
193
  export function aggregateScalarValues(values, operation) {
192
194
  if (!Array.isArray(values)) {
193
195
  throw new TypeError("Aggregate values must be an array.");
@@ -206,27 +208,45 @@ export function aggregateScalarValues(values, operation) {
206
208
  const finite = finiteValues(values);
207
209
  if (finite.length === 0) return undefined;
208
210
  if (operation === "sum") {
209
- return finite.reduce((sum, value) => sum + value, 0);
211
+ return stableFiniteSum(finite, "Aggregate sum");
210
212
  }
211
- if (operation === "min") return Math.min(...finite);
212
- if (operation === "max") return Math.max(...finite);
213
+ if (operation === "min") return numericExtent(finite)[0];
214
+ if (operation === "max") return numericExtent(finite)[1];
213
215
  if (operation === "median") return quantile(finite, 0.5);
214
216
  if (operation === "q1") return quantile(finite, 0.25);
215
217
  if (operation === "q3") return quantile(finite, 0.75);
216
218
 
217
- const { mean, squared } = moments(finite);
219
+ const mean = stableFiniteMean(finite, "Aggregate mean");
218
220
  if (operation === "mean") return mean;
219
- if (operation === "varianceP") return squared / finite.length;
220
- if (operation === "stdevP") return Math.sqrt(squared / finite.length);
221
+ if (["variance", "varianceP", "stdev", "stdevP"].includes(operation)) {
222
+ const population = operation.endsWith("P");
223
+ if (!population && finite.length < 2) return undefined;
224
+ const kind = population ? "population" : "sample";
225
+ const deviation = stableFiniteDeviation(finite, {
226
+ sample: !population,
227
+ label: `Aggregate ${kind} deviation`
228
+ });
229
+ if (operation.startsWith("stdev")) return deviation.deviation;
230
+ return deviation.squared === undefined
231
+ ? requireFiniteResult(
232
+ deviation.deviation ** 2,
233
+ `Aggregate ${kind} variance`
234
+ )
235
+ : deviation.squared / (finite.length - (population ? 0 : 1));
236
+ }
221
237
  if (finite.length < 2) return undefined;
222
-
223
- const variance = squared / (finite.length - 1);
224
- if (operation === "variance") return variance;
225
- if (operation === "stdev") return Math.sqrt(variance);
226
- const stderr = Math.sqrt(variance) / Math.sqrt(finite.length);
238
+ const stderr = stableFiniteDeviation(finite, {
239
+ sample: true,
240
+ divisor: Math.sqrt(finite.length),
241
+ label: "Aggregate standard error"
242
+ }).deviation;
227
243
  if (operation === "stderr") return stderr;
228
- if (operation === "ciLower") return mean - 1.96 * stderr;
229
- if (operation === "ciUpper") return mean + 1.96 * stderr;
244
+ if (operation === "ciLower") {
245
+ return requireFiniteResult(mean - 1.96 * stderr, "Aggregate lower CI");
246
+ }
247
+ if (operation === "ciUpper") {
248
+ return requireFiniteResult(mean + 1.96 * stderr, "Aggregate upper CI");
249
+ }
230
250
  throw new Error(`Unsupported scalar aggregate "${operation}".`);
231
251
  }
232
252
 
@@ -5,23 +5,50 @@ import {
5
5
  readNominalField,
6
6
  readQuantitativeField
7
7
  } from "./scales/index.js";
8
+ import {
9
+ interpolateNumber,
10
+ normalizedFiniteSum,
11
+ requireFiniteResult,
12
+ stableFiniteSum
13
+ } from "./numeric.js";
8
14
 
9
15
  function requireArcLayer(layer) {
10
16
  if (layer?.mark?.type !== "arc") {
11
17
  throw new Error("Arc derivation requires a semantic arc mark.");
12
18
  }
13
19
  const theta = layer.encoding?.theta;
14
- if (theta === undefined || !["nominal", "ordinal"].includes(theta.fieldType)) {
15
- 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
+ );
16
27
  }
17
28
  return theta;
18
29
  }
19
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
+
20
44
  function requireBandScale(scale, label) {
21
45
  if (
22
46
  scale?.type !== "band" ||
23
47
  !Array.isArray(scale.domain) ||
24
48
  !Array.isArray(scale.range) ||
49
+ scale.range.length !== 2 ||
50
+ !scale.range.every(Number.isFinite) ||
51
+ scale.range[0] === scale.range[1] ||
25
52
  !Number.isFinite(scale.bandwidth) ||
26
53
  scale.bandwidth <= 0
27
54
  ) {
@@ -36,24 +63,90 @@ function colorValues(rows, encoding) {
36
63
  : readNominalField(rows, encoding.field);
37
64
  }
38
65
 
39
- export function readArcThetaWeights(rows, field, layerId = "arc") {
40
- 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.`);
41
68
  if (typeof field !== "string" || field.length === 0) {
42
- 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.`);
43
70
  }
44
- const weights = rows.map((row, index) => {
71
+ const values = rows.map((row, index) => {
45
72
  const value = row?.[field];
46
73
  if (!Number.isFinite(value) || value < 0) {
47
74
  throw new TypeError(
48
- `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}.`
49
76
  );
50
77
  }
51
78
  return value;
52
79
  });
53
- if (weights.every(value => value === 0)) {
54
- 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.`);
55
82
  }
56
- 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
+ };
57
150
  }
58
151
 
59
152
  function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
@@ -73,31 +166,29 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
73
166
  throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
74
167
  }
75
168
  group.push({
76
- row: rows[index],
77
169
  color: colors[index],
78
170
  sourceIndex: index,
79
171
  weight: weights[index]
80
172
  });
81
173
  }
82
- const total = [...groups.values()].reduce(
83
- (sum, group) => sum + group.reduce((groupSum, item) => groupSum + item.weight, 0),
84
- 0
85
- );
86
- if (total === 0) throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
87
- const start = thetaScale.range[0];
88
- const span = thetaScale.range[1] - thetaScale.range[0];
89
- const innerRadius = frame.availableRadius * innerRadiusRatio;
90
- let cursor = start;
174
+ const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
91
175
  const sectors = [];
92
176
  const positiveValues = thetaScale.domain.filter(value =>
93
177
  groups.get(value).some(item => item.weight > 0)
94
178
  );
179
+ const aggregateValues = positiveValues.map(value => stableFiniteSum(
180
+ groups.get(value).map(item => item.weight),
181
+ `Arc theta group "${String(value)}" aggregate`
182
+ ));
183
+ const ranges = resolveProportionalRanges(
184
+ aggregateValues,
185
+ thetaScale.range,
186
+ index => `group "${String(positiveValues[index])}"`
187
+ );
95
188
  for (const [positiveIndex, value] of positiveValues.entries()) {
96
189
  const group = groups.get(value);
97
- const aggregateValue = group.reduce((sum, item) => sum + item.weight, 0);
98
- const endTheta = positiveIndex === positiveValues.length - 1
99
- ? thetaScale.range[1]
100
- : cursor + aggregateValue / total * span;
190
+ const aggregateValue = aggregateValues[positiveIndex];
191
+ const { startTheta, endTheta } = ranges[positiveIndex];
101
192
  const distinctColors = [...new Set(group.map(item => item.color))];
102
193
  if (distinctColors.length > 1) {
103
194
  throw new Error(
@@ -110,17 +201,46 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
110
201
  count: group.length,
111
202
  aggregateValue,
112
203
  color: distinctColors[0],
113
- startTheta: cursor,
204
+ startTheta,
114
205
  endTheta,
115
- innerRadius,
116
- outerRadius: frame.availableRadius,
206
+ ...radii,
117
207
  sourceIndices: group.map(item => item.sourceIndex)
118
208
  });
119
- cursor = endTheta;
120
209
  }
121
210
  return sectors;
122
211
  }
123
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
+
124
244
  function radialSectors(rows, layer, thetaScale, radiusScale) {
125
245
  const theta = layer.encoding.theta;
126
246
  const radius = layer.encoding?.radius;
@@ -176,7 +296,6 @@ export function deriveArcSectors(rows, layer, {
176
296
  } = {}) {
177
297
  if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
178
298
  const theta = requireArcLayer(layer);
179
- requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`);
180
299
  if (
181
300
  !Number.isFinite(innerRadiusRatio) ||
182
301
  innerRadiusRatio < 0 ||
@@ -184,8 +303,27 @@ export function deriveArcSectors(rows, layer, {
184
303
  ) {
185
304
  throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
186
305
  }
187
- const sectors = ["count", "sum"].includes(theta.aggregate)
188
- ? proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
189
- : radialSectors(rows, layer, thetaScale, radiusScale);
306
+ const sectors = theta.fieldType === "quantitative"
307
+ ? quantitativeSectors(
308
+ rows,
309
+ layer,
310
+ requireContinuousThetaScale(thetaScale, `Arc mark "${layer.id}" theta`),
311
+ frame,
312
+ innerRadiusRatio
313
+ )
314
+ : ["count", "sum"].includes(theta.aggregate)
315
+ ? proportionalSectors(
316
+ rows,
317
+ layer,
318
+ requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
319
+ frame,
320
+ innerRadiusRatio
321
+ )
322
+ : radialSectors(
323
+ rows,
324
+ layer,
325
+ requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
326
+ radiusScale
327
+ );
190
328
  return cloneAndFreeze({ sectors });
191
329
  }
@@ -11,6 +11,12 @@ import {
11
11
  import { stableOrderPathValues } from "./pathOrder.js";
12
12
  import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
13
13
 
14
+ function indexSegments(segments) {
15
+ const indexed = [];
16
+ for (const segment of segments) indexed[segment.index] = segment;
17
+ return indexed;
18
+ }
19
+
14
20
  export function deriveAreaSeries(rows, layer) {
15
21
  if (layer?.mark?.type !== "area") {
16
22
  throw new Error("Area series derivation requires a semantic area mark.");
@@ -187,15 +193,11 @@ export function deriveCenteredAreaSeries(rows, layer) {
187
193
  const valuesBySeries = groupOrder.map(() => []);
188
194
  for (const position of orderedPositions) {
189
195
  const partition = groupOrder.map(key => byGroup.get(key).get(position));
190
- const segments = new Map(
191
- layoutSeriesPartition(partition, "center").map(segment => [
192
- segment.index,
193
- segment
194
- ])
195
- );
196
- let endpoint = -partition.reduce((sum, value) => sum + value, 0) / 2;
196
+ const resolvedSegments = layoutSeriesPartition(partition, "center");
197
+ const segments = indexSegments(resolvedSegments);
198
+ let endpoint = resolvedSegments[0]?.start ?? 0;
197
199
  for (let index = 0; index < groupOrder.length; index += 1) {
198
- const segment = segments.get(index);
200
+ const segment = segments[index];
199
201
  valuesBySeries[index].push({
200
202
  x: position,
201
203
  y: partition[index],
@@ -331,17 +333,13 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
331
333
  }
332
334
  return series.values[sample].y;
333
335
  });
334
- const segments = new Map(
335
- layoutSeriesPartition(densities, layout).map(segment => [
336
- segment.index,
337
- segment
338
- ])
339
- );
336
+ const resolvedSegments = layoutSeriesPartition(densities, layout);
337
+ const segments = indexSegments(resolvedSegments);
340
338
  let zeroThicknessEndpoint = layout === "center"
341
- ? -densities.reduce((sum, value) => sum + value, 0) / 2
339
+ ? resolvedSegments[0]?.start ?? 0
342
340
  : 0;
343
341
  for (let index = 0; index < derived.series.length; index += 1) {
344
- const segment = segments.get(index);
342
+ const segment = segments[index];
345
343
  valuesBySeries[index].push({
346
344
  x,
347
345
  lower: segment?.start ?? zeroThicknessEndpoint,
@@ -2,25 +2,22 @@ import {
2
2
  cloneAndFreeze,
3
3
  isPlainObject
4
4
  } from "../core/immutable.js";
5
+ import { validateNonEmptyString as requireField } from "../core/validation.js";
6
+ import { interpolateNumber, numericExtent } from "./numeric.js";
7
+
8
+ const MAX_AXIS_BINS = 10_000;
9
+ const MAX_CELLS = 1_000_000;
5
10
 
6
11
  const TRANSFORM_KEYS = Object.freeze([
7
12
  "type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
8
13
  "resolved"
9
14
  ]);
10
- const BIN_KEYS = Object.freeze(["x", "y"]);
11
- const EXTENT_KEYS = Object.freeze(["x", "y"]);
15
+ const AXIS_KEYS = Object.freeze(["x", "y"]);
12
16
  const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
13
17
  const RESOLVED_KEYS = Object.freeze([
14
18
  "extent", "edges", "eligibleCount", "occupiedCount"
15
19
  ]);
16
20
 
17
- function requireField(value, label) {
18
- if (typeof value !== "string" || value.length === 0) {
19
- throw new TypeError(`${label} must be a non-empty string.`);
20
- }
21
- return value;
22
- }
23
-
24
21
  function rejectUnknownKeys(value, supported, label) {
25
22
  const unknown = Object.keys(value).find(key => !supported.includes(key));
26
23
  if (unknown !== undefined) {
@@ -43,7 +40,7 @@ function normalizeBins(value) {
43
40
  "2D bin bins must be a positive integer or an object with x and y."
44
41
  );
45
42
  }
46
- rejectUnknownKeys(value, BIN_KEYS, "2D bin bins");
43
+ rejectUnknownKeys(value, AXIS_KEYS, "2D bin bins");
47
44
  return { x: value.x, y: value.y };
48
45
  }
49
46
 
@@ -51,9 +48,15 @@ function validateBins(value) {
51
48
  if (!isPlainObject(value)) {
52
49
  throw new TypeError("2D bin bins must be an object.");
53
50
  }
54
- rejectUnknownKeys(value, BIN_KEYS, "2D bin bins");
51
+ rejectUnknownKeys(value, AXIS_KEYS, "2D bin bins");
55
52
  requirePositiveInteger(value.x, "2D bin x bins");
56
53
  requirePositiveInteger(value.y, "2D bin y bins");
54
+ if (value.x > MAX_AXIS_BINS || value.y > MAX_AXIS_BINS) {
55
+ throw new RangeError(`2D bin axes support at most ${MAX_AXIS_BINS} bins.`);
56
+ }
57
+ if (value.x * value.y > MAX_CELLS) {
58
+ throw new RangeError(`2D bin grids support at most ${MAX_CELLS} cells.`);
59
+ }
57
60
  }
58
61
 
59
62
  function normalizeRequestedExtent(value) {
@@ -61,7 +64,7 @@ function normalizeRequestedExtent(value) {
61
64
  if (!isPlainObject(value)) {
62
65
  throw new TypeError("2D bin extent must be an object.");
63
66
  }
64
- rejectUnknownKeys(value, EXTENT_KEYS, "2D bin extent");
67
+ rejectUnknownKeys(value, AXIS_KEYS, "2D bin extent");
65
68
  return {
66
69
  x: value.x ?? "auto",
67
70
  y: value.y ?? "auto"
@@ -84,7 +87,7 @@ function validateRequestedExtent(value) {
84
87
  if (!isPlainObject(value)) {
85
88
  throw new TypeError("2D bin extent must be an object.");
86
89
  }
87
- rejectUnknownKeys(value, EXTENT_KEYS, "2D bin extent");
90
+ rejectUnknownKeys(value, AXIS_KEYS, "2D bin extent");
88
91
  validateExtentValue(value.x, "2D bin x extent");
89
92
  validateExtentValue(value.y, "2D bin y extent");
90
93
  }
@@ -152,9 +155,9 @@ function validateResolved(value, bins) {
152
155
  if (!isPlainObject(value.extent) || !isPlainObject(value.edges)) {
153
156
  throw new TypeError("Resolved 2D bin extent and edges must be objects.");
154
157
  }
155
- rejectUnknownKeys(value.extent, EXTENT_KEYS, "resolved 2D bin extent");
156
- rejectUnknownKeys(value.edges, EXTENT_KEYS, "resolved 2D bin edges");
157
- for (const axis of ["x", "y"]) {
158
+ rejectUnknownKeys(value.extent, AXIS_KEYS, "resolved 2D bin extent");
159
+ rejectUnknownKeys(value.edges, AXIS_KEYS, "resolved 2D bin edges");
160
+ for (const axis of AXIS_KEYS) {
158
161
  validateExtentValue(
159
162
  value.extent[axis],
160
163
  `Resolved 2D bin ${axis} extent`,
@@ -223,8 +226,7 @@ export function normalizeBin2DTransform({
223
226
  }
224
227
 
225
228
  function resolveExtent(values, requested, axis) {
226
- const minimum = Math.min(...values);
227
- const maximum = Math.max(...values);
229
+ const [minimum, maximum] = numericExtent(values);
228
230
  const extent = requested === "auto" ? [minimum, maximum] : requested;
229
231
  if (extent[0] > minimum || extent[1] < maximum) {
230
232
  throw new RangeError(
@@ -237,12 +239,24 @@ function resolveExtent(values, requested, axis) {
237
239
  return [...extent];
238
240
  }
239
241
 
240
- function createEdges(extent, count) {
242
+ function createEdges(extent, count, axis) {
241
243
  const [minimum, maximum] = extent;
242
244
  const step = (maximum - minimum) / count;
243
- return Array.from({ length: count + 1 }, (_, index) =>
244
- index === count ? maximum : minimum + step * index
245
+ const edges = Array.from({ length: count + 1 }, (_, index) =>
246
+ index === count
247
+ ? maximum
248
+ : Number.isFinite(step)
249
+ ? minimum + step * index
250
+ : interpolateNumber(minimum, maximum, index / count)
245
251
  );
252
+ if (edges.some((edge, index) =>
253
+ !Number.isFinite(edge) || index > 0 && edge <= edges[index - 1]
254
+ )) {
255
+ throw new RangeError(
256
+ `2D bin ${axis} extent cannot represent ${count} distinct bins.`
257
+ );
258
+ }
259
+ return edges;
246
260
  }
247
261
 
248
262
  function findBinIndex(value, edges) {
@@ -287,8 +301,8 @@ export function deriveBin2DRows(rows, transform) {
287
301
  )
288
302
  };
289
303
  const edges = {
290
- x: createEdges(extent.x, transform.bins.x),
291
- y: createEdges(extent.y, transform.bins.y)
304
+ x: createEdges(extent.x, transform.bins.x, "x"),
305
+ y: createEdges(extent.y, transform.bins.y, "y")
292
306
  };
293
307
  const buckets = Array.from(
294
308
  { length: transform.bins.x * transform.bins.y },
@@ -1,4 +1,5 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { interpolateNumber, stableDecimal } from "./numeric.js";
2
3
 
3
4
  export const BOX_FIELDS = Object.freeze({
4
5
  q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
@@ -7,10 +8,10 @@ export const BOX_FIELDS = Object.freeze({
7
8
  count: "__boxPlot_count"
8
9
  });
9
10
 
10
- const TRANSFORM_KEYS = Object.freeze([
11
+ const TRANSFORM_KEYS = [
11
12
  "type", "category", "field", "method", "whisker", "factor", "as"
12
- ]);
13
- const OUTPUT_KEYS = Object.freeze(Object.keys(BOX_FIELDS));
13
+ ];
14
+ const OUTPUT_KEYS = Object.keys(BOX_FIELDS);
14
15
 
15
16
  function field(value, label) {
16
17
  if (typeof value !== "string" || value.length === 0) throw new TypeError(`${label} must be a non-empty string.`);
@@ -82,11 +83,7 @@ function quantile(sorted, p) {
82
83
  const position = (sorted.length - 1) * p;
83
84
  const lower = Math.floor(position);
84
85
  const upper = Math.ceil(position);
85
- return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
86
- }
87
-
88
- function stableNumber(value) {
89
- return Number(value.toFixed(12));
86
+ return interpolateNumber(sorted[lower], sorted[upper], position - lower);
90
87
  }
91
88
 
92
89
  export function deriveBoxData(rows, transform) {
@@ -113,15 +110,19 @@ export function deriveBoxData(rows, transform) {
113
110
  const outlierSet = new Set();
114
111
  for (const group of groups) {
115
112
  const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
116
- const q1 = stableNumber(quantile(sorted, 0.25));
117
- const median = stableNumber(quantile(sorted, 0.5));
118
- const q3 = stableNumber(quantile(sorted, 0.75));
113
+ const [q1, median, q3] = [0.25, 0.5, 0.75].map(probability =>
114
+ stableDecimal(quantile(sorted, probability))
115
+ );
116
+ const spread = q3 - q1;
119
117
  const lowerFence = whisker === "minmax"
120
118
  ? sorted[0]
121
- : stableNumber(q1 - factor * (q3 - q1));
119
+ : stableDecimal(q1 - factor * spread);
122
120
  const upperFence = whisker === "minmax"
123
121
  ? sorted.at(-1)
124
- : stableNumber(q3 + factor * (q3 - q1));
122
+ : stableDecimal(q3 + factor * spread);
123
+ if (![lowerFence, upperFence].every(Number.isFinite)) {
124
+ throw new RangeError("Box Tukey fences are outside the finite numeric range.");
125
+ }
125
126
  const inliers = sorted.filter(value => value >= lowerFence && value <= upperFence);
126
127
  const lowerWhisker = inliers[0];
127
128
  const upperWhisker = inliers.at(-1);