ggaction 0.0.9 → 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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  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/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  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/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,8 +1,10 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
+ import { validateGeneratedItemLimit } from "../../core/validation.js";
2
3
  import {
3
4
  normalizePalette,
4
5
  resolveContinuousPalette
5
6
  } from "../palettes.js";
7
+ import { inverseLerp } from "../numeric.js";
6
8
 
7
9
  export const CONTINUOUS_COLOR_INTERPOLATIONS = cloneAndFreeze([
8
10
  "rgb",
@@ -105,6 +107,10 @@ function rgbToHex({ r, g, b }) {
105
107
  ).join("")}`;
106
108
  }
107
109
 
110
+ function byteChannels({ r, g, b }) {
111
+ return [r, g, b].map(channel => Math.round(channel * 255));
112
+ }
113
+
108
114
  function rgbToHsl({ r, g, b }) {
109
115
  const maximum = Math.max(r, g, b);
110
116
  const minimum = Math.min(r, g, b);
@@ -213,10 +219,6 @@ function interpolateHue(left, right, amount, long) {
213
219
  }
214
220
 
215
221
  function mix(left, right, amount, method) {
216
- const linear = key => left[key] + (right[key] - left[key]) * amount;
217
- if (method === "rgb") {
218
- return { r: linear("r"), g: linear("g"), b: linear("b") };
219
- }
220
222
  if (method.startsWith("hsl")) {
221
223
  const a = rgbToHsl(left);
222
224
  const b = rgbToHsl(right);
@@ -263,6 +265,7 @@ export function validateContinuousColorInterpolation(value) {
263
265
  export function validateSequentialColorRange(value) {
264
266
  if (value === "auto") return value;
265
267
  if (Array.isArray(value)) {
268
+ validateGeneratedItemLimit(value.length, "Sequential color range length");
266
269
  if (value.length < 2) {
267
270
  throw new TypeError("Sequential color range requires at least two colors.");
268
271
  }
@@ -293,29 +296,33 @@ export function resolveSequentialColorStops(value) {
293
296
  : resolveContinuousPalette(range.palette);
294
297
  }
295
298
 
299
+ function interpolateColors(colors, position, method, shouldClamp) {
300
+ const bounded = shouldClamp ? clamp(position) : position;
301
+ const scaled = bounded * (colors.length - 1);
302
+ const left = Math.max(0, Math.min(colors.length - 2, Math.floor(scaled)));
303
+ const right = left + 1;
304
+ const amount = scaled - left;
305
+ if (method === "rgb") {
306
+ const leftChannels = byteChannels(colors[left]);
307
+ const rightChannels = byteChannels(colors[right]);
308
+ return `#${leftChannels.map((channel, index) =>
309
+ Math.round(clamp(
310
+ channel + (rightChannels[index] - channel) * amount,
311
+ 0,
312
+ 255
313
+ )).toString(16).padStart(2, "0")
314
+ ).join("")}`;
315
+ }
316
+ return rgbToHex(mix(colors[left], colors[right], amount, method));
317
+ }
318
+
296
319
  export function interpolateColorStops(stops, position, interpolation = "rgb") {
297
320
  const method = validateContinuousColorInterpolation(interpolation);
298
321
  if (!Array.isArray(stops) || stops.length < 2) {
299
322
  throw new TypeError("Color interpolation requires at least two stops.");
300
323
  }
301
324
  const colors = stops.map(parseColor);
302
- const bounded = clamp(position);
303
- const scaled = bounded * (colors.length - 1);
304
- const left = Math.floor(scaled);
305
- const right = Math.min(colors.length - 1, left + 1);
306
- if (method === "rgb") {
307
- const leftHex = rgbToHex(colors[left]);
308
- const rightHex = rgbToHex(colors[right]);
309
- const amount = scaled - left;
310
- return `#${[1, 3, 5].map(offset =>
311
- Math.round(
312
- Number.parseInt(leftHex.slice(offset, offset + 2), 16) +
313
- (Number.parseInt(rightHex.slice(offset, offset + 2), 16) -
314
- Number.parseInt(leftHex.slice(offset, offset + 2), 16)) * amount
315
- ).toString(16).padStart(2, "0")
316
- ).join("")}`;
317
- }
318
- return rgbToHex(mix(colors[left], colors[right], scaled - left, method));
325
+ return interpolateColors(colors, position, method, true);
319
326
  }
320
327
 
321
328
  export function mapSequentialColors(
@@ -337,15 +344,17 @@ export function mapSequentialColors(
337
344
  }
338
345
  const [start, end] = domain;
339
346
  const constant = start === end;
347
+ const method = validateContinuousColorInterpolation(interpolation);
348
+ if (!Array.isArray(stops) || stops.length < 2) {
349
+ throw new TypeError("Color interpolation requires at least two stops.");
350
+ }
351
+ const colors = stops.map(parseColor);
340
352
  return cloneAndFreeze(values.map(value => {
341
353
  if (!Number.isFinite(value)) {
342
354
  if (hasUnknown) return options.unknown;
343
355
  throw new TypeError("Sequential color values must be finite numbers.");
344
356
  }
345
- const raw = constant ? 0.5 : (value - start) / (end - start);
346
- if (!shouldClamp && (raw < 0 || raw > 1)) {
347
- return interpolateColorStops(stops, raw < 0 ? 0 : 1, interpolation);
348
- }
349
- return interpolateColorStops(stops, clamp(raw), interpolation);
357
+ const raw = constant ? 0.5 : inverseLerp(value, start, end);
358
+ return interpolateColors(colors, raw, method, shouldClamp);
350
359
  }));
351
360
  }
@@ -1,5 +1,13 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
2
  import { POLAR_POSITION_CHANNELS } from "../../core/vocabulary.js";
3
+ import {
4
+ alignNumericStep,
5
+ cleanNumericValue,
6
+ interpolateNumber,
7
+ inverseLerp,
8
+ niceNumericStep,
9
+ normalizeNumericRange
10
+ } from "../numeric.js";
3
11
  import { resolvePolarScaleRange } from "../polar.js";
4
12
  import { niceTimeDomain } from "./temporal.js";
5
13
  import {
@@ -24,29 +32,48 @@ export function resolveScaleDomain(domain, values) {
24
32
  return cloneAndFreeze([minimum, maximum]);
25
33
  }
26
34
 
27
- function niceLinearStep(span, count = 5) {
28
- const rough = span / Math.max(1, count);
29
- const power = 10 ** Math.floor(Math.log10(rough));
30
- const fraction = rough / power;
31
- const factor = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 3 ? 3
32
- : fraction <= 5 ? 5 : 10;
33
- return factor * power;
34
- }
35
-
36
35
  export function niceLinearDomain(domain) {
37
36
  const [minimum, maximum] = domain;
37
+ if (minimum === maximum) return cloneAndFreeze([minimum, maximum]);
38
+
39
+ const directSpan = maximum - minimum;
38
40
  const tolerance = Number.EPSILON * 16 * Math.max(
39
- 1, Math.abs(minimum), Math.abs(maximum)
41
+ 1,
42
+ Math.abs(minimum),
43
+ Math.abs(maximum)
40
44
  );
41
- if (maximum - minimum <= tolerance) {
42
- const midpoint = (minimum + maximum) / 2;
43
- return cloneAndFreeze([midpoint, midpoint]);
45
+ if (Number.isFinite(directSpan) && directSpan > tolerance) {
46
+ const directStep = niceNumericStep(directSpan);
47
+ const direct = [
48
+ Number((Math.floor(minimum / directStep) * directStep).toPrecision(12)),
49
+ Number((Math.ceil(maximum / directStep) * directStep).toPrecision(12))
50
+ ];
51
+ if (
52
+ direct.every(Number.isFinite) &&
53
+ direct[0] < direct[1] &&
54
+ direct[0] <= minimum &&
55
+ direct[1] >= maximum
56
+ ) {
57
+ return cloneAndFreeze(direct);
58
+ }
44
59
  }
45
- const step = niceLinearStep(maximum - minimum);
46
- return cloneAndFreeze([
47
- Number((Math.floor(minimum / step) * step).toPrecision(12)),
48
- Number((Math.ceil(maximum / step) * step).toPrecision(12))
49
- ]);
60
+
61
+ const normalized = normalizeNumericRange(minimum, maximum);
62
+ const normalizedStep = niceNumericStep(normalized.span);
63
+ const resolution = normalizedStep * normalized.scale;
64
+ let lower = cleanNumericValue(
65
+ alignNumericStep(normalized.start, normalizedStep, "floor") * normalized.scale,
66
+ resolution
67
+ );
68
+ let upper = cleanNumericValue(
69
+ alignNumericStep(normalized.end, normalizedStep, "ceil") * normalized.scale,
70
+ resolution
71
+ );
72
+
73
+ if (!Number.isFinite(lower) || lower > minimum) lower = minimum;
74
+ if (!Number.isFinite(upper) || upper < maximum) upper = maximum;
75
+ if (lower === upper) return cloneAndFreeze([minimum, maximum]);
76
+ return cloneAndFreeze([lower, upper]);
50
77
  }
51
78
 
52
79
  export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
@@ -80,9 +107,13 @@ export function resolveScaleRange(range, channel, bounds) {
80
107
  ) {
81
108
  throw new Error("Automatic position range requires graphical bounds.");
82
109
  }
83
- return channel === "x"
84
- ? cloneAndFreeze([bounds.x, bounds.x + bounds.width])
85
- : cloneAndFreeze([bounds.y + bounds.height, bounds.y]);
110
+ const resolved = channel === "x"
111
+ ? [bounds.x, bounds.x + bounds.width]
112
+ : [bounds.y + bounds.height, bounds.y];
113
+ if (!resolved.every(Number.isFinite)) {
114
+ throw new RangeError("Automatic position range exceeds the finite numeric range.");
115
+ }
116
+ return cloneAndFreeze(resolved);
86
117
  }
87
118
 
88
119
  export function mapLinearValues(values, domain, range, options = {}) {
@@ -97,7 +128,7 @@ export function mapLinearValues(values, domain, range, options = {}) {
97
128
  throw new TypeError("Linear scale values must be finite numbers.");
98
129
  }
99
130
  if (domainStart === domainEnd) {
100
- const midpoint = (rangeStart + rangeEnd) / 2;
131
+ const midpoint = interpolateNumber(rangeStart, rangeEnd, 0.5);
101
132
  return cloneAndFreeze(values.map(value =>
102
133
  Number.isFinite(value) ? midpoint : options.unknown
103
134
  ));
@@ -109,8 +140,24 @@ export function mapLinearValues(values, domain, range, options = {}) {
109
140
  if (hasUnknown) return options.unknown;
110
141
  throw new TypeError("Linear scale values must be finite numbers.");
111
142
  }
112
- const proportion = (value - domainStart) / domainSpan;
143
+ if (value === domainStart) return rangeStart;
144
+ if (value === domainEnd) return rangeEnd;
145
+ const directProportion = (value - domainStart) / domainSpan;
146
+ const directResolved = clamp
147
+ ? Math.max(0, Math.min(1, directProportion))
148
+ : directProportion;
149
+ if (Number.isFinite(domainSpan) && directResolved === 0) return rangeStart;
150
+ if (Number.isFinite(domainSpan) && directResolved === 1) return rangeEnd;
151
+ const directValue = rangeStart + directResolved * rangeSpan;
152
+ if (
153
+ Number.isFinite(domainSpan) &&
154
+ Number.isFinite(rangeSpan) &&
155
+ Number.isFinite(directValue)
156
+ ) {
157
+ return directValue;
158
+ }
159
+ const proportion = inverseLerp(value, domainStart, domainEnd);
113
160
  const resolved = clamp ? Math.max(0, Math.min(1, proportion)) : proportion;
114
- return rangeStart + resolved * rangeSpan;
161
+ return interpolateNumber(rangeStart, rangeEnd, resolved);
115
162
  }));
116
163
  }
@@ -118,7 +118,7 @@ export function normalizeScaleDefinition({
118
118
  if (type === "pow") definition.exponent = parameters.exponent;
119
119
  if (type === "symlog") definition.constant = parameters.constant;
120
120
  if (definition.domain !== "auto") {
121
- validateTransformedDomain(type, definition.domain, parameters);
121
+ validateTransformedDomain(type, definition.domain, requested);
122
122
  }
123
123
  } else {
124
124
  for (const property of Object.keys(requested)) {
@@ -1,4 +1,6 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import { validateGeneratedItemLimit } from "../../core/validation.js";
3
+ import { interpolateNumber } from "../numeric.js";
2
4
  import { resolveColorRange, validateColorRange } from "./appearance.js";
3
5
  import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
4
6
 
@@ -35,7 +37,7 @@ function quantile(sorted, probability) {
35
37
  const position = (sorted.length - 1) * probability;
36
38
  const lower = Math.floor(position);
37
39
  const upper = Math.ceil(position);
38
- return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
40
+ return interpolateNumber(sorted[lower], sorted[upper], position - lower);
39
41
  }
40
42
 
41
43
  export function validateDiscretizedColorDomain(type, domain) {
@@ -62,6 +64,12 @@ export function validateDiscretizedColorDomain(type, domain) {
62
64
  }
63
65
 
64
66
  export function validateDiscretizedColorRange(range) {
67
+ if (Array.isArray(range)) {
68
+ validateGeneratedItemLimit(
69
+ range.length,
70
+ "Discretized color range length"
71
+ );
72
+ }
65
73
  const validated = validateColorRange(range);
66
74
  if (validated === "auto") return validated;
67
75
  if (Array.isArray(validated) && validated.length < 2) {
@@ -76,7 +84,7 @@ export function resolveDiscretizedColorScale({ type, domain, range, values }) {
76
84
  const requestedDomain = validateDiscretizedColorDomain(type, domain);
77
85
  const sample = [...values].sort((left, right) => left - right);
78
86
  const validatedRange = validateDiscretizedColorRange(range);
79
- const colorCount = type === "threshold" && requestedDomain !== "auto"
87
+ const colorCount = type === "threshold"
80
88
  ? requestedDomain.length + 1
81
89
  : Array.isArray(range) ? range.length : 5;
82
90
  const colors = resolveColorRange(
@@ -97,9 +105,20 @@ export function resolveDiscretizedColorScale({ type, domain, range, values }) {
97
105
  }
98
106
  thresholds = Array.from(
99
107
  { length: colors.length - 1 },
100
- (_, index) => resolvedDomain[0] + (index + 1) / colors.length *
101
- (resolvedDomain[1] - resolvedDomain[0])
108
+ (_, index) => interpolateNumber(
109
+ resolvedDomain[0],
110
+ resolvedDomain[1],
111
+ (index + 1) / colors.length
112
+ )
102
113
  );
114
+ if (thresholds.some((value, index) =>
115
+ value <= (index === 0 ? resolvedDomain[0] : thresholds[index - 1]) ||
116
+ value >= resolvedDomain[1]
117
+ )) {
118
+ throw new RangeError(
119
+ "Quantize color range requests more classes than its numeric domain can represent."
120
+ );
121
+ }
103
122
  } else if (type === "quantile") {
104
123
  const source = requestedDomain === "auto"
105
124
  ? sample
@@ -143,17 +162,28 @@ export function mapDiscretizedColors(values, scale) {
143
162
  }));
144
163
  }
145
164
 
146
- function formatBoundary(value) {
147
- return Number.isInteger(value) ? String(value) : value.toFixed(1);
148
- }
149
-
150
165
  export function formatDiscretizedIntervals(thresholds) {
151
166
  nondecreasing(thresholds, "Discretized color thresholds");
167
+ let resolution = Infinity;
168
+ for (let index = 1; index < thresholds.length; index += 1) {
169
+ const gap = thresholds[index] - thresholds[index - 1];
170
+ if (Number.isFinite(gap) && gap > 0) resolution = Math.min(resolution, gap);
171
+ }
172
+ let labels = thresholds.map(value =>
173
+ Number.isInteger(value) ? String(value) : value.toFixed(1)
174
+ );
175
+ if (thresholds.some((value, index) => {
176
+ const rounded = Number(labels[index]);
177
+ return value !== 0 && rounded === 0 ||
178
+ index > 0 && value !== thresholds[index - 1] &&
179
+ labels[index] === labels[index - 1] ||
180
+ !Number.isInteger(value) && Math.abs(rounded - value) > resolution / 4;
181
+ })) labels = thresholds.map(String);
152
182
  return cloneAndFreeze([
153
- `< ${formatBoundary(thresholds[0])}`,
154
- ...thresholds.slice(0, -1).map((value, index) =>
155
- `${formatBoundary(value)}–${formatBoundary(thresholds[index + 1])}`
183
+ `< ${labels[0]}`,
184
+ ...labels.slice(0, -1).map((value, index) =>
185
+ `${value}–${labels[index + 1]}`
156
186
  ),
157
- `≥ ${formatBoundary(thresholds.at(-1))}`
187
+ `≥ ${labels.at(-1)}`
158
188
  ]);
159
189
  }
@@ -55,8 +55,11 @@ export function readQuantitativeField(rows, field) {
55
55
  }
56
56
 
57
57
  function utcDate(year, month = 1, day = 1) {
58
- const timestamp = Date.UTC(year, month - 1, day);
59
- const date = new Date(timestamp);
58
+ const date = new Date(0);
59
+ date.setUTCFullYear(year, month - 1, day);
60
+ date.setUTCHours(0, 0, 0, 0);
61
+ const timestamp = date.getTime();
62
+ if (!Number.isFinite(timestamp)) return undefined;
60
63
  return date.getUTCFullYear() === year &&
61
64
  date.getUTCMonth() === month - 1 &&
62
65
  date.getUTCDate() === day
@@ -64,6 +67,20 @@ function utcDate(year, month = 1, day = 1) {
64
67
  : undefined;
65
68
  }
66
69
 
70
+ function parseTemporalString(value) {
71
+ const isoDateTime =
72
+ /^(\d{4})[-/](\d{2})[-/](\d{2})[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(Z|[+-]\d{2}:?\d{2})?$/iu
73
+ .exec(value);
74
+ if (isoDateTime === null) return Date.parse(value);
75
+ const [, year, month, day, time, zone] = isoDateTime;
76
+ const normalized = `${year}-${month}-${day}T${time}${zone ?? "Z"}`;
77
+ return Date.parse(normalized);
78
+ }
79
+
80
+ function validTimestamp(value) {
81
+ return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
82
+ }
83
+
67
84
  export function normalizeTemporalValue(value, field = "value", index = 0) {
68
85
  let timestamp;
69
86
  if (typeof value === "number") {
@@ -77,9 +94,9 @@ export function normalizeTemporalValue(value, field = "value", index = 0) {
77
94
  ? utcDate(Number(year[1]))
78
95
  : date !== null
79
96
  ? utcDate(Number(date[1]), Number(date[2]), Number(date[3]))
80
- : Date.parse(value);
97
+ : parseTemporalString(value);
81
98
  }
82
- if (!Number.isFinite(timestamp)) {
99
+ if (!validTimestamp(timestamp)) {
83
100
  throw new TypeError(
84
101
  `Field "${field}" must contain a temporal string or finite timestamp ` +
85
102
  `(including a valid date or four-digit year) at row ${index}.`
@@ -3,6 +3,54 @@ import { isNominalValue } from "./fields.js";
3
3
  import { validateColorRange, validateStrokeDashRange } from "./appearance.js";
4
4
  import { validatePair, validateScaleRange } from "./validation.js";
5
5
  import { resolveScaleRange } from "./continuous.js";
6
+ import { interpolateNumber } from "../numeric.js";
7
+
8
+ function divideRange(range, denominator, label) {
9
+ const direct = (range[1] - range[0]) / denominator;
10
+ if (Number.isFinite(direct) && (direct !== 0 || range[0] === range[1])) {
11
+ return direct;
12
+ }
13
+ const step = interpolateNumber(range[0], range[1], 1 / denominator) - range[0];
14
+ if (!Number.isFinite(step) || (step === 0 && range[0] !== range[1])) {
15
+ throw new RangeError(`${label} exceeds the finite numeric range.`);
16
+ }
17
+ return step;
18
+ }
19
+
20
+ function requireDistinctPositions(scale, label) {
21
+ const positions = mapOrdinalPositionValues(scale.domain, scale);
22
+ if (positions.some((position, index) =>
23
+ !Number.isFinite(position) ||
24
+ index > 0 && position === positions[index - 1]
25
+ )) {
26
+ throw new RangeError(`${label} cannot represent every domain position.`);
27
+ }
28
+ }
29
+
30
+ function finitePosition(value, scale, proportion) {
31
+ return Number.isFinite(value)
32
+ ? value
33
+ : interpolateNumber(scale.range[0], scale.range[1], proportion);
34
+ }
35
+
36
+ function resolvePositionRange(range, channel, bounds, label) {
37
+ const resolved = range === "auto" && channel === "y"
38
+ ? cloneAndFreeze([bounds.y, bounds.y + bounds.height])
39
+ : resolveScaleRange(range, channel, bounds);
40
+ if (!resolved.every(Number.isFinite)) {
41
+ throw new RangeError(`Automatic ${label} range exceeds the finite numeric range.`);
42
+ }
43
+ return resolved;
44
+ }
45
+
46
+ function validateDomainValues(domain, values, unknown, label) {
47
+ const domainValues = new Set(domain);
48
+ for (const value of values) {
49
+ if (!domainValues.has(value) && unknown === undefined) {
50
+ throw new Error(`Value "${value}" is outside the ${label} domain.`);
51
+ }
52
+ }
53
+ }
6
54
 
7
55
  export function validateOrdinalDomain(domain) {
8
56
  if (domain === "auto") return domain;
@@ -21,14 +69,14 @@ export function validateOrdinalDomain(domain) {
21
69
 
22
70
  export function validateOrdinalRange(range) {
23
71
  if (range === "auto") return range;
24
- if (Array.isArray(range) && range.length === 2 && range.every(Number.isFinite)) {
25
- return cloneAndFreeze(range);
26
- }
27
- if (Array.isArray(range) && range.every(item => typeof item === "string")) {
28
- return validateColorRange(range);
29
- }
30
- if (Array.isArray(range) && range.every(Array.isArray)) {
31
- return validateStrokeDashRange(range);
72
+ if (Array.isArray(range)) {
73
+ if (range.length === 2 && range.every(Number.isFinite)) {
74
+ return cloneAndFreeze(range);
75
+ }
76
+ if (range.every(item => typeof item === "string")) {
77
+ return validateColorRange(range);
78
+ }
79
+ if (range.every(Array.isArray)) return validateStrokeDashRange(range);
32
80
  }
33
81
  return validateColorRange(range);
34
82
  }
@@ -51,30 +99,22 @@ export function resolveOrdinalPositionScale({
51
99
  unknown
52
100
  }) {
53
101
  const resolvedDomain = resolveOrdinalDomain(domain, values);
54
- const resolvedRange = range === "auto" && channel === "y"
55
- ? cloneAndFreeze([bounds.y, bounds.y + bounds.height])
56
- : resolveScaleRange(range, channel, bounds);
57
- const domainValues = new Set(resolvedDomain);
58
- for (const value of values) {
59
- if (!domainValues.has(value) && unknown === undefined) {
60
- throw new Error(`Value "${value}" is outside the ordinal domain.`);
61
- }
62
- }
63
- const step = (resolvedRange[1] - resolvedRange[0]) / resolvedDomain.length;
64
- return cloneAndFreeze({
102
+ const resolvedRange = resolvePositionRange(range, channel, bounds, "ordinal");
103
+ validateDomainValues(resolvedDomain, values, unknown, "ordinal");
104
+ const step = divideRange(
105
+ resolvedRange,
106
+ resolvedDomain.length,
107
+ "Ordinal position step"
108
+ );
109
+ const resolved = {
65
110
  type: "ordinal",
66
111
  domain: resolvedDomain,
67
112
  range: resolvedRange,
68
113
  step,
69
114
  bandwidth: Math.abs(step)
70
- });
71
- }
72
-
73
- function validateAlign(value) {
74
- if (!Number.isFinite(value) || value < 0 || value > 1) {
75
- throw new RangeError("Discrete scale align must be between 0 and 1.");
76
- }
77
- return value;
115
+ };
116
+ requireDistinctPositions(resolved, "Ordinal position range");
117
+ return cloneAndFreeze(resolved);
78
118
  }
79
119
 
80
120
  export function resolveDiscretePositionScale({
@@ -90,21 +130,17 @@ export function resolveDiscretePositionScale({
90
130
  align = 0.5,
91
131
  unknown
92
132
  }) {
93
- if (!["band", "point"].includes(type)) {
133
+ const band = type === "band";
134
+ if (!band && type !== "point") {
94
135
  throw new Error(`Unsupported discrete position scale type "${type}".`);
95
136
  }
96
137
  const resolvedDomain = resolveOrdinalDomain(domain, values);
97
- const resolvedRange = range === "auto" && channel === "y"
98
- ? cloneAndFreeze([bounds.y, bounds.y + bounds.height])
99
- : resolveScaleRange(range, channel, bounds);
100
- const domainValues = new Set(resolvedDomain);
101
- for (const value of values) {
102
- if (!domainValues.has(value) && unknown === undefined) {
103
- throw new Error(`Value "${value}" is outside the discrete domain.`);
104
- }
138
+ const resolvedRange = resolvePositionRange(range, channel, bounds, "discrete");
139
+ validateDomainValues(resolvedDomain, values, unknown, "discrete");
140
+ if (!Number.isFinite(align) || align < 0 || align > 1) {
141
+ throw new RangeError("Discrete scale align must be between 0 and 1.");
105
142
  }
106
- validateAlign(align);
107
- if (type === "band") {
143
+ if (band) {
108
144
  if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
109
145
  throw new RangeError(
110
146
  "Band scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
@@ -120,20 +156,26 @@ export function resolveDiscretePositionScale({
120
156
  "Point scale padding must be a non-negative finite number."
121
157
  );
122
158
  }
123
- const span = Math.abs(resolvedRange[1] - resolvedRange[0]);
124
159
  const direction = Math.sign(resolvedRange[1] - resolvedRange[0]) || 1;
125
160
  const count = resolvedDomain.length;
126
- const denominator = type === "band"
161
+ const denominator = band
127
162
  ? Math.max(1, count - paddingInner + paddingOuter * 2)
128
163
  : Math.max(1, count - 1 + padding * 2);
129
- const step = direction * span / denominator;
130
- const bandwidth = type === "band"
164
+ const step = divideRange(resolvedRange, denominator, "Discrete position step");
165
+ const bandwidth = band
131
166
  ? Math.abs(step) * (1 - paddingInner)
132
167
  : 0;
133
- const outerPadding = type === "band" ? paddingOuter : padding;
134
- const start = resolvedRange[0] +
168
+ const outerPadding = band ? paddingOuter : padding;
169
+ const directStart = resolvedRange[0] +
135
170
  direction * Math.abs(step) * outerPadding * 2 * align;
136
- return cloneAndFreeze({
171
+ const start = Number.isFinite(directStart)
172
+ ? directStart
173
+ : interpolateNumber(
174
+ resolvedRange[0],
175
+ resolvedRange[1],
176
+ outerPadding * 2 * align / denominator
177
+ );
178
+ const resolved = {
137
179
  type,
138
180
  domain: resolvedDomain,
139
181
  range: resolvedRange,
@@ -141,8 +183,10 @@ export function resolveDiscretePositionScale({
141
183
  start,
142
184
  bandwidth,
143
185
  align,
144
- ...(type === "band" ? { paddingInner, paddingOuter } : { padding })
145
- });
186
+ ...(band ? { paddingInner, paddingOuter } : { padding })
187
+ };
188
+ requireDistinctPositions(resolved, "Discrete position range");
189
+ return cloneAndFreeze(resolved);
146
190
  }
147
191
 
148
192
  export function resolveOrdinalOffsetScale({
@@ -173,14 +217,16 @@ export function resolveOrdinalOffsetScale({
173
217
  "Offset scale paddingOuter must be a non-negative finite number."
174
218
  );
175
219
  }
176
- const domainValues = new Set(resolvedDomain);
177
- for (const value of values) {
178
- if (!domainValues.has(value)) {
179
- throw new Error(`Value "${value}" is outside the ordinal domain.`);
180
- }
181
- }
182
- const step = (resolvedRange[1] - resolvedRange[0]) /
183
- Math.max(1, resolvedDomain.length - paddingInner + paddingOuter * 2);
220
+ validateDomainValues(resolvedDomain, values, undefined, "ordinal");
221
+ const denominator = Math.max(
222
+ 1,
223
+ resolvedDomain.length - paddingInner + paddingOuter * 2
224
+ );
225
+ const step = divideRange(
226
+ resolvedRange,
227
+ denominator,
228
+ "Offset scale padding must leave a positive bandwidth"
229
+ );
184
230
  const bandwidth = Math.abs(step) * (1 - paddingInner);
185
231
  if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
186
232
  throw new Error("Offset scale padding must leave a positive bandwidth.");
@@ -206,19 +252,29 @@ export function mapOrdinalPositionValues(values, scale) {
206
252
  if (hasUnknown) return scale.unknown;
207
253
  throw new Error(`Value "${value}" is outside the ordinal domain.`);
208
254
  }
255
+ const positional = scale.type === "band" || scale.type === "point";
209
256
  if (
257
+ !positional ||
210
258
  (scale.type === "band" &&
211
259
  scale.paddingInner === 0 &&
212
260
  scale.paddingOuter === 0 &&
213
261
  scale.align === 0.5) ||
214
262
  (scale.type === "point" && scale.padding === 0.5 && scale.align === 0.5)
215
263
  ) {
216
- return scale.range[0] + (index + 0.5) * scale.step;
217
- }
218
- if (["band", "point"].includes(scale.type)) {
219
- const direction = Math.sign(scale.step) || 1;
220
- return scale.start + index * scale.step + direction * scale.bandwidth / 2;
264
+ const direct = scale.range[0] + (index + 0.5) * scale.step;
265
+ return finitePosition(direct, scale, (index + 0.5) / scale.domain.length);
221
266
  }
222
- return scale.range[0] + (index + 0.5) * scale.step;
267
+ const direction = Math.sign(scale.step) || 1;
268
+ const direct = scale.start + index * scale.step +
269
+ direction * scale.bandwidth / 2;
270
+ const band = scale.type === "band";
271
+ const outer = band ? scale.paddingOuter : scale.padding;
272
+ const inner = band ? scale.paddingInner : 1;
273
+ return finitePosition(
274
+ direct,
275
+ scale,
276
+ (outer * 2 * scale.align + index + (1 - inner) / 2) /
277
+ Math.max(1, scale.domain.length - inner + outer * 2)
278
+ );
223
279
  }));
224
280
  }