ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
readNominalField,
|
|
4
|
+
readQuantitativeField,
|
|
5
|
+
readTemporalField
|
|
6
|
+
} from "./scales.js";
|
|
7
|
+
import {
|
|
8
|
+
layoutSeriesPartition,
|
|
9
|
+
validateColorLayout
|
|
10
|
+
} from "./seriesLayout.js";
|
|
11
|
+
|
|
12
|
+
export function deriveAreaSeries(rows, layer) {
|
|
13
|
+
if (layer?.mark?.type !== "area") {
|
|
14
|
+
throw new Error("Area series derivation requires a semantic area mark.");
|
|
15
|
+
}
|
|
16
|
+
const { x, y, x2, y2, group } = layer.encoding ?? {};
|
|
17
|
+
const vertical =
|
|
18
|
+
["quantitative", "temporal"].includes(x?.fieldType) &&
|
|
19
|
+
y?.fieldType === "quantitative" &&
|
|
20
|
+
y2?.fieldType === "quantitative" &&
|
|
21
|
+
x2 === undefined;
|
|
22
|
+
const horizontal =
|
|
23
|
+
["quantitative", "temporal"].includes(y?.fieldType) &&
|
|
24
|
+
x?.fieldType === "quantitative" &&
|
|
25
|
+
x2?.fieldType === "quantitative" &&
|
|
26
|
+
y2 === undefined;
|
|
27
|
+
if (vertical === horizontal) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or temporal independent position.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (group !== undefined && group.fieldType !== "nominal") {
|
|
33
|
+
throw new Error(`Area group encoding on mark "${layer.id}" must be nominal.`);
|
|
34
|
+
}
|
|
35
|
+
const orientation = vertical ? "vertical" : "horizontal";
|
|
36
|
+
const independent = vertical ? x : y;
|
|
37
|
+
const independentValues = independent.fieldType === "temporal"
|
|
38
|
+
? readTemporalField(rows, independent.field)
|
|
39
|
+
: readQuantitativeField(rows, independent.field);
|
|
40
|
+
const lower = vertical
|
|
41
|
+
? readQuantitativeField(rows, y.field)
|
|
42
|
+
: readQuantitativeField(rows, x.field);
|
|
43
|
+
const upper = vertical
|
|
44
|
+
? readQuantitativeField(rows, y2.field)
|
|
45
|
+
: readQuantitativeField(rows, x2.field);
|
|
46
|
+
const groupValues = group === undefined
|
|
47
|
+
? rows.map(() => undefined)
|
|
48
|
+
: readNominalField(rows, group.field);
|
|
49
|
+
const groups = new Map();
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
52
|
+
const key = groupValues[index];
|
|
53
|
+
const series = groups.get(key) ?? {
|
|
54
|
+
key: group === undefined ? {} : { [group.field]: key },
|
|
55
|
+
values: []
|
|
56
|
+
};
|
|
57
|
+
series.values.push(vertical
|
|
58
|
+
? {
|
|
59
|
+
x: independentValues[index],
|
|
60
|
+
y: lower[index],
|
|
61
|
+
y2: upper[index]
|
|
62
|
+
}
|
|
63
|
+
: {
|
|
64
|
+
x: lower[index],
|
|
65
|
+
x2: upper[index],
|
|
66
|
+
y: independentValues[index]
|
|
67
|
+
});
|
|
68
|
+
groups.set(key, series);
|
|
69
|
+
}
|
|
70
|
+
if (groups.size === 0) {
|
|
71
|
+
throw new Error(`Area mark "${layer.id}" has no values.`);
|
|
72
|
+
}
|
|
73
|
+
const series = [...groups.values()].map(item => {
|
|
74
|
+
const key = vertical ? "x" : "y";
|
|
75
|
+
const values = item.values.sort((left, right) => left[key] - right[key]);
|
|
76
|
+
if (values.length < 2) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Area series on mark "${layer.id}" requires at least two points.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return { key: item.key, values };
|
|
82
|
+
});
|
|
83
|
+
return cloneAndFreeze({
|
|
84
|
+
orientation,
|
|
85
|
+
xValues: series.flatMap(item => item.values.flatMap(value =>
|
|
86
|
+
vertical ? [value.x] : [value.x, value.x2]
|
|
87
|
+
)),
|
|
88
|
+
yValues: series.flatMap(item => item.values.flatMap(value =>
|
|
89
|
+
vertical ? [value.y, value.y2] : [value.y]
|
|
90
|
+
)),
|
|
91
|
+
series
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function deriveDensityAreaSeries(rows, layer, transform) {
|
|
96
|
+
if (layer?.mark?.type !== "area") {
|
|
97
|
+
throw new Error("Density area derivation requires a semantic area mark.");
|
|
98
|
+
}
|
|
99
|
+
if (transform?.type !== "density" || !Array.isArray(transform.as)) {
|
|
100
|
+
throw new Error(`Area mark "${layer.id}" requires density provenance.`);
|
|
101
|
+
}
|
|
102
|
+
const { x, y, group } = layer.encoding ?? {};
|
|
103
|
+
if (
|
|
104
|
+
x?.fieldType !== "quantitative" ||
|
|
105
|
+
y?.fieldType !== "quantitative"
|
|
106
|
+
) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Density area mark "${layer.id}" requires quantitative x and y encodings.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const [valueField, densityField] = transform.as;
|
|
112
|
+
const mode = x.field === valueField && y.field === densityField
|
|
113
|
+
? "y-density"
|
|
114
|
+
: x.field === densityField && y.field === valueField
|
|
115
|
+
? "x-density"
|
|
116
|
+
: undefined;
|
|
117
|
+
if (mode === undefined) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Density area mark "${layer.id}" must encode its value and density fields.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (transform.groupBy === undefined) {
|
|
123
|
+
if (group !== undefined) {
|
|
124
|
+
throw new Error(`Ungrouped density area mark "${layer.id}" cannot encode group.`);
|
|
125
|
+
}
|
|
126
|
+
} else if (
|
|
127
|
+
group?.field !== transform.groupBy ||
|
|
128
|
+
group.fieldType !== "nominal"
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Density area mark "${layer.id}" must group by "${transform.groupBy}".`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const xValues = readQuantitativeField(rows, x.field);
|
|
136
|
+
const yValues = readQuantitativeField(rows, y.field);
|
|
137
|
+
const groupValues = transform.groupBy === undefined
|
|
138
|
+
? rows.map(() => undefined)
|
|
139
|
+
: readNominalField(rows, transform.groupBy);
|
|
140
|
+
const groups = new Map();
|
|
141
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
142
|
+
const key = groupValues[index];
|
|
143
|
+
const series = groups.get(key) ?? {
|
|
144
|
+
key: transform.groupBy === undefined ? {} : { [transform.groupBy]: key },
|
|
145
|
+
values: []
|
|
146
|
+
};
|
|
147
|
+
series.values.push({ x: xValues[index], y: yValues[index] });
|
|
148
|
+
groups.set(key, series);
|
|
149
|
+
}
|
|
150
|
+
if (groups.size === 0) {
|
|
151
|
+
throw new Error(`Density area mark "${layer.id}" has no values.`);
|
|
152
|
+
}
|
|
153
|
+
const valueKey = mode === "y-density" ? "x" : "y";
|
|
154
|
+
const series = [...groups.values()].map(item => {
|
|
155
|
+
const values = item.values.sort((left, right) => left[valueKey] - right[valueKey]);
|
|
156
|
+
if (values.length < 2) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Density area series on mark "${layer.id}" requires at least two points.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return { key: item.key, values };
|
|
162
|
+
});
|
|
163
|
+
return cloneAndFreeze({ mode, series });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function layoutDensityAreaSeries(derived, layout = "overlay") {
|
|
167
|
+
validateColorLayout(layout);
|
|
168
|
+
if (layout === "group") {
|
|
169
|
+
throw new Error('Density area series do not support "group" layout.');
|
|
170
|
+
}
|
|
171
|
+
if (derived?.mode !== "y-density") {
|
|
172
|
+
if (layout === "overlay") return derived;
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Area layout "${layout}" currently requires vertical density series.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const sampleCount = derived.series[0]?.values.length ?? 0;
|
|
178
|
+
if (
|
|
179
|
+
sampleCount === 0 ||
|
|
180
|
+
derived.series.some(series => series.values.length !== sampleCount)
|
|
181
|
+
) {
|
|
182
|
+
throw new Error("Density area layout requires aligned non-empty samples.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const valuesBySeries = derived.series.map(() => []);
|
|
186
|
+
for (let sample = 0; sample < sampleCount; sample += 1) {
|
|
187
|
+
const x = derived.series[0].values[sample].x;
|
|
188
|
+
const densities = derived.series.map(series => {
|
|
189
|
+
if (series.values[sample].x !== x) {
|
|
190
|
+
throw new Error("Density area layout requires one shared sample grid.");
|
|
191
|
+
}
|
|
192
|
+
return series.values[sample].y;
|
|
193
|
+
});
|
|
194
|
+
const segments = new Map(
|
|
195
|
+
layoutSeriesPartition(densities, layout).map(segment => [
|
|
196
|
+
segment.index,
|
|
197
|
+
segment
|
|
198
|
+
])
|
|
199
|
+
);
|
|
200
|
+
for (let index = 0; index < derived.series.length; index += 1) {
|
|
201
|
+
const segment = segments.get(index);
|
|
202
|
+
valuesBySeries[index].push({
|
|
203
|
+
x,
|
|
204
|
+
lower: segment?.start ?? 0,
|
|
205
|
+
upper: segment?.end ?? 0
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return cloneAndFreeze({
|
|
211
|
+
mode: derived.mode,
|
|
212
|
+
series: derived.series.map((series, index) => ({
|
|
213
|
+
key: series.key,
|
|
214
|
+
values: valuesBySeries[index]
|
|
215
|
+
}))
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
import { readNominalField, readTemporalField } from "../scales.js";
|
|
3
|
+
import {
|
|
4
|
+
aggregateRows,
|
|
5
|
+
validateAggregateFieldType,
|
|
6
|
+
validateAggregateFieldValues,
|
|
7
|
+
} from "../aggregate.js";
|
|
8
|
+
import {
|
|
9
|
+
BAR_GRAINS,
|
|
10
|
+
resolveBarChannels,
|
|
11
|
+
resolveBarGrain
|
|
12
|
+
} from "./policy.js";
|
|
13
|
+
|
|
14
|
+
function requireAggregateBarEncoding(layer) {
|
|
15
|
+
if (layer?.mark?.type !== "bar") {
|
|
16
|
+
throw new Error("Bar aggregate derivation requires a semantic bar mark.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const channels = resolveBarChannels(layer);
|
|
20
|
+
if (resolveBarGrain(layer) !== BAR_GRAINS.aggregate) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Bar mark "${layer.id}" requires a categorical position and quantitative aggregate measure.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const category = layer.encoding[channels.category];
|
|
26
|
+
const measure = layer.encoding[channels.measure];
|
|
27
|
+
validateAggregateFieldType(measure.aggregate, measure.fieldType);
|
|
28
|
+
|
|
29
|
+
return { channels, category, measure };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function deriveBarAggregates(rows, layer) {
|
|
33
|
+
const { channels, category, measure } = requireAggregateBarEncoding(layer);
|
|
34
|
+
validateAggregateFieldValues(rows, measure.field, measure.fieldType);
|
|
35
|
+
const categoryValues = category.fieldType === "temporal"
|
|
36
|
+
? readTemporalField(rows, category.field)
|
|
37
|
+
: readNominalField(rows, category.field);
|
|
38
|
+
const color = layer.encoding?.color;
|
|
39
|
+
let colorValues;
|
|
40
|
+
let colorAggregate;
|
|
41
|
+
|
|
42
|
+
if (color !== undefined) {
|
|
43
|
+
if (color.fieldType === "nominal") {
|
|
44
|
+
colorValues = readNominalField(rows, color.field);
|
|
45
|
+
} else if (color.fieldType === "quantitative") {
|
|
46
|
+
colorAggregate = color.aggregate;
|
|
47
|
+
validateAggregateFieldType(colorAggregate, color.fieldType);
|
|
48
|
+
validateAggregateFieldValues(rows, color.field, color.fieldType);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Bar color encoding on mark "${layer.id}" must be nominal or aggregate quantitative.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const groups = new Map();
|
|
56
|
+
|
|
57
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
58
|
+
const categoryValue = categoryValues[index];
|
|
59
|
+
const colorValue = colorValues?.[index];
|
|
60
|
+
const key = JSON.stringify([categoryValue, colorValue]);
|
|
61
|
+
const group = groups.get(key) ?? {
|
|
62
|
+
category: categoryValue,
|
|
63
|
+
...(colorValues === undefined ? {} : { color: colorValue }),
|
|
64
|
+
rows: []
|
|
65
|
+
};
|
|
66
|
+
group.rows.push(rows[index]);
|
|
67
|
+
groups.set(key, group);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (groups.size === 0) {
|
|
71
|
+
throw new Error(`Bar mark "${layer.id}" has no values to aggregate.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const values = [...groups.values()].flatMap(group => {
|
|
75
|
+
const value = aggregateRows(group.rows, measure.field, measure.aggregate);
|
|
76
|
+
const aggregateColor = colorAggregate === undefined
|
|
77
|
+
? group.color
|
|
78
|
+
: aggregateRows(group.rows, color.field, colorAggregate);
|
|
79
|
+
return value === undefined || (
|
|
80
|
+
colorAggregate !== undefined && aggregateColor === undefined
|
|
81
|
+
) ? [] : [{
|
|
82
|
+
x: channels.category === "x" ? group.category : value,
|
|
83
|
+
y: channels.category === "y" ? group.category : value,
|
|
84
|
+
...(color === undefined ? {} : { color: aggregateColor }),
|
|
85
|
+
count: group.rows.length
|
|
86
|
+
}];
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (values.length === 0) {
|
|
90
|
+
throw new Error(`Bar mark "${layer.id}" has no complete aggregate values.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return cloneAndFreeze({
|
|
94
|
+
xValues: values.map(value => value.x),
|
|
95
|
+
yValues: values.map(value => value.y),
|
|
96
|
+
values
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BAR_WIDTH = Object.freeze({ band: 0.72 });
|
|
4
|
+
export const DEFAULT_OFFSET_PADDING = Object.freeze({
|
|
5
|
+
paddingInner: 0,
|
|
6
|
+
paddingOuter: 0
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function normalizeBarWidth(options = {}, existing) {
|
|
10
|
+
const hasBand = options.band !== undefined;
|
|
11
|
+
const hasPixels = options.pixels !== undefined;
|
|
12
|
+
if (hasBand && hasPixels) {
|
|
13
|
+
throw new Error("encodeBarWidth band and pixels are mutually exclusive.");
|
|
14
|
+
}
|
|
15
|
+
if (!hasBand && !hasPixels) {
|
|
16
|
+
return cloneAndFreeze(existing ?? DEFAULT_BAR_WIDTH);
|
|
17
|
+
}
|
|
18
|
+
if (hasBand) {
|
|
19
|
+
if (!Number.isFinite(options.band) || options.band <= 0 || options.band > 1) {
|
|
20
|
+
throw new RangeError("Bar width band must be greater than 0 and at most 1.");
|
|
21
|
+
}
|
|
22
|
+
return cloneAndFreeze({ band: options.band });
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isFinite(options.pixels) || options.pixels <= 0) {
|
|
25
|
+
throw new RangeError("Bar width pixels must be a positive finite number.");
|
|
26
|
+
}
|
|
27
|
+
return cloneAndFreeze({ pixels: options.pixels });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeOffsetPadding(options = {}, existing) {
|
|
31
|
+
const paddingInner = options.paddingInner ??
|
|
32
|
+
existing?.paddingInner ?? DEFAULT_OFFSET_PADDING.paddingInner;
|
|
33
|
+
const paddingOuter = options.paddingOuter ??
|
|
34
|
+
existing?.paddingOuter ?? DEFAULT_OFFSET_PADDING.paddingOuter;
|
|
35
|
+
if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
|
|
36
|
+
throw new RangeError(
|
|
37
|
+
"xOffset paddingInner must be from 0 (inclusive) to 1 (exclusive)."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
|
|
41
|
+
throw new RangeError(
|
|
42
|
+
"xOffset paddingOuter must be a non-negative finite number."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return cloneAndFreeze({ paddingInner, paddingOuter });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveBarWidth(config, slotBandwidth) {
|
|
49
|
+
if (!Number.isFinite(slotBandwidth) || slotBandwidth <= 0) {
|
|
50
|
+
throw new Error("Bar width requires a positive resolved slot bandwidth.");
|
|
51
|
+
}
|
|
52
|
+
const normalized = normalizeBarWidth(config);
|
|
53
|
+
return normalized.pixels ?? slotBandwidth * normalized.band;
|
|
54
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isAggregate } from "../aggregate.js";
|
|
2
|
+
|
|
3
|
+
export const BAR_GRAINS = Object.freeze({
|
|
4
|
+
histogram: "histogram",
|
|
5
|
+
aggregate: "aggregate",
|
|
6
|
+
ranged: "ranged"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const BAR_ORIENTATIONS = Object.freeze({
|
|
10
|
+
vertical: "vertical",
|
|
11
|
+
horizontal: "horizontal"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function isCategory(encoding) {
|
|
15
|
+
return ["nominal", "ordinal", "temporal"].includes(encoding?.fieldType);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isMeasure(encoding) {
|
|
19
|
+
return encoding?.fieldType === "quantitative" &&
|
|
20
|
+
isAggregate(encoding.aggregate);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveBarOrientation(layer) {
|
|
24
|
+
if (layer?.mark?.type !== "bar") return undefined;
|
|
25
|
+
const x = layer.encoding?.x;
|
|
26
|
+
const y = layer.encoding?.y;
|
|
27
|
+
if (x?.bin !== undefined && y?.aggregate === "count") {
|
|
28
|
+
return BAR_ORIENTATIONS.vertical;
|
|
29
|
+
}
|
|
30
|
+
if (isCategory(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") {
|
|
31
|
+
return BAR_ORIENTATIONS.vertical;
|
|
32
|
+
}
|
|
33
|
+
if (isCategory(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative") {
|
|
34
|
+
return BAR_ORIENTATIONS.horizontal;
|
|
35
|
+
}
|
|
36
|
+
if (isCategory(x) && isMeasure(y)) return BAR_ORIENTATIONS.vertical;
|
|
37
|
+
if (isMeasure(x) && isCategory(y)) return BAR_ORIENTATIONS.horizontal;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveBarChannels(layer) {
|
|
42
|
+
const orientation = resolveBarOrientation(layer);
|
|
43
|
+
if (orientation === undefined) return undefined;
|
|
44
|
+
return orientation === BAR_ORIENTATIONS.vertical
|
|
45
|
+
? { orientation, category: "x", measure: "y" }
|
|
46
|
+
: { orientation, category: "y", measure: "x" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveBarGrain(layer) {
|
|
50
|
+
if (layer?.mark?.type !== "bar") return undefined;
|
|
51
|
+
const x = layer.encoding?.x;
|
|
52
|
+
const y = layer.encoding?.y;
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
x?.bin !== undefined &&
|
|
56
|
+
y?.aggregate === "count"
|
|
57
|
+
) {
|
|
58
|
+
return BAR_GRAINS.histogram;
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
(isCategory(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") ||
|
|
62
|
+
(isCategory(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative")
|
|
63
|
+
) return BAR_GRAINS.ranged;
|
|
64
|
+
if (resolveBarOrientation(layer) !== undefined) {
|
|
65
|
+
return BAR_GRAINS.aggregate;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function inferBarColorLayout(layer) {
|
|
71
|
+
if (layer?.encoding?.color?.layout !== undefined) {
|
|
72
|
+
return layer.encoding.color.layout;
|
|
73
|
+
}
|
|
74
|
+
const grain = resolveBarGrain(layer);
|
|
75
|
+
if (grain === BAR_GRAINS.histogram) return "stack";
|
|
76
|
+
if (grain === BAR_GRAINS.aggregate) return "group";
|
|
77
|
+
if (grain === BAR_GRAINS.ranged) return "overlay";
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveBarColorLayout(layer) {
|
|
82
|
+
if (layer?.encoding?.color?.layout !== undefined) {
|
|
83
|
+
return layer.encoding.color.layout;
|
|
84
|
+
}
|
|
85
|
+
const channels = resolveBarChannels(layer);
|
|
86
|
+
if (layer?.encoding?.[channels?.measure]?.stack === "normalize") return "fill";
|
|
87
|
+
if (layer?.encoding?.xOffset !== undefined) return "group";
|
|
88
|
+
if (layer?.encoding?.[channels?.measure]?.stack === null) return "overlay";
|
|
89
|
+
return "stack";
|
|
90
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const BOX_FIELDS = Object.freeze({
|
|
4
|
+
q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
|
|
5
|
+
lowerWhisker: "__boxPlot_lowerWhisker", upperWhisker: "__boxPlot_upperWhisker",
|
|
6
|
+
lowerFence: "__boxPlot_lowerFence", upperFence: "__boxPlot_upperFence",
|
|
7
|
+
count: "__boxPlot_count"
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
11
|
+
"type", "category", "field", "method", "whisker", "factor", "as"
|
|
12
|
+
]);
|
|
13
|
+
const OUTPUT_KEYS = Object.freeze(Object.keys(BOX_FIELDS));
|
|
14
|
+
|
|
15
|
+
function field(value, label) {
|
|
16
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`${label} must be a non-empty string.`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateOutputs(as, occupied) {
|
|
21
|
+
if (!isPlainObject(as)) throw new TypeError("Box as must be a plain object.");
|
|
22
|
+
const keys = Object.keys(as);
|
|
23
|
+
if (keys.length !== OUTPUT_KEYS.length || !OUTPUT_KEYS.every(key => Object.hasOwn(as, key))) {
|
|
24
|
+
throw new Error(`Box as requires exactly ${OUTPUT_KEYS.join(", ")}.`);
|
|
25
|
+
}
|
|
26
|
+
const values = OUTPUT_KEYS.map(key => field(as[key], `Box as.${key}`));
|
|
27
|
+
if (new Set(values).size !== values.length) throw new Error("Box output fields must be distinct.");
|
|
28
|
+
if (values.some(value => occupied.has(value))) {
|
|
29
|
+
throw new Error("Box output fields must not collide with input fields.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateBoxTransform(value) {
|
|
34
|
+
if (!isPlainObject(value)) throw new TypeError("Box transform must be a plain object.");
|
|
35
|
+
const unknown = Object.keys(value).find(key => !TRANSFORM_KEYS.includes(key));
|
|
36
|
+
if (unknown) throw new Error(`Unknown box transform property "${unknown}".`);
|
|
37
|
+
if (!["boxSummary", "boxOutlier"].includes(value.type)) {
|
|
38
|
+
throw new Error(`Unsupported box transform "${value.type}".`);
|
|
39
|
+
}
|
|
40
|
+
field(value.category, "Box category field");
|
|
41
|
+
field(value.field, "Box measure field");
|
|
42
|
+
if (value.method !== "linear") throw new Error(`Unsupported box quantile method "${value.method}".`);
|
|
43
|
+
const whisker = value.whisker ?? "tukey";
|
|
44
|
+
if (!["tukey", "minmax"].includes(whisker)) {
|
|
45
|
+
throw new Error(`Unsupported box whisker policy "${whisker}".`);
|
|
46
|
+
}
|
|
47
|
+
if (whisker === "tukey" && (!Number.isFinite(value.factor) || value.factor <= 0)) {
|
|
48
|
+
throw new RangeError("Box factor must be positive and finite.");
|
|
49
|
+
}
|
|
50
|
+
if (whisker === "minmax" && value.factor !== undefined) {
|
|
51
|
+
throw new Error("Box minmax whiskers do not accept factor.");
|
|
52
|
+
}
|
|
53
|
+
validateOutputs(value.as, new Set([value.category, value.field]));
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeBoxTransform(options = {}) {
|
|
58
|
+
const {
|
|
59
|
+
type = "boxSummary",
|
|
60
|
+
category,
|
|
61
|
+
field: measure,
|
|
62
|
+
whisker = "tukey",
|
|
63
|
+
factor = 1.5,
|
|
64
|
+
as = BOX_FIELDS
|
|
65
|
+
} = options;
|
|
66
|
+
if (whisker === "minmax" && Object.hasOwn(options, "factor")) {
|
|
67
|
+
throw new Error("Box minmax whiskers do not accept factor.");
|
|
68
|
+
}
|
|
69
|
+
const normalized = {
|
|
70
|
+
type,
|
|
71
|
+
category,
|
|
72
|
+
field: measure,
|
|
73
|
+
method: "linear",
|
|
74
|
+
...(whisker === "tukey" ? { factor } : { whisker }),
|
|
75
|
+
as
|
|
76
|
+
};
|
|
77
|
+
validateBoxTransform(normalized);
|
|
78
|
+
return cloneAndFreeze(normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function quantile(sorted, p) {
|
|
82
|
+
const position = (sorted.length - 1) * p;
|
|
83
|
+
const lower = Math.floor(position);
|
|
84
|
+
const upper = Math.ceil(position);
|
|
85
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stableNumber(value) {
|
|
89
|
+
return Number(value.toFixed(12));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deriveBoxData(rows, transform) {
|
|
93
|
+
if (!Array.isArray(rows)) throw new TypeError("Box rows must be an array.");
|
|
94
|
+
validateBoxTransform(transform);
|
|
95
|
+
const { category, field: measure, factor, as } = transform;
|
|
96
|
+
const whisker = transform.whisker ?? "tukey";
|
|
97
|
+
const groups = [];
|
|
98
|
+
const byCategory = new Map();
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
const key = row[category];
|
|
101
|
+
const value = row[measure];
|
|
102
|
+
if (key === undefined || key === null || key === "" || value === undefined || value === null) continue;
|
|
103
|
+
if (!Number.isFinite(value)) throw new TypeError(`Box field "${measure}" must contain finite numbers.`);
|
|
104
|
+
if (!byCategory.has(key)) {
|
|
105
|
+
const group = { key, rows: [] };
|
|
106
|
+
byCategory.set(key, group);
|
|
107
|
+
groups.push(group);
|
|
108
|
+
}
|
|
109
|
+
byCategory.get(key).rows.push(row);
|
|
110
|
+
}
|
|
111
|
+
if (groups.length === 0) throw new Error("Box transform requires at least one valid row.");
|
|
112
|
+
const summaries = [];
|
|
113
|
+
const outlierSet = new Set();
|
|
114
|
+
for (const group of groups) {
|
|
115
|
+
const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
|
|
116
|
+
const q1 = stableNumber(quantile(sorted, 0.25));
|
|
117
|
+
const median = stableNumber(quantile(sorted, 0.5));
|
|
118
|
+
const q3 = stableNumber(quantile(sorted, 0.75));
|
|
119
|
+
const lowerFence = whisker === "minmax"
|
|
120
|
+
? sorted[0]
|
|
121
|
+
: stableNumber(q1 - factor * (q3 - q1));
|
|
122
|
+
const upperFence = whisker === "minmax"
|
|
123
|
+
? sorted.at(-1)
|
|
124
|
+
: stableNumber(q3 + factor * (q3 - q1));
|
|
125
|
+
const inliers = sorted.filter(value => value >= lowerFence && value <= upperFence);
|
|
126
|
+
const lowerWhisker = inliers[0];
|
|
127
|
+
const upperWhisker = inliers.at(-1);
|
|
128
|
+
summaries.push({
|
|
129
|
+
[category]: group.key, [as.q1]: q1, [as.median]: median, [as.q3]: q3,
|
|
130
|
+
[as.lowerWhisker]: lowerWhisker, [as.upperWhisker]: upperWhisker,
|
|
131
|
+
[as.lowerFence]: lowerFence, [as.upperFence]: upperFence, [as.count]: sorted.length
|
|
132
|
+
});
|
|
133
|
+
if (whisker === "tukey") {
|
|
134
|
+
for (const row of group.rows) {
|
|
135
|
+
if (row[measure] < lowerWhisker || row[measure] > upperWhisker) outlierSet.add(row);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return cloneAndFreeze({ summaries, outliers: rows.filter(row => outlierSet.has(row)).map(row => ({ ...row })) });
|
|
140
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const COORDINATE_TYPES = new Set(["cartesian", "polar"]);
|
|
2
|
+
const POSITION_COORDINATES = Object.freeze({
|
|
3
|
+
x: Object.freeze({ id: "main", type: "cartesian" }),
|
|
4
|
+
y: Object.freeze({ id: "main", type: "cartesian" }),
|
|
5
|
+
theta: Object.freeze({ id: "polar", type: "polar" }),
|
|
6
|
+
radius: Object.freeze({ id: "polar", type: "polar" })
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function validateCoordinateType(value) {
|
|
10
|
+
if (!COORDINATE_TYPES.has(value)) {
|
|
11
|
+
throw new Error(`Unknown coordinate type "${value}".`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getPositionCoordinateDefaults(channel) {
|
|
18
|
+
const coordinate = POSITION_COORDINATES[channel];
|
|
19
|
+
|
|
20
|
+
if (coordinate === undefined) {
|
|
21
|
+
throw new Error(`Unknown positional channel "${channel}".`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return coordinate;
|
|
25
|
+
}
|