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.
- package/CHANGELOG.md +56 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/color/continuous.js +7 -1
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +32 -9
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/area/materialize.js +1 -190
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/consumers/common.js +7 -1
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/aggregate.js +3 -2
- package/src/grammar/areaSeries.js +1 -415
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/interval.js +22 -6
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pathSeries.js +2 -2
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +6 -3
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/statistics/confidenceInterval.js +1 -101
- package/src/grammar/summary.js +1 -320
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/grouping.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +7 -0
- package/types/program.d.ts +220 -64
|
@@ -1,415 +1 @@
|
|
|
1
|
-
import {
|
|
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
|
+
}
|
package/src/grammar/computed.js
CHANGED
|
@@ -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:
|
|
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) {
|