ggaction 0.0.10 → 0.0.11

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 (217) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +4 -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 +8 -24
  43. package/src/actions/encodings/parallel.js +9 -10
  44. package/src/actions/encodings/pathOrder.js +6 -13
  45. package/src/actions/encodings/position/policies/index.js +4 -4
  46. package/src/actions/encodings/position/policies/line.js +47 -17
  47. package/src/actions/encodings/position/resolve.js +24 -5
  48. package/src/actions/encodings/ranged.js +18 -23
  49. package/src/actions/encodings/remove.js +15 -25
  50. package/src/actions/encodings/ruleAppearance.js +9 -24
  51. package/src/actions/encodings/shared.js +37 -0
  52. package/src/actions/encodings/strokeDash.js +8 -23
  53. package/src/actions/encodings/text.js +9 -10
  54. package/src/actions/errorBands/create.js +105 -39
  55. package/src/actions/errorBands/edit.js +89 -93
  56. package/src/actions/errorBands/index.js +1 -2
  57. package/src/actions/errorBars/create.js +78 -27
  58. package/src/actions/errorBars/edit.js +81 -48
  59. package/src/actions/errorBars/index.js +1 -2
  60. package/src/actions/facets/actions.js +45 -69
  61. package/src/actions/facets/derive.js +38 -53
  62. package/src/actions/facets/replay.js +0 -1
  63. package/src/actions/gradientPlots/components.js +59 -48
  64. package/src/actions/gradientPlots/edit.js +35 -216
  65. package/src/actions/gradientPlots/index.js +11 -10
  66. package/src/actions/gradientPlots/materialize.js +21 -35
  67. package/src/actions/gradientPlots/options.js +5 -7
  68. package/src/actions/gradientPlots/paint.js +9 -6
  69. package/src/actions/gradientPlots/resolve.js +1 -1
  70. package/src/actions/guides/axes/index.js +3 -3
  71. package/src/actions/guides/axes/labels.js +68 -26
  72. package/src/actions/guides/axes/lines.js +22 -2
  73. package/src/actions/guides/axes/policy.js +28 -8
  74. package/src/actions/guides/axes/ticks.js +27 -9
  75. package/src/actions/guides/axes/titles.js +113 -65
  76. package/src/actions/guides/grids/resolve.js +14 -0
  77. package/src/actions/guides/guides.js +2 -4
  78. package/src/actions/guides/index.js +2 -2
  79. package/src/actions/guides/legends/categorical/actions.js +44 -68
  80. package/src/actions/guides/legends/categorical/components.js +6 -1
  81. package/src/actions/guides/legends/categorical/index.js +2 -2
  82. package/src/actions/guides/legends/categorical/layout.js +148 -175
  83. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  84. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  85. package/src/actions/guides/legends/continuous/common.js +117 -66
  86. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  87. package/src/actions/guides/legends/continuous/interval.js +97 -76
  88. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  89. package/src/actions/guides/legends/edit.js +14 -1
  90. package/src/actions/guides/legends/index.js +1 -1
  91. package/src/actions/guides/legends/lane.js +2 -1
  92. package/src/actions/guides/legends/remove.js +8 -0
  93. package/src/actions/guides/legends/size.js +36 -43
  94. package/src/actions/guides/legends/strokeWidth.js +20 -26
  95. package/src/actions/guides/polar/axes/labels.js +17 -1
  96. package/src/actions/guides/polar/axes/shared.js +7 -1
  97. package/src/actions/guides/polar/axes/titles.js +19 -2
  98. package/src/actions/guides/polar/grids.js +4 -0
  99. package/src/actions/guides/polar/resolve.js +6 -0
  100. package/src/actions/index.js +13 -13
  101. package/src/actions/intervals/resolve.js +87 -117
  102. package/src/actions/marks/arc/actions.js +31 -36
  103. package/src/actions/marks/area/actions.js +23 -31
  104. package/src/actions/marks/index.js +6 -6
  105. package/src/actions/marks/line/actions.js +33 -52
  106. package/src/actions/marks/line/materialize.js +7 -5
  107. package/src/actions/marks/point/materialize.js +5 -3
  108. package/src/actions/marks/remove.js +21 -0
  109. package/src/actions/marks/rule/actions.js +53 -53
  110. package/src/actions/marks/shared.js +8 -4
  111. package/src/actions/marks/text/actions.js +14 -6
  112. package/src/actions/marks/text/layout.js +21 -45
  113. package/src/actions/marks/tick/actions.js +22 -21
  114. package/src/actions/primitives/createGraphics.js +4 -0
  115. package/src/actions/primitives/editGraphics.js +4 -0
  116. package/src/actions/primitives/index.js +3 -3
  117. package/src/actions/primitives/semantic.js +1 -1
  118. package/src/actions/primitives/semanticValidation/index.js +2 -2
  119. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  120. package/src/actions/regression/edit.js +3 -43
  121. package/src/actions/scales/consumers/families.js +24 -4
  122. package/src/actions/scales/consumers/index.js +1 -1
  123. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  124. package/src/actions/scales/definitions.js +100 -103
  125. package/src/actions/scales/materialize.js +5 -0
  126. package/src/actions/selection/actions.js +79 -121
  127. package/src/actions/titles/actions.js +48 -94
  128. package/src/actions/titles/resolve.js +14 -27
  129. package/src/actions/violinPlots/create.js +37 -44
  130. package/src/core/textMetrics.js +74 -37
  131. package/src/core/validation.js +18 -0
  132. package/src/grammar/aggregate.js +48 -28
  133. package/src/grammar/arcs.js +58 -11
  134. package/src/grammar/areaSeries.js +14 -16
  135. package/src/grammar/bin2d.js +37 -23
  136. package/src/grammar/boxPlot.js +14 -13
  137. package/src/grammar/categoricalDensity.js +14 -14
  138. package/src/grammar/categoryOrder.js +8 -14
  139. package/src/grammar/curveCommands.js +87 -20
  140. package/src/grammar/density.js +162 -141
  141. package/src/grammar/facets/index.js +14 -0
  142. package/src/grammar/facets/scales.js +7 -4
  143. package/src/grammar/gradientProfile.js +12 -15
  144. package/src/grammar/histogram.js +145 -83
  145. package/src/grammar/horizon.js +60 -23
  146. package/src/grammar/interval.js +14 -13
  147. package/src/grammar/lineSeries.js +91 -61
  148. package/src/grammar/numeric.js +290 -0
  149. package/src/grammar/paint.js +3 -4
  150. package/src/grammar/palettes.js +21 -32
  151. package/src/grammar/pathCommands.js +6 -0
  152. package/src/grammar/polarPaths.js +5 -5
  153. package/src/grammar/regression/derive.js +48 -9
  154. package/src/grammar/regression/models.js +248 -69
  155. package/src/grammar/regression/parameters.js +8 -0
  156. package/src/grammar/scales/color.js +35 -26
  157. package/src/grammar/scales/continuous.js +71 -24
  158. package/src/grammar/scales/definition.js +1 -1
  159. package/src/grammar/scales/discretized.js +42 -12
  160. package/src/grammar/scales/fields.js +21 -4
  161. package/src/grammar/scales/ordinal.js +117 -61
  162. package/src/grammar/scales/temporal.js +40 -11
  163. package/src/grammar/scales/transformed.js +199 -52
  164. package/src/grammar/schemas/graphicBounds.js +270 -79
  165. package/src/grammar/seriesLayout.js +50 -18
  166. package/src/grammar/ticks.js +123 -53
  167. package/src/grammar/timeUnit.js +18 -8
  168. package/src/grammar/transforms.js +4 -1
  169. package/src/grammar/window.js +70 -57
  170. package/src/index.js +1 -1
  171. package/src/layout/canvas.js +4 -8
  172. package/src/layout/composition.js +39 -40
  173. package/src/layout/facets.js +10 -14
  174. package/src/layout/labels.js +91 -15
  175. package/src/layout/legend.js +22 -20
  176. package/src/layout/legendLane.js +54 -40
  177. package/src/layout/title.js +7 -7
  178. package/src/materialization/bars/aggregate.js +14 -40
  179. package/src/materialization/bars/grouped.js +7 -18
  180. package/src/materialization/bars/histogram.js +2 -19
  181. package/src/materialization/bars/ranged.js +20 -15
  182. package/src/materialization/bars/resolve.js +21 -0
  183. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  184. package/src/materialization/facetGuides/placement.js +34 -19
  185. package/src/materialization/facetGuides/preparation.js +9 -5
  186. package/src/materialization/facets.js +39 -27
  187. package/src/materialization/guides/resources.js +2 -1
  188. package/src/materialization/marks/capabilities.js +5 -2
  189. package/src/materialization/marks/index.js +24 -4
  190. package/src/materialization/scales/policies/bar.js +61 -59
  191. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  192. package/src/materialization/scales/policies/series.js +13 -7
  193. package/src/materialization/scales/resolve.js +4 -5
  194. package/src/materialization/selection/items/arc.js +16 -2
  195. package/src/materialization/selection/items/bar.js +4 -2
  196. package/src/materialization/selection/items/common.js +21 -1
  197. package/src/materialization/selection/items/path.js +19 -2
  198. package/src/materialization/selection/items/point.js +4 -16
  199. package/src/materialization/selection/items/tick.js +13 -21
  200. package/src/materialization/selection/point.js +11 -4
  201. package/src/materialization/selection/styles.js +19 -27
  202. package/src/materialization/text.js +4 -1
  203. package/src/mcp/adapter.js +8 -3
  204. package/src/renderers/canvas/fill.js +2 -2
  205. package/src/renderers/canvas/index.js +35 -2
  206. package/src/renderers/canvas/native.js +160 -0
  207. package/src/renderers/pdf.js +14 -0
  208. package/src/renderers/svg.js +37 -4
  209. package/types/index.d.ts +3 -0
  210. package/types/program.d.ts +505 -131
  211. package/types/svg.d.ts +1 -0
  212. package/src/actions/errorBands/components.js +0 -81
  213. package/src/actions/errorBands/options.js +0 -39
  214. package/src/actions/errorBands/resolve.js +0 -28
  215. package/src/actions/errorBars/components.js +0 -62
  216. package/src/actions/errorBars/options.js +0 -58
  217. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,4 +1,5 @@
1
1
  import { freezeOwned, isPlainObject } from "../core/immutable.js";
2
+ import { validateGeneratedItemLimit } from "../core/validation.js";
2
3
  import { buildLinearPathCommands } from "./pathCommands.js";
3
4
 
4
5
  export const CURVE_INTERPOLATIONS = Object.freeze([
@@ -154,11 +155,12 @@ function buildCardinalCommands(points) {
154
155
  }
155
156
 
156
157
  function monotoneTangents(points) {
158
+ const direction = Math.sign(points[1].x - points[0].x);
157
159
  const intervals = points.slice(1).map((point, index) => {
158
160
  const previous = points[index];
159
161
  const width = point.x - previous.x;
160
- if (!(width > 0)) {
161
- throw new Error("Monotone curve requires strictly increasing x values.");
162
+ if (width === 0 || Math.sign(width) !== direction) {
163
+ throw new Error("Monotone curve requires strictly monotonic x values.");
162
164
  }
163
165
  return { width, slope: (point.y - previous.y) / width };
164
166
  });
@@ -263,26 +265,83 @@ function buildNaturalCommands(points) {
263
265
  return ownCommands(commands);
264
266
  }
265
267
 
266
- export function buildCurvePathCommands(points, curve = "linear") {
267
- const validatedPoints = validatePoints(points);
268
- const validatedCurve = validateCurveInterpolation(curve);
269
- if (validatedCurve === "linear" ||
270
- (SMOOTH_CURVES.has(validatedCurve) &&
271
- validatedCurve !== "monotone" &&
268
+ function buildValidatedCurvePathCommands(points, curve) {
269
+ if (curve === "linear" ||
270
+ (SMOOTH_CURVES.has(curve) &&
271
+ curve !== "monotone" &&
272
272
  points.length < 3)) {
273
- return buildLinearPathCommands(validatedPoints);
273
+ return buildLinearPathCommands(points);
274
274
  }
275
- if (validatedCurve.startsWith("step")) {
276
- return buildStepCommands(validatedPoints, validatedCurve);
275
+ if (curve.startsWith("step")) {
276
+ return buildStepCommands(points, curve);
277
277
  }
278
- if (validatedCurve === "basis") return buildBasisCommands(validatedPoints);
279
- if (validatedCurve === "cardinal") {
280
- return buildCardinalCommands(validatedPoints);
278
+ if (curve === "basis") return buildBasisCommands(points);
279
+ if (curve === "cardinal") {
280
+ return buildCardinalCommands(points);
281
281
  }
282
- if (validatedCurve === "monotone") {
283
- return buildMonotoneCommands(validatedPoints);
282
+ if (curve === "monotone") {
283
+ return buildMonotoneCommands(points);
284
+ }
285
+ return buildNaturalCommands(points);
286
+ }
287
+
288
+ function finiteCommands(commands) {
289
+ return commands.every(command => Object.values(command).every(value =>
290
+ typeof value !== "number" || Number.isFinite(value)
291
+ ));
292
+ }
293
+
294
+ function curveCommandCount(pointCount, curve) {
295
+ if (curve === "step") return pointCount * 3 - 2;
296
+ if (curve === "step-before" || curve === "step-after") {
297
+ return pointCount * 2 - 1;
284
298
  }
285
- return buildNaturalCommands(validatedPoints);
299
+ return curve === "basis" && pointCount > 2
300
+ ? pointCount + 2
301
+ : pointCount;
302
+ }
303
+
304
+ export function buildCurvePathCommands(points, curve = "linear") {
305
+ const validatedPoints = validatePoints(points);
306
+ const validatedCurve = validateCurveInterpolation(curve);
307
+ validateGeneratedItemLimit(
308
+ curveCommandCount(validatedPoints.length, validatedCurve),
309
+ "Curve path command count"
310
+ );
311
+ const direct = buildValidatedCurvePathCommands(
312
+ validatedPoints,
313
+ validatedCurve
314
+ );
315
+ if (finiteCommands(direct)) return direct;
316
+
317
+ let xScale = 0;
318
+ let yScale = 0;
319
+ for (const point of validatedPoints) {
320
+ xScale = Math.max(xScale, Math.abs(point.x));
321
+ yScale = Math.max(yScale, Math.abs(point.y));
322
+ }
323
+ xScale ||= 1;
324
+ yScale ||= 1;
325
+ const normalized = buildValidatedCurvePathCommands(
326
+ validatedPoints.map(point => ({
327
+ x: point.x / xScale,
328
+ y: point.y / yScale
329
+ })),
330
+ validatedCurve
331
+ );
332
+ const restored = normalized.map(command => {
333
+ const copy = {};
334
+ for (const [key, value] of Object.entries(command)) {
335
+ copy[key] = typeof value === "number"
336
+ ? value * (key[0] === "x" ? xScale : yScale)
337
+ : value;
338
+ }
339
+ return copy;
340
+ });
341
+ if (!finiteCommands(restored)) {
342
+ throw new RangeError("Curve interpolation exceeds the finite numeric range.");
343
+ }
344
+ return ownCommands(restored);
286
345
  }
287
346
 
288
347
  function reverseOpenPathCommands(commands) {
@@ -341,13 +400,21 @@ export function buildAreaCurvePathCommands(
341
400
  curve = "linear",
342
401
  { independentAxis = "x" } = {}
343
402
  ) {
403
+ const validatedCurve = validateCurveInterpolation(curve);
404
+ const validatedLower = validatePoints(lowerPoints);
405
+ const validatedUpper = validatePoints(upperPoints);
406
+ validateGeneratedItemLimit(
407
+ curveCommandCount(validatedLower.length, validatedCurve) +
408
+ curveCommandCount(validatedUpper.length, validatedCurve) + 1,
409
+ "Area path command count"
410
+ );
344
411
  const lower = buildOrientedCurvePathCommands(
345
- lowerPoints,
346
- curve,
412
+ validatedLower,
413
+ validatedCurve,
347
414
  independentAxis
348
415
  );
349
416
  const upper = reverseOpenPathCommands(
350
- buildOrientedCurvePathCommands(upperPoints, curve, independentAxis)
417
+ buildOrientedCurvePathCommands(validatedUpper, validatedCurve, independentAxis)
351
418
  );
352
419
  return ownCommands([
353
420
  ...lower,
@@ -1,14 +1,34 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ validateGeneratedItemLimit,
4
+ validateNonEmptyString as requireField,
5
+ validateWorkLimit
6
+ } from "../core/validation.js";
2
7
  import { isNominalValue } from "./scales/index.js";
8
+ import { interpolateNumber } from "./numeric.js";
9
+ import {
10
+ requireFiniteResult,
11
+ stableFiniteDeviation
12
+ } from "./numeric.js";
3
13
 
4
14
  const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
5
15
 
6
- export const DENSITY_KERNELS = Object.freeze([
7
- "gaussian",
8
- "epanechnikov",
9
- "uniform",
10
- "triangular"
11
- ]);
16
+ const KERNEL_FUNCTIONS = Object.freeze({
17
+ gaussian(value) {
18
+ return Math.exp(-0.5 * value ** 2) / SQRT_TWO_PI;
19
+ },
20
+ epanechnikov(value) {
21
+ return Math.abs(value) <= 1 ? 0.75 * (1 - value ** 2) : 0;
22
+ },
23
+ uniform(value) {
24
+ return Math.abs(value) <= 1 ? 0.5 : 0;
25
+ },
26
+ triangular(value) {
27
+ return Math.abs(value) <= 1 ? 1 - Math.abs(value) : 0;
28
+ }
29
+ });
30
+
31
+ export const DENSITY_KERNELS = Object.freeze(Object.keys(KERNEL_FUNCTIONS));
12
32
  export const DENSITY_NORMALIZATIONS = Object.freeze(["unit", "count"]);
13
33
  export const DENSITY_PLACEMENT_SIDES = Object.freeze([
14
34
  "both", "left", "right", "top", "bottom"
@@ -17,6 +37,13 @@ export const DENSITY_WIDTH_RESOLUTIONS = Object.freeze([
17
37
  "shared", "independent"
18
38
  ]);
19
39
 
40
+ export function validateDensitySteps(value, label = "Density steps") {
41
+ if (!Number.isInteger(value) || value < 2) {
42
+ throw new RangeError(`${label} must be an integer of at least 2.`);
43
+ }
44
+ return validateGeneratedItemLimit(value, label);
45
+ }
46
+
20
47
  export function validateDensityKernel(value) {
21
48
  if (!DENSITY_KERNELS.includes(value)) {
22
49
  throw new Error(`Unsupported density kernel "${value}".`);
@@ -31,9 +58,26 @@ export function validateDensityNormalization(value) {
31
58
  return value;
32
59
  }
33
60
 
34
- function requireField(value, label) {
35
- if (typeof value !== "string" || value.length === 0) {
36
- throw new TypeError(`${label} must be a non-empty string.`);
61
+ function validateDensityBandwidth(value) {
62
+ if (value !== "auto" && (!Number.isFinite(value) || value <= 0)) {
63
+ throw new RangeError(
64
+ "Density bandwidth must be a positive finite number or auto."
65
+ );
66
+ }
67
+ return value;
68
+ }
69
+
70
+ function validateDensityExtent(value) {
71
+ if (
72
+ value !== "auto" &&
73
+ (!Array.isArray(value) ||
74
+ value.length !== 2 ||
75
+ !value.every(Number.isFinite) ||
76
+ value[0] >= value[1])
77
+ ) {
78
+ throw new RangeError(
79
+ "Density extent must be an ascending pair of finite numbers or auto."
80
+ );
37
81
  }
38
82
  return value;
39
83
  }
@@ -52,6 +96,27 @@ function validateSplitDomain(value) {
52
96
  return [...value];
53
97
  }
54
98
 
99
+ function validateOutputFields(value, field, groupBy, placement) {
100
+ if (
101
+ !Array.isArray(value) ||
102
+ value.length !== 2 ||
103
+ !value.every(field => typeof field === "string" && field.length > 0) ||
104
+ value[0] === value[1]
105
+ ) {
106
+ throw new TypeError("Density as must contain two distinct non-empty fields.");
107
+ }
108
+ const collisions = new Set([
109
+ field,
110
+ groupBy,
111
+ placement?.categoryField,
112
+ placement?.split?.field
113
+ ].filter(Boolean));
114
+ if (value.some(field => collisions.has(field))) {
115
+ throw new Error("Density output fields must not collide with source or group fields.");
116
+ }
117
+ return value;
118
+ }
119
+
55
120
  function validatePlacementWidth(value = {}) {
56
121
  if (!isPlainObject(value)) {
57
122
  throw new TypeError("Density placement width must be a plain object.");
@@ -183,10 +248,9 @@ function validateStoredDensityPlacement(value, groupBy) {
183
248
  )
184
249
  );
185
250
  if (
186
- normalized.type !== value.type ||
187
- normalized.channel !== value.channel ||
188
- normalized.categoryField !== value.categoryField ||
189
- normalized.side !== value.side ||
251
+ ["type", "channel", "categoryField", "side"].some(
252
+ property => normalized[property] !== value[property]
253
+ ) ||
190
254
  value.width === undefined ||
191
255
  normalized.width.band !== value.width.band ||
192
256
  normalized.width.resolve !== value.width.resolve ||
@@ -218,47 +282,15 @@ export function validateDensityTransform(transform) {
218
282
  }
219
283
  validateDensityKernel(transform.kernel ?? "gaussian");
220
284
  validateDensityNormalization(transform.normalization ?? "unit");
221
- if (
222
- transform.bandwidth !== "auto" &&
223
- (!Number.isFinite(transform.bandwidth) || transform.bandwidth <= 0)
224
- ) {
225
- throw new RangeError(
226
- "Density bandwidth must be a positive finite number or auto."
227
- );
228
- }
229
- if (
230
- transform.extent !== "auto" &&
231
- (!Array.isArray(transform.extent) ||
232
- transform.extent.length !== 2 ||
233
- !transform.extent.every(Number.isFinite) ||
234
- transform.extent[0] >= transform.extent[1])
235
- ) {
236
- throw new RangeError(
237
- "Density extent must be an ascending pair of finite numbers or auto."
238
- );
239
- }
240
- if (!Number.isInteger(transform.steps) || transform.steps < 2) {
241
- throw new RangeError("Density steps must be an integer of at least 2.");
242
- }
243
- if (
244
- !Array.isArray(transform.as) ||
245
- transform.as.length !== 2 ||
246
- !transform.as.every(value => typeof value === "string" && value.length > 0) ||
247
- transform.as[0] === transform.as[1]
248
- ) {
249
- throw new TypeError("Density as must contain two distinct non-empty fields.");
250
- }
251
- const collisions = new Set([
285
+ validateDensityBandwidth(transform.bandwidth);
286
+ validateDensityExtent(transform.extent);
287
+ validateDensitySteps(transform.steps);
288
+ validateOutputFields(
289
+ transform.as,
252
290
  transform.field,
253
291
  transform.groupBy,
254
- transform.placement?.categoryField,
255
- transform.placement?.split?.field
256
- ].filter(Boolean));
257
- if (transform.as.some(value => collisions.has(value))) {
258
- throw new Error(
259
- "Density output fields must not collide with source or group fields."
260
- );
261
- }
292
+ transform.placement
293
+ );
262
294
  if (transform.resolve !== "shared") {
263
295
  throw new Error(`Unsupported density resolve "${transform.resolve}".`);
264
296
  }
@@ -300,17 +332,11 @@ function quantile(sortedValues, probability) {
300
332
  const upper = Math.ceil(index);
301
333
  if (lower === upper) return sortedValues[lower];
302
334
  const ratio = index - lower;
303
- return sortedValues[lower] * (1 - ratio) + sortedValues[upper] * ratio;
304
- }
305
-
306
- function sampleDeviation(values) {
307
- if (values.length < 2) return 0;
308
- const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
309
- const sumSquares = values.reduce(
310
- (sum, value) => sum + (value - mean) ** 2,
311
- 0
312
- );
313
- return Math.sqrt(sumSquares / (values.length - 1));
335
+ const ordinary = sortedValues[lower] * (1 - ratio) +
336
+ sortedValues[upper] * ratio;
337
+ return Number.isFinite(ordinary)
338
+ ? ordinary
339
+ : interpolateNumber(sortedValues[lower], sortedValues[upper], ratio);
314
340
  }
315
341
 
316
342
  export function estimateDensityBandwidth(values) {
@@ -321,13 +347,16 @@ export function estimateDensityBandwidth(values) {
321
347
  throw new Error("Density auto bandwidth requires at least two values.");
322
348
  }
323
349
  const sorted = [...values].sort((left, right) => left - right);
324
- const deviation = sampleDeviation(sorted);
350
+ const deviation = stableFiniteDeviation(sorted, {
351
+ sample: true,
352
+ label: "Density sample deviation"
353
+ }).deviation;
325
354
  const interquartileRange = quantile(sorted, 0.75) - quantile(sorted, 0.25);
326
355
  const robustDeviation = interquartileRange / 1.34;
327
356
  const spread = robustDeviation > 0
328
357
  ? Math.min(deviation, robustDeviation)
329
358
  : deviation;
330
- const bandwidth = 1.06 * spread * sorted.length ** -0.2;
359
+ const bandwidth = spread * (1.06 * sorted.length ** -0.2);
331
360
  if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
332
361
  throw new Error("Density auto bandwidth requires varying finite values.");
333
362
  }
@@ -338,72 +367,26 @@ function resolveBandwidth(value, sourceValues) {
338
367
  if (value === undefined || value === "auto") {
339
368
  return estimateDensityBandwidth(sourceValues);
340
369
  }
341
- if (!Number.isFinite(value) || value <= 0) {
342
- throw new RangeError(
343
- "Density bandwidth must be a positive finite number or auto."
344
- );
345
- }
346
- return value;
370
+ return validateDensityBandwidth(value);
347
371
  }
348
372
 
349
373
  function resolveExtent(value, sourceValues) {
350
374
  if (value === undefined || value === "auto") {
351
- const extent = [Math.min(...sourceValues), Math.max(...sourceValues)];
375
+ let lower = Infinity;
376
+ let upper = -Infinity;
377
+ for (const sourceValue of sourceValues) {
378
+ lower = Math.min(lower, sourceValue);
379
+ upper = Math.max(upper, sourceValue);
380
+ }
381
+ const extent = [lower, upper];
352
382
  if (extent[0] === extent[1]) {
353
383
  throw new Error("Density observed extent requires varying finite values.");
354
384
  }
355
385
  return extent;
356
386
  }
357
- if (
358
- !Array.isArray(value) ||
359
- value.length !== 2 ||
360
- !value.every(Number.isFinite) ||
361
- value[0] >= value[1]
362
- ) {
363
- throw new RangeError(
364
- "Density extent must be an ascending pair of finite numbers or auto."
365
- );
366
- }
367
- return [...value];
387
+ return [...validateDensityExtent(value)];
368
388
  }
369
389
 
370
- function resolveOutputFields(as, field, groupBy, placement) {
371
- const resolved = as ?? [`${field}_value`, `${field}_density`];
372
- if (
373
- !Array.isArray(resolved) ||
374
- resolved.length !== 2 ||
375
- !resolved.every(value => typeof value === "string" && value.length > 0) ||
376
- resolved[0] === resolved[1]
377
- ) {
378
- throw new TypeError("Density as must contain two distinct non-empty fields.");
379
- }
380
- const collisions = new Set([
381
- field,
382
- groupBy,
383
- placement?.categoryField,
384
- placement?.split?.field
385
- ].filter(Boolean));
386
- if (resolved.some(value => collisions.has(value))) {
387
- throw new Error("Density output fields must not collide with source or group fields.");
388
- }
389
- return [...resolved];
390
- }
391
-
392
- const KERNEL_FUNCTIONS = Object.freeze({
393
- gaussian(value) {
394
- return Math.exp(-0.5 * value ** 2) / SQRT_TWO_PI;
395
- },
396
- epanechnikov(value) {
397
- return Math.abs(value) <= 1 ? 0.75 * (1 - value ** 2) : 0;
398
- },
399
- uniform(value) {
400
- return Math.abs(value) <= 1 ? 0.5 : 0;
401
- },
402
- triangular(value) {
403
- return Math.abs(value) <= 1 ? 1 - Math.abs(value) : 0;
404
- }
405
- });
406
-
407
390
  function estimateAt(sample, values, bandwidth, kernel, normalization) {
408
391
  const sum = values.reduce(
409
392
  (total, value) => total + KERNEL_FUNCTIONS[kernel](
@@ -414,7 +397,14 @@ function estimateAt(sample, values, bandwidth, kernel, normalization) {
414
397
  const denominator = normalization === "unit"
415
398
  ? values.length * bandwidth
416
399
  : bandwidth;
417
- return sum / denominator;
400
+ const ordinary = sum / denominator;
401
+ if (Number.isFinite(denominator) && Number.isFinite(ordinary)) {
402
+ return ordinary;
403
+ }
404
+ const stable = normalization === "unit"
405
+ ? (sum / values.length) / bandwidth
406
+ : sum / bandwidth;
407
+ return requireFiniteResult(stable, "Density estimate");
418
408
  }
419
409
 
420
410
  export function deriveKernelDensity(values, {
@@ -435,13 +425,16 @@ export function deriveKernelDensity(values, {
435
425
  const groupField = groupBy === undefined
436
426
  ? undefined
437
427
  : requireField(groupBy, "Density groupBy");
438
- if (!Number.isInteger(steps) || steps < 2) {
439
- throw new RangeError("Density steps must be an integer of at least 2.");
440
- }
428
+ validateDensitySteps(steps);
441
429
  if (placement !== undefined) {
442
430
  validateStoredDensityPlacement(placement, groupField);
443
431
  }
444
- const outputFields = resolveOutputFields(as, sourceField, groupField, placement);
432
+ const outputFields = [...validateOutputFields(
433
+ as ?? [`${sourceField}_value`, `${sourceField}_density`],
434
+ sourceField,
435
+ groupField,
436
+ placement
437
+ )];
445
438
  const resolvedKernel = validateDensityKernel(kernel);
446
439
  const resolvedNormalization = validateDensityNormalization(normalization);
447
440
  const validRows = values.filter(row =>
@@ -454,12 +447,23 @@ export function deriveKernelDensity(values, {
454
447
  if (validRows.length === 0) {
455
448
  throw new Error("Density requires at least one valid field/group row.");
456
449
  }
450
+ validateWorkLimit(validRows.length * steps, "Density computation");
457
451
  const sourceValues = validRows.map(row => row[sourceField]);
458
452
  const resolvedBandwidth = resolveBandwidth(bandwidth, sourceValues);
459
453
  const resolvedExtent = resolveExtent(extent, sourceValues);
460
- const groups = groupField === undefined
461
- ? [undefined]
462
- : [...new Set(validRows.map(row => row[groupField]))];
454
+ const groupedValues = new Map();
455
+ for (const row of validRows) {
456
+ const group = groupField === undefined ? undefined : row[groupField];
457
+ const split = placement?.split === undefined
458
+ ? undefined
459
+ : row[placement.split.field];
460
+ const bySplit = groupedValues.get(group) ?? new Map();
461
+ const groupValues = bySplit.get(split) ?? [];
462
+ groupValues.push(row[sourceField]);
463
+ bySplit.set(split, groupValues);
464
+ groupedValues.set(group, bySplit);
465
+ }
466
+ const groups = [...groupedValues.keys()];
463
467
  const observedSplits = placement?.split === undefined
464
468
  ? []
465
469
  : [...new Set(validRows.map(row => row[placement.split.field]))];
@@ -477,27 +481,44 @@ export function deriveKernelDensity(values, {
477
481
  throw new Error("Density split domain must include every observed split value.");
478
482
  }
479
483
  }
480
- const sampleStep = (resolvedExtent[1] - resolvedExtent[0]) / (steps - 1);
484
+ const splits = splitDomain ?? [undefined];
485
+ let outputGroups = 0;
486
+ for (const group of groups) {
487
+ for (const split of splits) {
488
+ if (groupedValues.get(group).has(split)) outputGroups += 1;
489
+ }
490
+ }
491
+ validateGeneratedItemLimit(
492
+ outputGroups * steps,
493
+ "Density generated row count"
494
+ );
495
+ const extentSpan = resolvedExtent[1] - resolvedExtent[0];
496
+ const sampleStep = extentSpan / (steps - 1);
481
497
  const samples = Array.from(
482
498
  { length: steps },
483
499
  (_, index) => index === steps - 1
484
500
  ? resolvedExtent[1]
485
- : placement === undefined
486
- ? resolvedExtent[0] + sampleStep * index
487
- : resolvedExtent[0] +
488
- (resolvedExtent[1] - resolvedExtent[0]) * index / (steps - 1)
501
+ : Number.isFinite(extentSpan)
502
+ ? placement === undefined
503
+ ? resolvedExtent[0] + sampleStep * index
504
+ : resolvedExtent[0] + extentSpan * index / (steps - 1)
505
+ : interpolateNumber(
506
+ resolvedExtent[0],
507
+ resolvedExtent[1],
508
+ index / (steps - 1)
509
+ )
489
510
  );
511
+ if (samples.some((sample, index) =>
512
+ !Number.isFinite(sample) ||
513
+ (index > 0 && sample <= samples[index - 1]))) {
514
+ throw new RangeError(
515
+ "Density extent cannot represent the requested finite sample grid."
516
+ );
517
+ }
490
518
  const rows = [];
491
519
  for (const group of groups) {
492
- const splits = splitDomain ?? [undefined];
493
520
  for (const split of splits) {
494
- const groupValues = validRows
495
- .filter(row =>
496
- (groupField === undefined || Object.is(row[groupField], group)) &&
497
- (placement?.split === undefined ||
498
- Object.is(row[placement.split.field], split))
499
- )
500
- .map(row => row[sourceField]);
521
+ const groupValues = groupedValues.get(group).get(split) ?? [];
501
522
  if (groupValues.length === 0) continue;
502
523
  for (const sample of samples) {
503
524
  rows.push({
@@ -1,5 +1,9 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
+ import {
4
+ validateGeneratedItemLimit,
5
+ validateWorkLimit
6
+ } from "../../core/validation.js";
3
7
  import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
4
8
  import { planFacetDependencies } from "./dependencies.js";
5
9
  import { readNominalField } from "../scales/index.js";
@@ -12,6 +16,7 @@ const SUPPORTED_BAR_GRAINS = new Set([
12
16
  BAR_GRAINS.aggregate,
13
17
  BAR_GRAINS.ranged
14
18
  ]);
19
+ const MAX_FACET_CHILDREN = 100;
15
20
 
16
21
  function requireFacetField(field) {
17
22
  if (typeof field !== "string" || field.length === 0) {
@@ -109,6 +114,15 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
109
114
  throw new Error(`facet field "${field}" has no values.`);
110
115
  }
111
116
  const values = resolveValues(observed, options.values);
117
+ validateGeneratedItemLimit(
118
+ values.length,
119
+ "Facet child count",
120
+ MAX_FACET_CHILDREN
121
+ );
122
+ validateWorkLimit(
123
+ dataset.values.length * values.length,
124
+ "Facet partition work"
125
+ );
112
126
  return cloneAndFreeze({
113
127
  id,
114
128
  data,
@@ -131,10 +131,13 @@ function continuousUnion(domains, id) {
131
131
  )) {
132
132
  throw new TypeError(`Facet shared scale "${id}" requires finite pair domains.`);
133
133
  }
134
- return [
135
- Math.min(...domains.map(domain => Math.min(...domain))),
136
- Math.max(...domains.map(domain => Math.max(...domain)))
137
- ];
134
+ let minimum = Infinity;
135
+ let maximum = -Infinity;
136
+ for (const domain of domains) {
137
+ minimum = Math.min(minimum, domain[0], domain[1]);
138
+ maximum = Math.max(maximum, domain[0], domain[1]);
139
+ }
140
+ return [minimum, maximum];
138
141
  }
139
142
 
140
143
  function stableUnion(domains) {
@@ -2,8 +2,10 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import {
3
3
  deriveKernelDensity,
4
4
  validateDensityKernel,
5
- validateDensityNormalization
5
+ validateDensityNormalization,
6
+ validateDensitySteps
6
7
  } from "./density.js";
8
+ import { finiteMidpoint, stableFiniteMean } from "./numeric.js";
7
9
 
8
10
  export const GRADIENT_PROFILE_FIELDS = Object.freeze({
9
11
  category: "__gradientPlot_category",
@@ -15,11 +17,11 @@ export const GRADIENT_PROFILE_FIELDS = Object.freeze({
15
17
  count: "__gradientPlot_count"
16
18
  });
17
19
 
18
- const TRANSFORM_KEYS = Object.freeze([
20
+ const TRANSFORM_KEYS = [
19
21
  "type", "category", "field", "bandwidth", "extent", "steps", "kernel",
20
22
  "normalization", "center", "as", "resolve", "resolved"
21
- ]);
22
- const OUTPUT_KEYS = Object.freeze(Object.keys(GRADIENT_PROFILE_FIELDS));
23
+ ];
24
+ const OUTPUT_KEYS = Object.keys(GRADIENT_PROFILE_FIELDS);
23
25
 
24
26
  function requireField(value, label) {
25
27
  if (typeof value !== "string" || value.length === 0) {
@@ -92,9 +94,7 @@ export function validateGradientProfileTransform(value) {
92
94
  if (value.extent !== "auto") {
93
95
  validatePair(value.extent, "Gradient profile extent");
94
96
  }
95
- if (!Number.isInteger(value.steps) || value.steps < 2) {
96
- throw new RangeError("Gradient profile steps must be an integer of at least 2.");
97
- }
97
+ validateDensitySteps(value.steps, "Gradient profile steps");
98
98
  validateDensityKernel(value.kernel);
99
99
  validateDensityNormalization(value.normalization);
100
100
  if (!["mean", "median"].includes(value.center)) {
@@ -148,14 +148,10 @@ export function requestedGradientProfileTransform(transform) {
148
148
  }
149
149
 
150
150
  function centerOf(values, type) {
151
- if (type === "mean") {
152
- return values.reduce((sum, value) => sum + value, 0) / values.length;
153
- }
151
+ if (type === "mean") return stableFiniteMean(values);
154
152
  const sorted = [...values].sort((left, right) => left - right);
155
153
  const middle = (sorted.length - 1) / 2;
156
- const lower = Math.floor(middle);
157
- const upper = Math.ceil(middle);
158
- return (sorted[lower] + sorted[upper]) / 2;
154
+ return finiteMidpoint(sorted[Math.floor(middle)], sorted[Math.ceil(middle)]);
159
155
  }
160
156
 
161
157
  export function deriveGradientProfiles(rows, transform) {
@@ -178,8 +174,9 @@ export function deriveGradientProfiles(rows, transform) {
178
174
  row[transform.category] !== undefined && row[transform.category] !== null &&
179
175
  row[transform.category] !== "" && Number.isFinite(row[transform.field])
180
176
  );
181
- const maximumIntensity = Math.max(
182
- ...density.values.map(row => row.__gradientProfile_intensity)
177
+ const maximumIntensity = density.values.reduce(
178
+ (maximum, row) => Math.max(maximum, row.__gradientProfile_intensity),
179
+ -Infinity
183
180
  );
184
181
  if (!Number.isFinite(maximumIntensity) || maximumIntensity <= 0) {
185
182
  throw new Error("Gradient profile requires a positive resolved density range.");