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
@@ -8,11 +8,16 @@ import {
8
8
  aggregateRows,
9
9
  isAggregate,
10
10
  validateAggregateFieldType,
11
- validateAggregateFieldValues,
11
+ validateAggregateFieldValues
12
12
  } from "./aggregate.js";
13
13
  import { stableOrderPathValues } from "./pathOrder.js";
14
+ import { finiteMidpoint } from "./numeric.js";
15
+ import {
16
+ findHistogramBinIndex,
17
+ resolveHistogramBins
18
+ } from "./histogram.js";
14
19
 
15
- const SERIES_CHANNELS = Object.freeze(["group", "color", "strokeDash"]);
20
+ const SERIES_CHANNELS = ["group", "color", "strokeDash"];
16
21
 
17
22
  function requireLineEncoding(layer) {
18
23
  if (layer?.mark?.type !== "line") {
@@ -26,10 +31,16 @@ function requireLineEncoding(layer) {
26
31
  throw new Error(`Line mark "${layer.id}" requires a temporal x encoding.`);
27
32
  }
28
33
 
34
+ const binnedAggregate =
35
+ x?.fieldType === "quantitative" &&
36
+ x.bin !== undefined &&
37
+ isAggregate(y?.aggregate);
29
38
  const aggregateMode =
30
- x?.fieldType === "temporal" && isAggregate(y?.aggregate);
39
+ (x?.fieldType === "temporal" && isAggregate(y?.aggregate)) ||
40
+ binnedAggregate;
31
41
  const directQuantitative =
32
42
  x?.fieldType === "quantitative" &&
43
+ x.bin === undefined &&
33
44
  y?.fieldType === "quantitative" &&
34
45
  y.aggregate === undefined;
35
46
  const directTemporal =
@@ -53,6 +64,8 @@ function requireLineEncoding(layer) {
53
64
  x,
54
65
  y,
55
66
  isAggregate: aggregateMode,
67
+ binnedAggregate,
68
+ directRows: directTemporal || directQuantitative,
56
69
  directTemporal
57
70
  };
58
71
  }
@@ -87,8 +100,50 @@ function groupKey(values) {
87
100
  return JSON.stringify(values);
88
101
  }
89
102
 
90
- function deriveCartesianLineSeries(rows, layer) {
91
- const { x, y, isAggregate, directTemporal } = requireLineEncoding(layer);
103
+ function groupedSeries(groups, fields, dimensions) {
104
+ const id = groupKey(dimensions);
105
+ let series = groups.get(id);
106
+ if (series === undefined) {
107
+ series = {
108
+ key: Object.fromEntries(
109
+ fields.map((field, index) => [field, dimensions[index]])
110
+ ),
111
+ values: []
112
+ };
113
+ groups.set(id, series);
114
+ }
115
+ return series;
116
+ }
117
+
118
+ function freezeCartesianSeries(series) {
119
+ return cloneAndFreeze({
120
+ xValues: series.flatMap(item => item.values.map(value => value.x)),
121
+ yValues: series.flatMap(item => item.values.map(value => value.y)),
122
+ series
123
+ });
124
+ }
125
+
126
+ export function resolveLineBins(rows, layer, scale = {}) {
127
+ const x = layer?.encoding?.x;
128
+ if (
129
+ layer?.mark?.type !== "line" ||
130
+ x?.fieldType !== "quantitative" ||
131
+ x.bin === undefined
132
+ ) {
133
+ return undefined;
134
+ }
135
+ return resolveHistogramBins({
136
+ values: readQuantitativeField(rows, x.field),
137
+ bin: x.bin,
138
+ domain: scale.domain ?? "auto",
139
+ nice: scale.nice ?? true,
140
+ zero: scale.zero ?? false
141
+ });
142
+ }
143
+
144
+ function deriveCartesianLineSeries(rows, layer, options = {}) {
145
+ const { x, y, isAggregate, binnedAggregate, directRows, directTemporal } =
146
+ requireLineEncoding(layer);
92
147
  if (isAggregate) {
93
148
  validateAggregateFieldValues(rows, y.field, y.fieldType);
94
149
  }
@@ -101,6 +156,9 @@ function deriveCartesianLineSeries(rows, layer) {
101
156
  ? readTemporalField(rows, y.field)
102
157
  : readQuantitativeField(rows, y.field);
103
158
  const seriesFields = readSeriesFields(rows, layer);
159
+ const binBoundaries = binnedAggregate
160
+ ? options.xBinBoundaries ?? resolveLineBins(rows, layer).boundaries
161
+ : undefined;
104
162
 
105
163
  const pathOrder = layer.encoding?.pathOrder;
106
164
  if (pathOrder !== undefined) {
@@ -115,17 +173,10 @@ function deriveCartesianLineSeries(rows, layer) {
115
173
  const dimensions = seriesFields.fields.map(
116
174
  field => seriesFields.values.get(field)[index]
117
175
  );
118
- const key = groupKey(dimensions);
119
- const series = groups.get(key) ?? {
120
- key: Object.fromEntries(
121
- seriesFields.fields.map((field, item) => [field, dimensions[item]])
122
- ),
123
- values: [],
124
- orderValues: []
125
- };
176
+ const series = groupedSeries(groups, seriesFields.fields, dimensions);
126
177
  series.values.push({ x: xValues[index], y: yValues[index] });
178
+ series.orderValues ??= [];
127
179
  series.orderValues.push(orderValues[index]);
128
- groups.set(key, series);
129
180
  }
130
181
  const series = [...groups.values()].flatMap(item => {
131
182
  const values = stableOrderPathValues(
@@ -140,30 +191,19 @@ function deriveCartesianLineSeries(rows, layer) {
140
191
  `Line series on mark "${layer.id}" requires at least two ordered points.`
141
192
  );
142
193
  }
143
- return cloneAndFreeze({
144
- xValues: series.flatMap(item => item.values.map(value => value.x)),
145
- yValues: series.flatMap(item => item.values.map(value => value.y)),
146
- series
147
- });
194
+ return freezeCartesianSeries(series);
148
195
  }
149
196
 
150
- if (directTemporal) {
197
+ if (directRows) {
151
198
  const groups = new Map();
152
199
  for (let index = 0; index < rows.length; index += 1) {
153
200
  const dimensions = seriesFields.fields.map(
154
201
  field => seriesFields.values.get(field)[index]
155
202
  );
156
- const key = groupKey(dimensions);
157
- const series = groups.get(key) ?? {
158
- key: Object.fromEntries(
159
- seriesFields.fields.map((field, item) => [field, dimensions[item]])
160
- ),
161
- values: []
162
- };
203
+ const series = groupedSeries(groups, seriesFields.fields, dimensions);
163
204
  series.values.push({ x: xValues[index], y: yValues[index] });
164
- groups.set(key, series);
165
205
  }
166
- const orderBy = x.fieldType === "temporal" ? "x" : "y";
206
+ const orderBy = directTemporal && y.fieldType === "temporal" ? "y" : "x";
167
207
  const series = [...groups.values()].flatMap(item => {
168
208
  const values = item.values.sort(
169
209
  (left, right) => left[orderBy] - right[orderBy]
@@ -175,11 +215,7 @@ function deriveCartesianLineSeries(rows, layer) {
175
215
  `Line series on mark "${layer.id}" requires at least two direct points.`
176
216
  );
177
217
  }
178
- return cloneAndFreeze({
179
- xValues: series.flatMap(item => item.values.map(value => value.x)),
180
- yValues: series.flatMap(item => item.values.map(value => value.y)),
181
- series
182
- });
218
+ return freezeCartesianSeries(series);
183
219
  }
184
220
  const aggregateGroups = new Map();
185
221
 
@@ -187,9 +223,19 @@ function deriveCartesianLineSeries(rows, layer) {
187
223
  const dimensions = seriesFields.fields.map(
188
224
  field => seriesFields.values.get(field)[index]
189
225
  );
190
- const key = groupKey([xValues[index], ...dimensions]);
226
+ const binIndex = binnedAggregate
227
+ ? findHistogramBinIndex(xValues[index], binBoundaries)
228
+ : undefined;
229
+ if (binIndex === -1) continue;
230
+ const position = binnedAggregate
231
+ ? finiteMidpoint(
232
+ binBoundaries[binIndex],
233
+ binBoundaries[binIndex + 1]
234
+ )
235
+ : xValues[index];
236
+ const key = groupKey([binnedAggregate ? binIndex : position, ...dimensions]);
191
237
  const group = aggregateGroups.get(key) ?? {
192
- x: xValues[index],
238
+ x: position,
193
239
  dimensions,
194
240
  rows: []
195
241
  };
@@ -205,23 +251,18 @@ function deriveCartesianLineSeries(rows, layer) {
205
251
  const seriesGroups = new Map();
206
252
 
207
253
  for (const group of aggregateGroups.values()) {
208
- const value = isAggregate
209
- ? aggregateRows(group.rows, y.field, y.aggregate)
210
- : group.rows.reduce((sum, row) => sum + row[y.field], 0);
254
+ const value = aggregateRows(group.rows, y.field, y.aggregate);
211
255
  if (value === undefined) continue;
212
- const key = groupKey(group.dimensions);
213
- const series = seriesGroups.get(key) ?? {
214
- key: Object.fromEntries(
215
- seriesFields.fields.map((field, index) => [field, group.dimensions[index]])
216
- ),
217
- values: []
218
- };
256
+ const series = groupedSeries(
257
+ seriesGroups,
258
+ seriesFields.fields,
259
+ group.dimensions
260
+ );
219
261
 
220
262
  series.values.push({
221
263
  x: group.x,
222
264
  y: value
223
265
  });
224
- seriesGroups.set(key, series);
225
266
  }
226
267
 
227
268
  const series = [...seriesGroups.values()].flatMap(item => {
@@ -240,11 +281,7 @@ function deriveCartesianLineSeries(rows, layer) {
240
281
  );
241
282
  }
242
283
 
243
- return cloneAndFreeze({
244
- xValues: series.flatMap(item => item.values.map(value => value.x)),
245
- yValues: series.flatMap(item => item.values.map(value => value.y)),
246
- series
247
- });
284
+ return freezeCartesianSeries(series);
248
285
  }
249
286
 
250
287
  function requirePolarLineEncoding(layer) {
@@ -311,20 +348,13 @@ export function derivePolarLineSeries(rows, layer, { thetaDomain } = {}) {
311
348
  const dimensions = seriesFields.fields.map(
312
349
  field => seriesFields.values.get(field)[index]
313
350
  );
314
- const key = groupKey(dimensions);
315
- const series = groups.get(key) ?? {
316
- key: Object.fromEntries(
317
- seriesFields.fields.map((field, item) => [field, dimensions[item]])
318
- ),
319
- values: []
320
- };
351
+ const series = groupedSeries(groups, seriesFields.fields, dimensions);
321
352
  series.values.push({
322
353
  theta: thetaValues[index],
323
354
  radius: radiusValues[index],
324
355
  order: sortValues[index],
325
356
  sourceIndex: index
326
357
  });
327
- groups.set(key, series);
328
358
  }
329
359
 
330
360
  const series = [...groups.values()].flatMap(item => {
@@ -352,7 +382,7 @@ export function deriveLineSeries(rows, layer, options) {
352
382
  layer?.encoding?.radius !== undefined;
353
383
  return polar
354
384
  ? derivePolarLineSeries(rows, layer, options)
355
- : deriveCartesianLineSeries(rows, layer);
385
+ : deriveCartesianLineSeries(rows, layer, options);
356
386
  }
357
387
 
358
388
  export function deriveLineSeriesFieldValues(rows, layer, derived, field) {
@@ -0,0 +1,290 @@
1
+ export const NICE_FACTORS = Object.freeze([1, 2, 3, 5]);
2
+
3
+ function finiteArray(values, label) {
4
+ if (!Array.isArray(values) || !values.every(Number.isFinite)) {
5
+ throw new TypeError(`${label} must contain finite numbers.`);
6
+ }
7
+ }
8
+
9
+ function rangeError(label) {
10
+ return new RangeError(`${label} is outside the finite numeric range.`);
11
+ }
12
+
13
+ function compensatedSum(values, scale = 1) {
14
+ let total = 0;
15
+ let correction = 0;
16
+ for (const source of values) {
17
+ const value = source / scale;
18
+ const next = total + value;
19
+ correction += Math.abs(total) >= Math.abs(value)
20
+ ? total - next + value
21
+ : value - next + total;
22
+ total = next;
23
+ }
24
+ return total + correction;
25
+ }
26
+
27
+ export function maximumMagnitude(values) {
28
+ let maximum = 0;
29
+ for (const value of values) maximum = Math.max(maximum, Math.abs(value));
30
+ return maximum;
31
+ }
32
+
33
+ export function normalizedFiniteSum(values, label = "Numeric values") {
34
+ finiteArray(values, label);
35
+ const scale = maximumMagnitude(values) || 1;
36
+ return { scale, total: compensatedSum(values, scale) };
37
+ }
38
+
39
+ export function requireFiniteResult(value, label = "Numeric result") {
40
+ if (!Number.isFinite(value)) throw rangeError(label);
41
+ return value === 0 ? 0 : value;
42
+ }
43
+
44
+ export function restoreFiniteScale(value, scale, label = "Numeric result") {
45
+ if (!Number.isFinite(value) || !Number.isFinite(scale) || scale < 0) {
46
+ throw rangeError(label);
47
+ }
48
+ return requireFiniteResult(value * scale, label);
49
+ }
50
+
51
+ function stableReducedSum(values, label, divisor = 1) {
52
+ finiteArray(values, label);
53
+ const ordinary = values.reduce((sum, value) => sum + value, 0);
54
+ if (Number.isFinite(ordinary)) {
55
+ return ordinary === 0 ? 0 : ordinary / divisor;
56
+ }
57
+ const scale = maximumMagnitude(values) || 1;
58
+ return restoreFiniteScale(
59
+ compensatedSum(values, scale) / divisor, scale, label
60
+ );
61
+ }
62
+
63
+ export function stableFiniteSum(values, label = "Numeric sum") {
64
+ return stableReducedSum(values, label);
65
+ }
66
+
67
+ export function stableFinitePrefixSums(values, label = "Numeric sum") {
68
+ const normalized = normalizedFiniteSum(values, label);
69
+ let ordinary = 0;
70
+ let sum = 0;
71
+ let correction = 0;
72
+ return values.map(value => {
73
+ ordinary += value;
74
+ const scaled = value / normalized.scale;
75
+ const next = sum + scaled;
76
+ correction += Math.abs(sum) >= Math.abs(scaled)
77
+ ? sum - next + scaled
78
+ : scaled - next + sum;
79
+ sum = next;
80
+ return Number.isFinite(ordinary)
81
+ ? ordinary === 0 ? 0 : ordinary
82
+ : restoreFiniteScale(sum + correction, normalized.scale, label);
83
+ });
84
+ }
85
+
86
+ export function stableFiniteMean(values, label = "Numeric mean") {
87
+ finiteArray(values, label);
88
+ if (values.length === 0) {
89
+ throw new RangeError(`${label} requires at least one value.`);
90
+ }
91
+ return stableReducedSum(values, label, values.length);
92
+ }
93
+
94
+ export function stableFiniteSquareSum(values, label = "Numeric square sum") {
95
+ finiteArray(values, label);
96
+ const ordinary = values.reduce((sum, value) => sum + value ** 2, 0);
97
+ if (Number.isFinite(ordinary)) return ordinary;
98
+ const scale = maximumMagnitude(values);
99
+ if (scale === 0) return 0;
100
+ const normalized = values.reduce(
101
+ (sum, value) => sum + (value / scale) ** 2,
102
+ 0
103
+ );
104
+ const magnitude = restoreFiniteScale(Math.sqrt(normalized), scale, label);
105
+ return requireFiniteResult(magnitude ** 2, label);
106
+ }
107
+
108
+ export function stableFiniteDeviation(values, {
109
+ sample = false,
110
+ divisor = 1,
111
+ label = "Numeric deviation"
112
+ } = {}) {
113
+ if (!Number.isFinite(divisor) || divisor <= 0) {
114
+ throw new RangeError(`${label} divisor must be positive and finite.`);
115
+ }
116
+ const denominator = values.length - (sample ? 1 : 0);
117
+ if (denominator <= 0) {
118
+ throw new RangeError(`${label} requires more values.`);
119
+ }
120
+ const mean = stableFiniteMean(values, `${label} mean`);
121
+ const differences = values.map(value => value - mean);
122
+ const squared = differences.reduce(
123
+ (sum, difference) => sum + difference ** 2,
124
+ 0
125
+ );
126
+ if (Number.isFinite(squared)) {
127
+ return {
128
+ mean,
129
+ deviation: Math.sqrt(squared / denominator) / divisor,
130
+ squared
131
+ };
132
+ }
133
+ const finiteDifferences = differences.every(Number.isFinite);
134
+ const scale = finiteDifferences
135
+ ? maximumMagnitude(differences)
136
+ : Math.max(Math.abs(mean), maximumMagnitude(values));
137
+ if (scale === 0) return { mean, deviation: 0 };
138
+ const normalizedSquares = values.reduce((sum, value, index) => {
139
+ const difference = finiteDifferences
140
+ ? differences[index] / scale
141
+ : value / scale - mean / scale;
142
+ return sum + difference ** 2;
143
+ }, 0);
144
+ const deviation = restoreFiniteScale(
145
+ Math.sqrt(normalizedSquares / denominator) / divisor,
146
+ scale,
147
+ label
148
+ );
149
+ return { mean, deviation };
150
+ }
151
+
152
+ export function numericExtent(values) {
153
+ let minimum = Infinity;
154
+ let maximum = -Infinity;
155
+ for (const value of values) {
156
+ minimum = Math.min(minimum, value);
157
+ maximum = Math.max(maximum, value);
158
+ }
159
+ return [minimum, maximum];
160
+ }
161
+
162
+ export function normalizeNumericRange(start, end) {
163
+ const magnitude = Math.max(Math.abs(start), Math.abs(end));
164
+ if (magnitude === 0) {
165
+ return { scale: 1, start: 0, end: 0, span: 0 };
166
+ }
167
+
168
+ const decimalScale = 10 ** Math.floor(Math.log10(magnitude));
169
+ const scale = Number.isFinite(decimalScale) && decimalScale > 0
170
+ ? decimalScale
171
+ : magnitude;
172
+ start /= scale;
173
+ end /= scale;
174
+ return {
175
+ scale,
176
+ start,
177
+ end,
178
+ span: end - start
179
+ };
180
+ }
181
+
182
+ export function niceNumericStep(
183
+ span,
184
+ count = 5,
185
+ factors = NICE_FACTORS
186
+ ) {
187
+ if (!Number.isFinite(span) || span <= 0) {
188
+ throw new RangeError("Nice numeric span must be positive and finite.");
189
+ }
190
+ const rough = span / Math.max(1, count);
191
+ const power = 10 ** Math.floor(Math.log10(rough));
192
+ const fraction = rough / power;
193
+ const factor = factors.find(candidate => candidate >= fraction) ?? 10;
194
+ return factor * power;
195
+ }
196
+
197
+ export function alignNumericStep(value, step, direction) {
198
+ let quotient = value / step;
199
+ const nearest = Math.round(quotient);
200
+ if (Math.abs(quotient - nearest) <= 1e-10) quotient = nearest;
201
+ return (direction === "floor" ? Math.floor(quotient) : Math.ceil(quotient)) * step;
202
+ }
203
+
204
+ export function cleanNumericValue(value, resolution) {
205
+ if (!Number.isFinite(value) || value === 0) return value === 0 ? 0 : value;
206
+ const rounded = Number(value.toPrecision(15));
207
+ return Number.isFinite(resolution) && resolution > 0 &&
208
+ Math.abs(rounded - value) <= resolution * 1e-10
209
+ ? rounded
210
+ : value;
211
+ }
212
+
213
+ export function inverseLerp(value, start, end) {
214
+ if (value === start) return 0;
215
+ if (value === end) return 1;
216
+
217
+ const span = end - start;
218
+ const offset = value - start;
219
+ if (Number.isFinite(span) && span !== 0 && Number.isFinite(offset)) {
220
+ return offset / span;
221
+ }
222
+
223
+ const scale = Math.max(Math.abs(value), Math.abs(start), Math.abs(end));
224
+ return (value / scale - start / scale) / (end / scale - start / scale);
225
+ }
226
+
227
+ export function interpolateNumber(start, end, proportion) {
228
+ let result = proportion === 0 ? start : proportion === 1 ? end : undefined;
229
+ if (result === undefined) {
230
+ const span = end - start;
231
+ if (Number.isFinite(span)) {
232
+ result = start + proportion * span;
233
+ } else {
234
+ const scale = Math.max(Math.abs(start), Math.abs(end));
235
+ result = scale * (
236
+ start / scale * (1 - proportion) + end / scale * proportion
237
+ );
238
+ }
239
+ }
240
+ return requireFiniteResult(result, "Numeric interpolation");
241
+ }
242
+
243
+ export function finiteMidpoint(start, end) {
244
+ const sum = start + end;
245
+ const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
246
+ return requireFiniteResult(result, "Numeric midpoint");
247
+ }
248
+
249
+ export function sampleNumericRange(start, end, count, label = "Numeric range") {
250
+ const resolution = Math.abs((end - start) / (count - 1));
251
+ const values = [];
252
+ const direction = Math.sign(end - start);
253
+ for (let index = 0; index < count; index += 1) {
254
+ const value = cleanNumericValue(
255
+ interpolateNumber(start, end, index / (count - 1)),
256
+ resolution
257
+ );
258
+ if (
259
+ !Number.isFinite(value) ||
260
+ (direction !== 0 && index > 0 &&
261
+ direction * (value - values[index - 1]) <= 0)
262
+ ) {
263
+ throw new RangeError(`${label} cannot represent ${count} distinct samples.`);
264
+ }
265
+ values.push(value);
266
+ }
267
+ return values;
268
+ }
269
+
270
+ export function formatDistinctNumericSamples(values, initialFormat) {
271
+ let labels = values.map(initialFormat ?? (value =>
272
+ String(+value.toPrecision(3))
273
+ ));
274
+ const distinct = () => labels.every((label, index) =>
275
+ index === 0 || values[index] === values[index - 1] ||
276
+ label !== labels[index - 1]
277
+ );
278
+ for (let precision = 4; !distinct() && precision <= 17; precision += 1) {
279
+ labels = values.map(value => String(+value.toPrecision(precision)));
280
+ }
281
+ return distinct() ? labels : values.map(String);
282
+ }
283
+
284
+ export function stableDecimal(value, places = 12) {
285
+ if (value === 0) return 0;
286
+ const threshold = 10 ** -places;
287
+ return Math.abs(value) < threshold
288
+ ? Number(value.toPrecision(places))
289
+ : Number(value.toFixed(places));
290
+ }
@@ -1,4 +1,5 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
+ import { interpolateNumber } from "./numeric.js";
2
3
 
3
4
  const PAINT_KEYS = Object.freeze(["type", "from", "to", "stops"]);
4
5
  const POINT_KEYS = Object.freeze(["x", "y"]);
@@ -78,11 +79,9 @@ export function resolveLinearGradientCoordinates(paint, bounds) {
78
79
  ) {
79
80
  throw new TypeError("Linear gradient bounds must be finite and ordered.");
80
81
  }
81
- const width = bounds.right - bounds.left;
82
- const height = bounds.bottom - bounds.top;
83
82
  const point = value => Object.freeze({
84
- x: bounds.left + width * value.x,
85
- y: bounds.top + height * value.y
83
+ x: interpolateNumber(bounds.left, bounds.right, value.x),
84
+ y: interpolateNumber(bounds.top, bounds.bottom, value.y)
86
85
  });
87
86
  return Object.freeze({ from: point(paint.from), to: point(paint.to) });
88
87
  }
@@ -1,9 +1,12 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { validateKeys } from "../core/validation.js";
2
+ import {
3
+ validateGeneratedItemLimit,
4
+ validateKeys
5
+ } from "../core/validation.js";
3
6
 
4
7
  function colors(encoded) {
5
8
  return cloneAndFreeze(
6
- encoded.match(/.{6}/g).map(value => `#${value.toLowerCase()}`)
9
+ encoded.match(/.{6}/g).map(value => `#${value}`)
7
10
  );
8
11
  }
9
12
 
@@ -82,38 +85,21 @@ const CONTINUOUS = Object.freeze({
82
85
  });
83
86
 
84
87
  export const PALETTE_NAMES = cloneAndFreeze([
85
- "accent",
86
- "category10", "category20", "category20b", "category20c",
87
- "observable10",
88
- "dark2", "paired", "pastel1", "pastel2",
89
- "set1", "set2", "set3",
90
- "tableau10", "tableau20",
91
- "blues", "tealblues", "teals", "greens", "browns",
92
- "oranges", "reds", "purples", "warmgreys", "greys",
93
- "viridis", "magma", "inferno", "plasma", "cividis", "turbo",
94
- "bluegreen", "bluepurple",
95
- "goldgreen", "goldorange", "goldred",
96
- "greenblue", "orangered",
97
- "purplebluegreen", "purpleblue", "purplered", "redpurple",
98
- "yellowgreenblue", "yellowgreen", "yelloworangebrown", "yelloworangered",
99
- "darkblue", "darkgold", "darkgreen", "darkmulti", "darkred",
100
- "lightgreyred", "lightgreyteal", "lightmulti", "lightorange", "lighttealblue",
101
- "blueorange", "brownbluegreen", "purplegreen", "pinkyellowgreen",
102
- "purpleorange", "redblue", "redgrey",
103
- "redyellowblue", "redyellowgreen", "spectral",
104
- "rainbow", "sinebow"
88
+ ...Object.keys(DISCRETE),
89
+ ...Object.keys(CONTINUOUS)
105
90
  ]);
106
91
 
107
- const PALETTE_SET = new Set(PALETTE_NAMES);
108
-
109
92
  export function validatePaletteName(name) {
110
- if (typeof name !== "string" || !PALETTE_SET.has(name)) {
93
+ if (
94
+ typeof name !== "string" ||
95
+ !Object.hasOwn(DISCRETE, name) && !Object.hasOwn(CONTINUOUS, name)
96
+ ) {
111
97
  throw new Error(`Unknown palette "${name}".`);
112
98
  }
113
99
  return name;
114
100
  }
115
101
 
116
- export function normalizePalette(value) {
102
+ export function normalizePalette(value, maximum) {
117
103
  if (typeof value === "string") return { name: validatePaletteName(value) };
118
104
  if (!isPlainObject(value)) {
119
105
  throw new TypeError("Palette must be a name or a plain object.");
@@ -125,6 +111,7 @@ export function normalizePalette(value) {
125
111
  if (!Number.isInteger(value.count) || value.count <= 0) {
126
112
  throw new RangeError("Palette count must be a positive integer.");
127
113
  }
114
+ validateGeneratedItemLimit(value.count, "Palette count", maximum);
128
115
  normalized.count = value.count;
129
116
  }
130
117
  if (value.extent !== undefined) {
@@ -161,15 +148,14 @@ function interpolateColor(left, right, amount) {
161
148
  }
162
149
 
163
150
  function sample(colorsValue, position) {
164
- const bounded = Math.max(0, Math.min(1, position));
165
- const scaled = bounded * (colorsValue.length - 1);
151
+ const scaled = position * (colorsValue.length - 1);
166
152
  const left = Math.floor(scaled);
167
153
  const right = Math.min(colorsValue.length - 1, left + 1);
168
154
  return interpolateColor(colorsValue[left], colorsValue[right], scaled - left);
169
155
  }
170
156
 
171
- export function resolvePalette(value, domainCount) {
172
- const palette = normalizePalette(value);
157
+ export function resolvePalette(value, domainCount, maximum) {
158
+ const palette = normalizePalette(value, maximum);
173
159
  const discrete = DISCRETE[palette.name];
174
160
  if (discrete !== undefined) {
175
161
  const count = palette.count ?? discrete.length;
@@ -182,6 +168,7 @@ export function resolvePalette(value, domainCount) {
182
168
  }
183
169
  const source = CONTINUOUS[palette.name];
184
170
  const count = palette.count ?? domainCount;
171
+ validateGeneratedItemLimit(count, "Palette count", maximum);
185
172
  const extent = palette.extent ?? [0, 1];
186
173
  return cloneAndFreeze(Array.from({ length: count }, (_, index) => {
187
174
  const unit = count === 1 ? 0.5 : index / (count - 1);
@@ -195,12 +182,14 @@ export function resolveContinuousPalette(value, count = 31) {
195
182
  if (!Number.isInteger(resolvedCount) || resolvedCount < 2) {
196
183
  throw new RangeError("Continuous palette count must be an integer of at least 2.");
197
184
  }
185
+ validateGeneratedItemLimit(resolvedCount, "Continuous palette count");
198
186
  const discrete = DISCRETE[palette.name];
199
187
  if (discrete !== undefined && palette.count === undefined) return discrete;
200
188
  return resolvePalette({ ...palette, count: resolvedCount }, resolvedCount);
201
189
  }
202
190
 
203
191
  export function paletteFamily(name) {
204
- const validated = validatePaletteName(name);
205
- return DISCRETE[validated] === undefined ? "continuous" : "categorical";
192
+ return DISCRETE[validatePaletteName(name)] === undefined
193
+ ? "continuous"
194
+ : "categorical";
206
195
  }