ggaction 0.0.18 → 0.0.20

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 (164) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +1 -1
  7. package/src/actions/canvas/fitting.js +6 -2
  8. package/src/actions/charts/heatmap.js +4 -3
  9. package/src/actions/data/derived.js +11 -1
  10. package/src/actions/data/edit.js +2 -0
  11. package/src/actions/data/index.js +4 -0
  12. package/src/actions/data/regression.js +24 -16
  13. package/src/actions/data/regressionStatistics.js +73 -0
  14. package/src/actions/data/shared.js +5 -2
  15. package/src/actions/distributions/revision.js +24 -18
  16. package/src/actions/encodings/color/continuous.js +7 -1
  17. package/src/actions/encodings/remove.js +11 -6
  18. package/src/actions/encodings/shared.js +26 -24
  19. package/src/actions/facets/actions.js +18 -7
  20. package/src/actions/facets/guides.js +2 -1
  21. package/src/actions/guides/axes/axes.js +30 -11
  22. package/src/actions/guides/axes/axis.js +58 -8
  23. package/src/actions/guides/axes/components.js +13 -13
  24. package/src/actions/guides/axes/edit.js +36 -24
  25. package/src/actions/guides/axes/labels.js +37 -27
  26. package/src/actions/guides/axes/lines.js +22 -20
  27. package/src/actions/guides/axes/remove.js +10 -6
  28. package/src/actions/guides/axes/tickGroups.js +8 -7
  29. package/src/actions/guides/axes/ticks.js +31 -22
  30. package/src/actions/guides/axes/titles.js +53 -60
  31. package/src/actions/guides/facadeAxes.js +33 -24
  32. package/src/actions/guides/facadeLegend.js +3 -3
  33. package/src/actions/guides/grids/grid.js +1 -1
  34. package/src/actions/guides/grids/resolve.js +48 -51
  35. package/src/actions/guides/legends/blocks.js +43 -19
  36. package/src/actions/guides/legends/categorical/actions.js +32 -9
  37. package/src/actions/guides/legends/categorical/components.js +3 -10
  38. package/src/actions/guides/legends/categorical/index.js +2 -0
  39. package/src/actions/guides/legends/categorical/layout.js +9 -13
  40. package/src/actions/guides/legends/categorical/options.js +8 -2
  41. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  42. package/src/actions/guides/legends/categorical/resolve.js +9 -2
  43. package/src/actions/guides/legends/categorical/symbols.js +22 -8
  44. package/src/actions/guides/legends/colorSize.js +110 -0
  45. package/src/actions/guides/legends/combine.js +37 -0
  46. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
  47. package/src/actions/guides/legends/continuous/common.js +1 -1
  48. package/src/actions/guides/legends/continuous/gradient.js +27 -13
  49. package/src/actions/guides/legends/continuous/opacity.js +16 -11
  50. package/src/actions/guides/legends/creation.js +2 -0
  51. package/src/actions/guides/legends/edit.js +1 -1
  52. package/src/actions/guides/legends/focused.js +6 -5
  53. package/src/actions/guides/legends/lane.js +1 -1
  54. package/src/actions/guides/legends/lifecycle.js +2 -1
  55. package/src/actions/guides/legends/remove.js +6 -3
  56. package/src/actions/guides/legends/size.js +16 -6
  57. package/src/actions/guides/legends/strokeWidth.js +2 -0
  58. package/src/actions/guides/legends/target.js +2 -79
  59. package/src/actions/guides/legends/transition.js +24 -5
  60. package/src/actions/guides/tickValues.js +8 -5
  61. package/src/actions/marks/area/materialize.js +1 -190
  62. package/src/actions/marks/bar/edit.js +23 -12
  63. package/src/actions/marks/bar/materialize.js +6 -1
  64. package/src/actions/marks/point/materialize.js +5 -1
  65. package/src/actions/marks/remove.js +10 -10
  66. package/src/actions/primitives/createGraphics.js +3 -2
  67. package/src/actions/primitives/editGraphics.js +15 -11
  68. package/src/actions/primitives/graphicProperties.js +1 -6
  69. package/src/actions/primitives/semanticAction.js +27 -1
  70. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  71. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  72. package/src/actions/regression/edit.js +15 -3
  73. package/src/actions/scales/channels.js +2 -2
  74. package/src/actions/scales/consumers/common.js +7 -1
  75. package/src/actions/scales/create.js +3 -3
  76. package/src/actions/scales/definitions.js +1 -1
  77. package/src/actions/scales/edit.js +1 -1
  78. package/src/actions/scales/editPolicy.js +3 -0
  79. package/src/actions/scales/patch.js +1 -1
  80. package/src/actions/scales/quantitativeColor.js +8 -3
  81. package/src/actions/theme/reconcile.js +50 -23
  82. package/src/core/compositionState.js +2 -2
  83. package/src/core/graphicProperties.js +1 -0
  84. package/src/core/programState.js +4 -0
  85. package/src/core/resourceReferences.js +2 -2
  86. package/src/core/validation.js +9 -0
  87. package/src/core/vocabulary.js +3 -0
  88. package/src/grammar/aggregate.js +3 -2
  89. package/src/grammar/areaSeries.js +1 -415
  90. package/src/grammar/axisTextPlacement.js +1 -0
  91. package/src/grammar/colorSizeLegend.js +39 -0
  92. package/src/grammar/computed.js +23 -1
  93. package/src/grammar/datasetSchema.js +10 -2
  94. package/src/grammar/displayLabels.js +8 -0
  95. package/src/grammar/facets/guides.js +54 -31
  96. package/src/grammar/facets/legendIntent.js +65 -0
  97. package/src/grammar/guideDash.js +1 -0
  98. package/src/grammar/interval.js +22 -6
  99. package/src/grammar/legendBlocks.js +1 -0
  100. package/src/grammar/legendBorder.js +9 -0
  101. package/src/grammar/legendClip.js +19 -0
  102. package/src/grammar/legendCombinations.js +45 -0
  103. package/src/grammar/legendGradient.js +9 -0
  104. package/src/grammar/legendGroups.js +1 -1
  105. package/src/grammar/legendOpacity.js +6 -0
  106. package/src/grammar/pathSeries.js +2 -2
  107. package/src/grammar/pointShapes.js +16 -2
  108. package/src/grammar/regression/derive.js +9 -1
  109. package/src/grammar/regression/models.js +60 -0
  110. package/src/grammar/regression/parameters.js +9 -1
  111. package/src/grammar/regression/statistics.js +64 -0
  112. package/src/grammar/scales/color.js +1 -1
  113. package/src/grammar/scales/colorConsumers.js +6 -3
  114. package/src/grammar/scales/definition.js +1 -1
  115. package/src/grammar/scales/types.js +1 -1
  116. package/src/grammar/schemas/concreteGraphic.js +3 -0
  117. package/src/grammar/schemas/graphic.js +3 -0
  118. package/src/grammar/schemas/graphicBounds.js +31 -7
  119. package/src/grammar/schemas/semanticPath.js +17 -2
  120. package/src/grammar/statistics/confidenceInterval.js +1 -101
  121. package/src/grammar/summary.js +1 -320
  122. package/src/grammar/timeUnit.js +6 -4
  123. package/src/grammar/transformTopology.js +1 -0
  124. package/src/grammar/transforms.js +11 -1
  125. package/src/inspectionDescriptors.js +1 -1
  126. package/src/layout/guideCollisions.js +9 -1
  127. package/src/layout/legendGroup.js +1 -1
  128. package/src/materialization/compositionSnapshot.js +2 -7
  129. package/src/materialization/coordinateBounds.js +17 -4
  130. package/src/materialization/facetGuides/edit.js +30 -0
  131. package/src/materialization/facetGuides/layout.js +1 -1
  132. package/src/materialization/facetGuides/owner.js +125 -0
  133. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  134. package/src/materialization/facetGuides/placement.js +52 -6
  135. package/src/materialization/facetGuides/preparation.js +7 -0
  136. package/src/materialization/facets.js +1 -1
  137. package/src/materialization/graphicHierarchy.js +5 -1
  138. package/src/materialization/graphicTranslation.js +73 -0
  139. package/src/materialization/guides/layout.js +1 -1
  140. package/src/materialization/guides/resources.js +1 -1
  141. package/src/materialization/legendBorder.js +28 -0
  142. package/src/materialization/legendClip.js +76 -0
  143. package/src/materialization/legendGroups.js +1 -1
  144. package/src/materialization/legends.js +1 -1
  145. package/src/materialization/marks/grouping.js +1 -1
  146. package/src/materialization/scaleGuideDependencies.js +13 -14
  147. package/src/materialization/scales/map.js +3 -0
  148. package/src/materialization/scales/resolve.js +2 -0
  149. package/src/materialization/typography.js +10 -2
  150. package/src/persistence/validation.js +174 -1
  151. package/src/persistence.js +1 -1
  152. package/src/renderers/canvas/index.js +20 -10
  153. package/src/renderers/canvas/isolated.js +166 -0
  154. package/src/renderers/canvas/limits.js +3 -0
  155. package/src/renderers/canvas/rect.js +1 -1
  156. package/src/renderers/pdf.js +2 -2
  157. package/src/renderers/png.js +2 -2
  158. package/src/renderers/svg.js +10 -3
  159. package/src/selectors/axes.js +1 -0
  160. package/src/selectors/graphicCapacity.js +38 -0
  161. package/src/version.js +1 -1
  162. package/types/basic.d.ts +2 -0
  163. package/types/index.d.ts +7 -0
  164. package/types/program.d.ts +220 -64
@@ -1,415 +1 @@
1
- import { cloneAndFreeze } from "../core/immutable.js";
2
- import { readNominalField, readQuantitativeField, readTemporalField } from "./scales/index.js";
3
- import { layoutSeriesPartition, validateColorLayout } from "./seriesLayout.js";
4
- import { stableOrderPathValues } from "./pathOrder.js";
5
- import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
6
- import { validatePathSeriesAppearance } from "./pathSeries.js";
7
- import { readAreaEndpoint, validateAreaEndpointPair } from "./areaEndpoints.js";
8
- import { validateGeneratedItemLimit } from "../core/validation.js";
9
-
10
- function indexSegments(segments) {
11
- const indexed = [];
12
- for (const segment of segments) indexed[segment.index] = segment;
13
- return indexed;
14
- }
15
-
16
- export function deriveAreaSeries(rows, layer, options = {}) {
17
- if (layer?.mark?.type !== "area") {
18
- throw new Error("Area series derivation requires a semantic area mark.");
19
- }
20
- const { x, y, x2, y2 } = layer.encoding ?? {};
21
- const vertical =
22
- ["quantitative", "temporal", "nominal", "ordinal"].includes(x?.fieldType) &&
23
- y?.fieldType === "quantitative" &&
24
- y2?.fieldType === "quantitative" &&
25
- x2 === undefined;
26
- const horizontal =
27
- ["quantitative", "temporal", "nominal", "ordinal"].includes(y?.fieldType) &&
28
- x?.fieldType === "quantitative" &&
29
- x2?.fieldType === "quantitative" &&
30
- y2 === undefined;
31
- if (vertical === horizontal) {
32
- throw new Error(
33
- `Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative, temporal, or categorical independent position.`
34
- );
35
- }
36
- const grouping = validatePathSeriesAppearance(rows, layer);
37
- const orientation = vertical ? "vertical" : "horizontal";
38
- const independent = vertical ? x : y;
39
- const categorical = ["nominal", "ordinal"].includes(independent.fieldType);
40
- const independentValues = categorical ? readNominalField(rows, independent.field) : independent.fieldType === "temporal"
41
- ? readTemporalField(rows, independent.field, independent.temporalUnit)
42
- : readQuantitativeField(rows, independent.field);
43
- if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
44
- const primary = vertical ? y : x;
45
- const secondary = vertical ? y2 : x2;
46
- validateAreaEndpointPair(primary, secondary);
47
- const missing = layer.mark.missing ?? "error";
48
- const lower = readAreaEndpoint(rows, primary, missing);
49
- const upper = readAreaEndpoint(rows, secondary, missing);
50
- const categoryOrder = categorical ? options[vertical ? "xDomain" : "yDomain"] ?? [...new Set(independentValues)] : undefined;
51
- const ranks = new Map(categoryOrder?.map((value, index) => [value, index]));
52
- if (categorical && independentValues.some(value => !ranks.has(value))) throw new Error("Area categorical domain must contain every observed position.");
53
- const pathOrder = layer.encoding?.pathOrder;
54
- const orderValues = pathOrder === undefined
55
- ? undefined
56
- : readQuantitativeField(rows, pathOrder.field);
57
- const groups = new Map();
58
-
59
- for (let index = 0; index < rows.length; index += 1) {
60
- const tuple = grouping.map(field => rows[index][field]);
61
- const key = JSON.stringify(tuple);
62
- const series = groups.get(key) ?? {
63
- key: Object.fromEntries(grouping.map((field, i) => [field, tuple[i]])),
64
- values: []
65
- };
66
- series.values.push(vertical
67
- ? {
68
- x: independentValues[index],
69
- y: lower[index],
70
- y2: upper[index],
71
- ...(missing === "break" ? { sourceIndex: index } : {}),
72
- ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
73
- }
74
- : {
75
- x: lower[index],
76
- x2: upper[index],
77
- ...(missing === "break" ? { sourceIndex: index } : {}),
78
- y: independentValues[index],
79
- ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
80
- });
81
- groups.set(key, series);
82
- }
83
- if (groups.size === 0) {
84
- throw new Error(`Area mark "${layer.id}" has no values.`);
85
- }
86
- layoutRawAreaGroups(groups, { layer, vertical, primary, secondary });
87
- const series = [...groups.values()].flatMap(item => {
88
- const key = vertical ? "x" : "y";
89
- const values = pathOrder === undefined
90
- ? item.values.sort((left, right) => categorical ? ranks.get(left[key]) - ranks.get(right[key]) : left[key] - right[key])
91
- : stableOrderPathValues(
92
- item.values.map(({ pathOrder: _pathOrder, ...value }) => value),
93
- item.values.map(value => value.pathOrder),
94
- pathOrder.order
95
- );
96
- if (missing === "break") {
97
- const segments = [];
98
- let segment = [];
99
- const emit = () => {
100
- if (segment.length >= 2) segments.push({ key: item.key,
101
- sourceIndices: segment.map(value => value.sourceIndex),
102
- values: segment.map(({ sourceIndex, ...value }) => value) });
103
- segment = [];
104
- };
105
- for (const value of values) {
106
- if ((vertical ? value.y == null || value.y2 == null : value.x == null || value.x2 == null)) emit();
107
- else segment.push(value);
108
- }
109
- emit();
110
- return segments;
111
- }
112
- if (values.length < 2) {
113
- throw new Error(
114
- `Area series on mark "${layer.id}" requires at least two points.`
115
- );
116
- }
117
- return { key: item.key, values };
118
- });
119
- if (series.length === 0) throw new Error(`Area mark "${layer.id}" has no valid segment with at least two points.`);
120
- return cloneAndFreeze({
121
- orientation,
122
- xValues: series.flatMap(item => item.values.flatMap(value =>
123
- vertical ? [value.x] : [value.x, value.x2]
124
- )),
125
- yValues: series.flatMap(item => item.values.flatMap(value =>
126
- vertical ? [value.y, value.y2] : [value.y]
127
- )),
128
- series
129
- });
130
- }
131
-
132
- export function deriveCenteredAreaSeries(rows, layer, options = {}) {
133
- if (layer?.mark?.type !== "area") {
134
- throw new Error("Centered area series derivation requires an area mark.");
135
- }
136
- const { x, y, x2, y2, group, color } = layer.encoding ?? {};
137
- if (
138
- !["quantitative", "temporal", "nominal", "ordinal"].includes(x?.fieldType) ||
139
- y?.fieldType !== "quantitative" ||
140
- y?.stack !== "center" ||
141
- x2 !== undefined ||
142
- y2 !== undefined
143
- ) {
144
- throw new Error(
145
- `Centered area mark "${layer.id}" requires one x field and one quantitative y field without ranged endpoints.`
146
- );
147
- }
148
- if (group?.fieldType !== "nominal") {
149
- throw new Error(`Centered area mark "${layer.id}" requires a nominal group encoding.`);
150
- }
151
- if (color !== undefined && color.field !== group.field) {
152
- throw new Error(
153
- `Centered area color on mark "${layer.id}" must match its group field.`
154
- );
155
- }
156
- const categorical = ["nominal", "ordinal"].includes(x.fieldType);
157
- const xValues = categorical ? readNominalField(rows, x.field) : x.fieldType === "temporal"
158
- ? readTemporalField(rows, x.field, x.temporalUnit)
159
- : readQuantitativeField(rows, x.field);
160
- const yValues = readQuantitativeField(rows, y.field);
161
- const groups = readNominalField(rows, group.field);
162
- const groupOrder = [];
163
- const positions = new Set();
164
- const byGroup = new Map();
165
- for (let index = 0; index < rows.length; index += 1) {
166
- const key = groups[index];
167
- if (!byGroup.has(key)) {
168
- groupOrder.push(key);
169
- byGroup.set(key, new Map());
170
- }
171
- const values = byGroup.get(key);
172
- if (values.has(xValues[index])) {
173
- throw new Error(
174
- `Centered area mark "${layer.id}" has duplicate ${group.field}/${x.field} rows.`
175
- );
176
- }
177
- values.set(xValues[index], yValues[index]);
178
- positions.add(xValues[index]);
179
- }
180
- const orderedPositions = categorical
181
- ? (options.xDomain ?? [...positions]).filter(value => positions.has(value))
182
- : [...positions].sort((left, right) => left - right);
183
- if (orderedPositions.length !== positions.size) throw new Error("Area categorical domain must contain every observed position.");
184
- if (groupOrder.length === 0 || orderedPositions.length < 2) {
185
- throw new Error(
186
- `Centered area mark "${layer.id}" requires at least two aligned positions.`
187
- );
188
- }
189
- for (const [key, values] of byGroup) {
190
- if (
191
- values.size !== orderedPositions.length ||
192
- orderedPositions.some(position => !values.has(position))
193
- ) {
194
- throw new Error(
195
- `Centered area series "${String(key)}" requires one aligned value at every x position.`
196
- );
197
- }
198
- }
199
-
200
- const valuesBySeries = groupOrder.map(() => []);
201
- for (const position of orderedPositions) {
202
- const partition = groupOrder.map(key => byGroup.get(key).get(position));
203
- const resolvedSegments = layoutSeriesPartition(partition, "center");
204
- const segments = indexSegments(resolvedSegments);
205
- let endpoint = resolvedSegments[0]?.start ?? 0;
206
- for (let index = 0; index < groupOrder.length; index += 1) {
207
- const segment = segments[index];
208
- valuesBySeries[index].push({
209
- x: position,
210
- y: partition[index],
211
- lower: segment?.start ?? endpoint,
212
- upper: segment?.end ?? endpoint
213
- });
214
- if (segment !== undefined) endpoint = segment.end;
215
- }
216
- }
217
- const series = groupOrder.map((key, index) => ({
218
- key: {
219
- [group.field]: key,
220
- ...(color === undefined ? {} : { [color.field]: key })
221
- },
222
- values: valuesBySeries[index]
223
- }));
224
- return cloneAndFreeze({
225
- mode: "y-center",
226
- orientation: "vertical",
227
- xValues: orderedPositions,
228
- yValues: series.flatMap(item => item.values.flatMap(value => [
229
- value.lower,
230
- value.upper
231
- ])),
232
- series
233
- });
234
- }
235
-
236
- export function deriveDensityAreaSeries(rows, layer, transform) {
237
- if (layer?.mark?.type !== "area") {
238
- throw new Error("Density area derivation requires a semantic area mark.");
239
- }
240
- if (layer.encoding?.pathOrder !== undefined) {
241
- throw new Error(
242
- `Density area mark "${layer.id}" does not support path order.`
243
- );
244
- }
245
- if (transform?.type !== "density" || !Array.isArray(transform.as)) {
246
- throw new Error(`Area mark "${layer.id}" requires density provenance.`);
247
- }
248
- if (transform.placement?.type === "category") {
249
- return deriveCategoricalDensitySeries(rows, layer, transform);
250
- }
251
- const { x, y, group } = layer.encoding ?? {};
252
- if (
253
- x?.fieldType !== "quantitative" ||
254
- y?.fieldType !== "quantitative"
255
- ) {
256
- throw new Error(
257
- `Density area mark "${layer.id}" requires quantitative x and y encodings.`
258
- );
259
- }
260
- const [valueField, densityField] = transform.as;
261
- const mode = x.field === valueField && y.field === densityField
262
- ? "y-density"
263
- : x.field === densityField && y.field === valueField
264
- ? "x-density"
265
- : undefined;
266
- if (mode === undefined) {
267
- throw new Error(
268
- `Density area mark "${layer.id}" must encode its value and density fields.`
269
- );
270
- }
271
- if (transform.groupBy === undefined) {
272
- if (group !== undefined) {
273
- throw new Error(`Ungrouped density area mark "${layer.id}" cannot encode group.`);
274
- }
275
- } else if (
276
- group?.field !== transform.groupBy ||
277
- group.fieldType !== "nominal"
278
- ) {
279
- throw new Error(
280
- `Density area mark "${layer.id}" must group by "${transform.groupBy}".`
281
- );
282
- }
283
-
284
- const xValues = readQuantitativeField(rows, x.field);
285
- const yValues = readQuantitativeField(rows, y.field);
286
- const groupValues = transform.groupBy === undefined
287
- ? rows.map(() => undefined)
288
- : readNominalField(rows, transform.groupBy);
289
- const groups = new Map();
290
- for (let index = 0; index < rows.length; index += 1) {
291
- const key = groupValues[index];
292
- const series = groups.get(key) ?? {
293
- key: transform.groupBy === undefined ? {} : { [transform.groupBy]: key },
294
- values: []
295
- };
296
- series.values.push({ x: xValues[index], y: yValues[index] });
297
- groups.set(key, series);
298
- }
299
- if (groups.size === 0) {
300
- throw new Error(`Density area mark "${layer.id}" has no values.`);
301
- }
302
- const valueKey = mode === "y-density" ? "x" : "y";
303
- const series = [...groups.values()].map(item => {
304
- const values = item.values.sort((left, right) => left[valueKey] - right[valueKey]);
305
- if (values.length < 2) {
306
- throw new Error(
307
- `Density area series on mark "${layer.id}" requires at least two points.`
308
- );
309
- }
310
- return { key: item.key, values };
311
- });
312
- return cloneAndFreeze({ mode, series });
313
- }
314
-
315
- export function layoutDensityAreaSeries(derived, layout = "overlay") {
316
- validateColorLayout(layout);
317
- if (layout === "group") {
318
- throw new Error('Density area series do not support "group" layout.');
319
- }
320
- if (derived?.mode !== "y-density") {
321
- if (layout === "overlay") return derived;
322
- throw new Error(
323
- `Area layout "${layout}" currently requires vertical density series.`
324
- );
325
- }
326
- const sampleCount = derived.series[0]?.values.length ?? 0;
327
- if (
328
- sampleCount === 0 ||
329
- derived.series.some(series => series.values.length !== sampleCount)
330
- ) {
331
- throw new Error("Density area layout requires aligned non-empty samples.");
332
- }
333
-
334
- const valuesBySeries = derived.series.map(() => []);
335
- for (let sample = 0; sample < sampleCount; sample += 1) {
336
- const x = derived.series[0].values[sample].x;
337
- const densities = derived.series.map(series => {
338
- if (series.values[sample].x !== x) {
339
- throw new Error("Density area layout requires one shared sample grid.");
340
- }
341
- return series.values[sample].y;
342
- });
343
- const resolvedSegments = layoutSeriesPartition(densities, layout);
344
- const segments = indexSegments(resolvedSegments);
345
- let zeroThicknessEndpoint = layout === "center"
346
- ? resolvedSegments[0]?.start ?? 0
347
- : 0;
348
- for (let index = 0; index < derived.series.length; index += 1) {
349
- const segment = segments[index];
350
- valuesBySeries[index].push({
351
- x,
352
- lower: segment?.start ?? zeroThicknessEndpoint,
353
- upper: segment?.end ?? zeroThicknessEndpoint
354
- });
355
- if (segment !== undefined && ["stack", "fill", "center"].includes(layout)) {
356
- zeroThicknessEndpoint = segment.end;
357
- }
358
- }
359
- }
360
-
361
- return cloneAndFreeze({
362
- mode: derived.mode,
363
- series: derived.series.map((series, index) => ({
364
- key: series.key,
365
- values: valuesBySeries[index]
366
- }))
367
- });
368
- }
369
-
370
- export function layoutRawAreaGroups(groups, { layer, vertical, primary, secondary }) {
371
- const mode = layer.layout?.mode ?? "overlay";
372
- validateColorLayout(mode);
373
- if (mode === "overlay") return;
374
- if (mode === "group" || (mode === "center" && !vertical)) {
375
- throw new Error(`Area layout "${mode}" is incompatible with this orientation.`);
376
- }
377
- const primaryDatum = Object.hasOwn(primary, "datum");
378
- const secondaryDatum = Object.hasOwn(secondary, "datum");
379
- if (primaryDatum === secondaryDatum || (primaryDatum ? primary.datum : secondary.datum) !== 0) {
380
- throw new Error("Stacked area requires one value field and one zero datum endpoint.");
381
- }
382
- const position = vertical ? "x" : "y";
383
- const lower = vertical ? "y" : "x", upper = `${lower}2`;
384
- const measure = primaryDatum ? upper : lower;
385
- const series = [...groups.values()];
386
- const indices = series.map(item => {
387
- const index = new Map();
388
- for (const value of item.values) {
389
- if (index.has(value[position])) throw new Error("Area layout requires unique group/position rows.");
390
- index.set(value[position], value);
391
- }
392
- return index;
393
- });
394
- const positions = [...new Set(series.flatMap(item => item.values.map(value => value[position])))].sort((a, b) => a - b);
395
- validateGeneratedItemLimit(positions.length * series.length, "Area layout cell count");
396
- if (indices.some(index => index.size !== positions.length || positions.some(value => !index.has(value)))) {
397
- throw new Error("Area layout requires an aligned row for every group and position.");
398
- }
399
- for (const position of positions) {
400
- const points = indices.map(index => index.get(position));
401
- if (points.some(point => point[measure] == null)) {
402
- for (const point of points) { point[lower] = null; point[upper] = null; }
403
- continue;
404
- }
405
- const segments = layoutSeriesPartition(points.map(point => point[measure]), mode);
406
- const byIndex = new Map(segments.map(segment => [segment.index, segment]));
407
- let endpoint = mode === "center" ? segments[0]?.start ?? 0 : 0;
408
- points.forEach((point, index) => {
409
- const segment = byIndex.get(index);
410
- point[lower] = segment?.start ?? (mode === "diverging" ? 0 : endpoint);
411
- point[upper] = segment?.end ?? (mode === "diverging" ? 0 : endpoint);
412
- if (segment !== undefined && mode !== "diverging") endpoint = segment.end;
413
- });
414
- }
415
- }
1
+ import{cloneAndFreeze as C}from"../core/immutable.js";import{readNominalField as z,readQuantitativeField as b,readTemporalField as I}from"./scales/index.js";import{layoutSeriesPartition as B,validateColorLayout as F}from"./seriesLayout.js";import{stableOrderPathValues as L}from"./pathOrder.js";import{deriveCategoricalDensitySeries as N}from"./categoricalDensity.js";import{validatePathSeriesAppearance as P}from"./pathSeries.js";import{readAreaEndpoint as j,validateAreaEndpointPair as J}from"./areaEndpoints.js";import{validateGeneratedItemLimit as K}from"../core/validation.js";function G(n){const e=[];for(const a of n)e[a.index]=a;return e}function ne(n,e,a={}){if(e?.mark?.type!=="area")throw new Error("Area series derivation requires a semantic area mark.");const{x:p,y:d,x2:l,y2:k}=e.encoding??{},u=["quantitative","temporal","nominal","ordinal"].includes(p?.fieldType)&&d?.fieldType==="quantitative"&&k?.fieldType==="quantitative"&&l===void 0,m=["quantitative","temporal","nominal","ordinal"].includes(d?.fieldType)&&p?.fieldType==="quantitative"&&l?.fieldType==="quantitative"&&k===void 0;if(u===m)throw new Error(`Area mark "${e.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative, temporal, or categorical independent position.`);const w=P(n,e),c=u?"vertical":"horizontal",f=u?p:d,x=["nominal","ordinal"].includes(f.fieldType),y=x?z(n,f.field):f.fieldType==="temporal"?I(n,f.field,f.temporalUnit):b(n,f.field);if(Object.hasOwn(f,"datum"))throw new Error("Area independent position requires a field.");const v=u?d:p,o=u?k:l;J(v,o);const r=e.mark.missing??"error",g=j(n,v,r),O=j(n,o,r),s=x?a[u?"xDomain":"yDomain"]??[...new Set(y)]:void 0,t=new Map(s?.map((i,h)=>[i,h]));if(x&&y.some(i=>!t.has(i)))throw new Error("Area categorical domain must contain every observed position.");const E=e.encoding?.pathOrder,$=E===void 0?void 0:b(n,E.field),D=new Map;for(let i=0;i<n.length;i+=1){const h=w.map(q=>n[i][q]),M=JSON.stringify(h),T=D.get(M)??{key:Object.fromEntries(w.map((q,V)=>[q,h[V]])),values:[]};T.values.push(u?{x:y[i],y:g[i],y2:O[i],...r==="break"?{sourceIndex:i}:{},...$===void 0?{}:{pathOrder:$[i]}}:{x:g[i],x2:O[i],...r==="break"?{sourceIndex:i}:{},y:y[i],...$===void 0?{}:{pathOrder:$[i]}}),D.set(M,T)}if(D.size===0)throw new Error(`Area mark "${e.id}" has no values.`);Q(D,{layer:e,vertical:u,primary:v,secondary:o});const A=[...D.values()].flatMap(i=>{const h=u?"x":"y",M=E===void 0?i.values.sort((T,q)=>x?t.get(T[h])-t.get(q[h]):T[h]-q[h]):L(i.values.map(({pathOrder:T,...q})=>q),i.values.map(T=>T.pathOrder),E.order);if(r==="break"){const T=[];let q=[];const V=()=>{q.length>=2&&T.push({key:i.key,sourceIndices:q.map(S=>S.sourceIndex),values:q.map(({sourceIndex:S,...U})=>U)}),q=[]};for(const S of M)(u?S.y==null||S.y2==null:S.x==null||S.x2==null)?V():q.push(S);return V(),T}if(M.length<2)throw new Error(`Area series on mark "${e.id}" requires at least two points.`);return{key:i.key,values:M}});if(A.length===0)throw new Error(`Area mark "${e.id}" has no valid segment with at least two points.`);return C({orientation:c,xValues:A.flatMap(i=>i.values.flatMap(h=>u?[h.x]:[h.x,h.x2])),yValues:A.flatMap(i=>i.values.flatMap(h=>u?[h.y,h.y2]:[h.y])),series:A})}function te(n,e,a={}){if(e?.mark?.type!=="area")throw new Error("Centered area series derivation requires an area mark.");const{x:p,y:d,x2:l,y2:k,group:u,color:m}=e.encoding??{};if(!["quantitative","temporal","nominal","ordinal"].includes(p?.fieldType)||d?.fieldType!=="quantitative"||d?.stack!=="center"||l!==void 0||k!==void 0)throw new Error(`Centered area mark "${e.id}" requires one x field and one quantitative y field without ranged endpoints.`);if(u?.fieldType!=="nominal")throw new Error(`Centered area mark "${e.id}" requires a nominal group encoding.`);if(m!==void 0&&m.fieldType!=="quantitative"&&m.field!==u.field)throw new Error(`Centered area color on mark "${e.id}" must match its group field.`);P(n,e);const w=["nominal","ordinal"].includes(p.fieldType),c=w?z(n,p.field):p.fieldType==="temporal"?I(n,p.field,p.temporalUnit):b(n,p.field),f=b(n,d.field),x=z(n,u.field),y=[],v=new Set,o=new Map;for(let s=0;s<n.length;s+=1){const t=x[s];o.has(t)||(y.push(t),o.set(t,new Map));const E=o.get(t);if(E.has(c[s]))throw new Error(`Centered area mark "${e.id}" has duplicate ${u.field}/${p.field} rows.`);E.set(c[s],f[s]),v.add(c[s])}const r=w?(a.xDomain??[...v]).filter(s=>v.has(s)):[...v].sort((s,t)=>s-t);if(r.length!==v.size)throw new Error("Area categorical domain must contain every observed position.");if(y.length===0||r.length<2)throw new Error(`Centered area mark "${e.id}" requires at least two aligned positions.`);for(const[s,t]of o)if(t.size!==r.length||r.some(E=>!t.has(E)))throw new Error(`Centered area series "${String(s)}" requires one aligned value at every x position.`);const g=y.map(()=>[]);for(const s of r){const t=y.map(A=>o.get(A).get(s)),E=B(t,"center"),$=G(E);let D=E[0]?.start??0;for(let A=0;A<y.length;A+=1){const i=$[A];g[A].push({x:s,y:t[A],lower:i?.start??D,upper:i?.end??D}),i!==void 0&&(D=i.end)}}const O=y.map((s,t)=>({key:{[u.field]:s,...m===void 0||m.fieldType==="quantitative"?{}:{[m.field]:s}},values:g[t]}));return C({mode:"y-center",orientation:"vertical",xValues:r,yValues:O.flatMap(s=>s.values.flatMap(t=>[t.lower,t.upper])),series:O})}function re(n,e,a){if(e?.mark?.type!=="area")throw new Error("Density area derivation requires a semantic area mark.");if(e.encoding?.pathOrder!==void 0)throw new Error(`Density area mark "${e.id}" does not support path order.`);if(a?.type!=="density"||!Array.isArray(a.as))throw new Error(`Area mark "${e.id}" requires density provenance.`);if(a.placement?.type==="category")return N(n,e,a);const{x:p,y:d,group:l}=e.encoding??{};if(p?.fieldType!=="quantitative"||d?.fieldType!=="quantitative")throw new Error(`Density area mark "${e.id}" requires quantitative x and y encodings.`);const[k,u]=a.as,m=p.field===k&&d.field===u?"y-density":p.field===u&&d.field===k?"x-density":void 0;if(m===void 0)throw new Error(`Density area mark "${e.id}" must encode its value and density fields.`);if(a.groupBy===void 0){if(l!==void 0)throw new Error(`Ungrouped density area mark "${e.id}" cannot encode group.`)}else if(l?.field!==a.groupBy||l.fieldType!=="nominal")throw new Error(`Density area mark "${e.id}" must group by "${a.groupBy}".`);const w=b(n,p.field),c=b(n,d.field),f=a.groupBy===void 0?n.map(()=>{}):z(n,a.groupBy),x=new Map;for(let o=0;o<n.length;o+=1){const r=f[o],g=x.get(r)??{key:a.groupBy===void 0?{}:{[a.groupBy]:r},values:[]};g.values.push({x:w[o],y:c[o]}),x.set(r,g)}if(x.size===0)throw new Error(`Density area mark "${e.id}" has no values.`);const y=m==="y-density"?"x":"y",v=[...x.values()].map(o=>{const r=o.values.sort((g,O)=>g[y]-O[y]);if(r.length<2)throw new Error(`Density area series on mark "${e.id}" requires at least two points.`);return{key:o.key,values:r}});return C({mode:m,series:v})}function ie(n,e="overlay"){if(F(e),e==="group")throw new Error('Density area series do not support "group" layout.');if(n?.mode!=="y-density"){if(e==="overlay")return n;throw new Error(`Area layout "${e}" currently requires vertical density series.`)}const a=n.series[0]?.values.length??0;if(a===0||n.series.some(d=>d.values.length!==a))throw new Error("Density area layout requires aligned non-empty samples.");const p=n.series.map(()=>[]);for(let d=0;d<a;d+=1){const l=n.series[0].values[d].x,k=n.series.map(c=>{if(c.values[d].x!==l)throw new Error("Density area layout requires one shared sample grid.");return c.values[d].y}),u=B(k,e),m=G(u);let w=e==="center"?u[0]?.start??0:0;for(let c=0;c<n.series.length;c+=1){const f=m[c];p[c].push({x:l,lower:f?.start??w,upper:f?.end??w}),f!==void 0&&["stack","fill","center"].includes(e)&&(w=f.end)}}return C({mode:n.mode,series:n.series.map((d,l)=>({key:d.key,values:p[l]}))})}function Q(n,{layer:e,vertical:a,primary:p,secondary:d}){const l=e.layout?.mode??"overlay";if(F(l),l==="overlay")return;if(l==="group"||l==="center"&&!a)throw new Error(`Area layout "${l}" is incompatible with this orientation.`);const k=Object.hasOwn(p,"datum"),u=Object.hasOwn(d,"datum");if(k===u||(k?p.datum:d.datum)!==0)throw new Error("Stacked area requires one value field and one zero datum endpoint.");const m=a?"x":"y",w=a?"y":"x",c=`${w}2`,f=k?c:w,x=[...n.values()],y=x.map(o=>{const r=new Map;for(const g of o.values){if(r.has(g[m]))throw new Error("Area layout requires unique group/position rows.");r.set(g[m],g)}return r}),v=[...new Set(x.flatMap(o=>o.values.map(r=>r[m])))].sort((o,r)=>o-r);if(K(v.length*x.length,"Area layout cell count"),y.some(o=>o.size!==v.length||v.some(r=>!o.has(r))))throw new Error("Area layout requires an aligned row for every group and position.");for(const o of v){const r=y.map(t=>t.get(o));if(r.some(t=>t[f]==null)){for(const t of r)t[w]=null,t[c]=null;continue}const g=B(r.map(t=>t[f]),l),O=new Map(g.map(t=>[t.index,t]));let s=l==="center"?g[0]?.start??0:0;r.forEach((t,E)=>{const $=O.get(E);t[w]=$?.start??(l==="diverging"?0:s),t[c]=$?.end??(l==="diverging"?0:s),$!==void 0&&l!=="diverging"&&(s=$.end)})}}export{ne as deriveAreaSeries,te as deriveCenteredAreaSeries,re as deriveDensityAreaSeries,ie as layoutDensityAreaSeries,Q as layoutRawAreaGroups};
@@ -0,0 +1 @@
1
+ import{resolveTextLines as o}from"../core/textMetrics.js";function a(e,{title:i=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(i){if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");for(const t of["offset","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Axis title ${t} must be a finite number.`)}}function f(e,i){if(!/[\r\n]/u.test(e))return;const t=i.baseline??"middle",n=o(e,{fontSize:i.fontSize,align:i.align??"center",baseline:t,...t==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(t)){const s=(n.length-1)*i.fontSize*1.2;return n.map(r=>({...r,y:r.y-s}))}return n.length>1?n:void 0}export{f as axisTitleTextLines,a as validateAxisTextPlacement};
@@ -0,0 +1,39 @@
1
+ import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
2
+ import {isContinuousColorScaleType} from './scales/types.js';
3
+ import {isContinuousSizeScaleType} from './scales/size.js';
4
+
5
+ export function isColorSizeChannels(channels) {
6
+ return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
7
+ }
8
+
9
+ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
10
+ const color=layer?.encoding?.color,size=layer?.encoding?.size;
11
+ const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
12
+ if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
13
+ pointConfig?.shape!=='circle'||layer.encoding.shape!==undefined||
14
+ color?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
15
+ typeof color.field!=='string'||color.field!==size.field||
16
+ color.aggregate!==size.aggregate||color.data!==size.data||
17
+ !isContinuousColorScaleType(colorScale?.type)||!isContinuousSizeScaleType(sizeScale?.type)) {
18
+ throw new Error('Color-size combination requires one Cartesian circle point owner with the same quantitative field and grain.');
19
+ }
20
+ const ends=scale=>[scale.domain?.[0],scale.domain?.at(-1)];
21
+ const domain=ends(colorScale),sizeDomain=ends(sizeScale);
22
+ if(!domain.every(Number.isFinite)||!sizeDomain.every(Number.isFinite)||
23
+ !sameOrderedValues(domain,sizeDomain)||domain[0]===domain[1]) {
24
+ throw new Error('Color-size combination requires identical finite numeric domain endpoints.');
25
+ }
26
+ return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
27
+ }
28
+
29
+ export function normalizeColorSizeSymbol(value,previous) {
30
+ if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
31
+ if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
32
+ validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
33
+ if(value.type!==undefined&&value.type!=='point')throw new Error('Color-size symbols require type point.');
34
+ const symbol={type:'point',strokeWidth:0,opacity:1,...previous,...value};
35
+ if(symbol.stroke!==undefined)validateNonEmptyString(symbol.stroke,'Color-size symbol stroke');
36
+ validateNonNegativeFinite(symbol.strokeWidth,'Color-size symbol strokeWidth');
37
+ validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
38
+ return symbol;
39
+ }
@@ -1,4 +1,5 @@
1
1
  import {parseTimeValue,validateParseTimeFormat} from "./parseTime.js";
2
+ import { calendarParts, validateTimeZone } from "./timeUnit.js";
2
3
  import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
4
  import { requireStringValue as requireField } from "../core/validation.js";
4
5
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
@@ -6,7 +7,7 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
6
7
  const TRANSFORM_KEYS = Object.freeze(["type", "as", "expression"]);
7
8
  const ARITHMETIC_BINARY = new Set(["add", "subtract", "multiply", "divide"]);
8
9
  const COMPARISONS = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
9
- const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt"]);
10
+ const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt", "floor"]);
10
11
  const BOOLEAN_UNARY = new Set(["not", "isNull"]);
11
12
  const LOGICAL = new Set(["and", "or"]);
12
13
  const VARIADIC = new Set(["coalesce", "concat"]);
@@ -107,6 +108,14 @@ function inspectExpression(expression, depth, state, optionalPathRoot = false) {
107
108
  inspectExpression(expression.operand, depth + 1, state);
108
109
  return;
109
110
  }
111
+ if (expression.op === "datePart") {
112
+ rejectUnknownKeys(expression, ["op", "part", "operand", "timeZone"], "computed calendar expression");
113
+ requireNode(expression, "operand", expression.op);
114
+ if (expression.part !== "year") throw new Error("Computed datePart supports part year only.");
115
+ if (Object.hasOwn(expression, "timeZone")) validateTimeZone(expression.timeZone);
116
+ inspectExpression(expression.operand, depth + 1, state);
117
+ return;
118
+ }
110
119
  if (expression.op === "parseTime") {
111
120
  rejectUnknownKeys(expression, ["op", "operand", "format"], "computed time expression");
112
121
  requireNode(expression, "operand", expression.op);
@@ -339,6 +348,18 @@ function evaluate(expression, row, rowIndex) {
339
348
  ? value === null
340
349
  : !boolean(value, operation, rowIndex);
341
350
  }
351
+ if (operation === "datePart") {
352
+ const value = evaluate(expression.operand, row, rowIndex);
353
+ if (value === null) return null;
354
+ number(value, operation, rowIndex);
355
+ if (Math.abs(value) > 8_640_000_000_000_000) {
356
+ throw new RangeError(`Computed datePart timestamp is outside the supported Date range at row ${rowIndex}.`);
357
+ }
358
+ const timestamp = new Date(value).getTime();
359
+ return (expression.timeZone ?? "UTC") === "UTC"
360
+ ? new Date(timestamp).getUTCFullYear()
361
+ : calendarParts(timestamp, expression.timeZone).year;
362
+ }
342
363
  if (operation === "parseTime") {
343
364
  return parseTimeValue(evaluate(expression.operand, row, rowIndex), expression.format);
344
365
  }
@@ -360,6 +381,7 @@ function evaluate(expression, row, rowIndex) {
360
381
  }
361
382
  const result = operation === "negate" ? -value
362
383
  : operation === "absolute" ? Math.abs(value)
384
+ : operation === "floor" ? Math.floor(value)
363
385
  : operation === "log" ? Math.log(value)
364
386
  : Math.sqrt(value);
365
387
  if (!Number.isFinite(result)) {
@@ -1,6 +1,7 @@
1
1
  import { requireStringValue as requireName } from "../core/validation.js";
2
2
  import { annotateError } from "../core/diagnostics.js";
3
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
4
+ import { requireRegressionStatisticsSource, REGRESSION_STATISTIC_FIELDS } from "./regression/statistics.js";
4
5
 
5
6
  const STORAGE_TYPES = new Set([
6
7
  "number", "string", "boolean", "array", "object", "unknown", "mixed"
@@ -267,7 +268,7 @@ function generatedField(name, storageType, transform, role, inputs = []) {
267
268
  };
268
269
  }
269
270
 
270
- export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
271
+ export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId, sourceTransform } = {}) {
271
272
  const input = validateDatasetSchema(inputSchema);
272
273
  const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
273
274
  const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
@@ -300,6 +301,12 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
300
301
  generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
301
302
  ];
302
303
  if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
304
+ } else if (transform.type === "regressionStatistics") {
305
+ const fit = requireRegressionStatisticsSource({ transform: sourceTransform === undefined ? [] : [sourceTransform] });
306
+ fields = [
307
+ ...(fit.groupBy === undefined ? [] : [inheritedField(input, fit.groupBy)]),
308
+ ...REGRESSION_STATISTIC_FIELDS.map(name => generatedField(name, "number", definition, name))
309
+ ].filter(Boolean);
303
310
  } else if (transform.type === "regression") {
304
311
  fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
305
312
  if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
@@ -367,7 +374,8 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
367
374
  if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
368
375
  return planned;
369
376
  }
370
- return { ...planned, storageType: field.storageType, nullable: field.nullable, optional: field.optional };
377
+ return { ...planned, storageType: transform.type === "regressionStatistics" && planned.storageType === "number"
378
+ ? "number" : field.storageType, nullable: field.nullable, optional: field.optional };
371
379
  });
372
380
  for (const field of inferred.fields) {
373
381
  if (!fields.some(planned => planned.name === field.name)) fields.push(field);
@@ -44,6 +44,14 @@ export function normalizeDisplayLabelMap(value, label = "labelMap") {
44
44
  return Object.freeze(normalized);
45
45
  }
46
46
 
47
+ export function normalizeNumericDisplayLabelMap(value, label = "labelMap") {
48
+ const map = normalizeDisplayLabelMap(value, label);
49
+ if (map.some(entry => typeof entry.value !== "number" || !Number.isFinite(entry.value))) {
50
+ throw new TypeError(`${label} values must be finite numbers.`);
51
+ }
52
+ return map;
53
+ }
54
+
47
55
  export function resolveDisplayLabel(value, map, fallback) {
48
56
  if (map !== undefined) {
49
57
  for (const entry of map) {