ggaction 0.0.4 → 0.0.6
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 +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- package/types/program.d.ts +703 -17
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
layoutSeriesPartition,
|
|
9
9
|
validateColorLayout
|
|
10
10
|
} from "./seriesLayout.js";
|
|
11
|
+
import { stableOrderPathValues } from "./pathOrder.js";
|
|
12
|
+
import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
|
|
11
13
|
|
|
12
14
|
export function deriveAreaSeries(rows, layer) {
|
|
13
15
|
if (layer?.mark?.type !== "area") {
|
|
14
16
|
throw new Error("Area series derivation requires a semantic area mark.");
|
|
15
17
|
}
|
|
16
|
-
const { x, y, x2, y2, group } = layer.encoding ?? {};
|
|
18
|
+
const { x, y, x2, y2, group, color } = layer.encoding ?? {};
|
|
17
19
|
const vertical =
|
|
18
20
|
["quantitative", "temporal"].includes(x?.fieldType) &&
|
|
19
21
|
y?.fieldType === "quantitative" &&
|
|
@@ -46,24 +48,44 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
46
48
|
const groupValues = group === undefined
|
|
47
49
|
? rows.map(() => undefined)
|
|
48
50
|
: readNominalField(rows, group.field);
|
|
51
|
+
const colorValues = color === undefined
|
|
52
|
+
? rows.map(() => undefined)
|
|
53
|
+
: readNominalField(rows, color.field);
|
|
54
|
+
const pathOrder = layer.encoding?.pathOrder;
|
|
55
|
+
const orderValues = pathOrder === undefined
|
|
56
|
+
? undefined
|
|
57
|
+
: readQuantitativeField(rows, pathOrder.field);
|
|
49
58
|
const groups = new Map();
|
|
50
59
|
|
|
51
60
|
for (let index = 0; index < rows.length; index += 1) {
|
|
52
61
|
const key = groupValues[index];
|
|
53
62
|
const series = groups.get(key) ?? {
|
|
54
|
-
key:
|
|
63
|
+
key: {
|
|
64
|
+
...(group === undefined ? {} : { [group.field]: key }),
|
|
65
|
+
...(color === undefined ? {} : { [color.field]: colorValues[index] })
|
|
66
|
+
},
|
|
55
67
|
values: []
|
|
56
68
|
};
|
|
69
|
+
if (
|
|
70
|
+
color !== undefined &&
|
|
71
|
+
!Object.is(series.key[color.field], colorValues[index])
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Area series "${String(key)}" must have one color value.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
57
77
|
series.values.push(vertical
|
|
58
78
|
? {
|
|
59
79
|
x: independentValues[index],
|
|
60
80
|
y: lower[index],
|
|
61
|
-
y2: upper[index]
|
|
81
|
+
y2: upper[index],
|
|
82
|
+
...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
|
|
62
83
|
}
|
|
63
84
|
: {
|
|
64
85
|
x: lower[index],
|
|
65
86
|
x2: upper[index],
|
|
66
|
-
y: independentValues[index]
|
|
87
|
+
y: independentValues[index],
|
|
88
|
+
...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
|
|
67
89
|
});
|
|
68
90
|
groups.set(key, series);
|
|
69
91
|
}
|
|
@@ -72,7 +94,13 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
72
94
|
}
|
|
73
95
|
const series = [...groups.values()].map(item => {
|
|
74
96
|
const key = vertical ? "x" : "y";
|
|
75
|
-
const values =
|
|
97
|
+
const values = pathOrder === undefined
|
|
98
|
+
? item.values.sort((left, right) => left[key] - right[key])
|
|
99
|
+
: stableOrderPathValues(
|
|
100
|
+
item.values.map(({ pathOrder: _pathOrder, ...value }) => value),
|
|
101
|
+
item.values.map(value => value.pathOrder),
|
|
102
|
+
pathOrder.order
|
|
103
|
+
);
|
|
76
104
|
if (values.length < 2) {
|
|
77
105
|
throw new Error(
|
|
78
106
|
`Area series on mark "${layer.id}" requires at least two points.`
|
|
@@ -96,9 +124,17 @@ export function deriveDensityAreaSeries(rows, layer, transform) {
|
|
|
96
124
|
if (layer?.mark?.type !== "area") {
|
|
97
125
|
throw new Error("Density area derivation requires a semantic area mark.");
|
|
98
126
|
}
|
|
127
|
+
if (layer.encoding?.pathOrder !== undefined) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Density area mark "${layer.id}" does not support path order.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
99
132
|
if (transform?.type !== "density" || !Array.isArray(transform.as)) {
|
|
100
133
|
throw new Error(`Area mark "${layer.id}" requires density provenance.`);
|
|
101
134
|
}
|
|
135
|
+
if (transform.placement?.type === "category") {
|
|
136
|
+
return deriveCategoricalDensitySeries(rows, layer, transform);
|
|
137
|
+
}
|
|
102
138
|
const { x, y, group } = layer.encoding ?? {};
|
|
103
139
|
if (
|
|
104
140
|
x?.fieldType !== "quantitative" ||
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneAndFreeze,
|
|
3
|
+
isPlainObject
|
|
4
|
+
} from "../core/immutable.js";
|
|
5
|
+
|
|
6
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
7
|
+
"type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
|
|
8
|
+
"resolved"
|
|
9
|
+
]);
|
|
10
|
+
const BIN_KEYS = Object.freeze(["x", "y"]);
|
|
11
|
+
const EXTENT_KEYS = Object.freeze(["x", "y"]);
|
|
12
|
+
const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
|
|
13
|
+
const RESOLVED_KEYS = Object.freeze([
|
|
14
|
+
"extent", "edges", "eligibleCount", "occupiedCount"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function requireField(value, label) {
|
|
18
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
25
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
26
|
+
if (unknown !== undefined) {
|
|
27
|
+
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requirePositiveInteger(value, label) {
|
|
32
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
33
|
+
throw new RangeError(`${label} must be a positive integer.`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeBins(value) {
|
|
39
|
+
if (value === undefined) return { x: 10, y: 10 };
|
|
40
|
+
if (Number.isInteger(value)) return { x: value, y: value };
|
|
41
|
+
if (!isPlainObject(value)) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
"2D bin bins must be a positive integer or an object with x and y."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
rejectUnknownKeys(value, BIN_KEYS, "2D bin bins");
|
|
47
|
+
return { x: value.x, y: value.y };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateBins(value) {
|
|
51
|
+
if (!isPlainObject(value)) {
|
|
52
|
+
throw new TypeError("2D bin bins must be an object.");
|
|
53
|
+
}
|
|
54
|
+
rejectUnknownKeys(value, BIN_KEYS, "2D bin bins");
|
|
55
|
+
requirePositiveInteger(value.x, "2D bin x bins");
|
|
56
|
+
requirePositiveInteger(value.y, "2D bin y bins");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeRequestedExtent(value) {
|
|
60
|
+
if (value === undefined) return { x: "auto", y: "auto" };
|
|
61
|
+
if (!isPlainObject(value)) {
|
|
62
|
+
throw new TypeError("2D bin extent must be an object.");
|
|
63
|
+
}
|
|
64
|
+
rejectUnknownKeys(value, EXTENT_KEYS, "2D bin extent");
|
|
65
|
+
return {
|
|
66
|
+
x: value.x ?? "auto",
|
|
67
|
+
y: value.y ?? "auto"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateExtentValue(value, label, { allowAuto = true } = {}) {
|
|
72
|
+
if (allowAuto && value === "auto") return;
|
|
73
|
+
if (
|
|
74
|
+
!Array.isArray(value) ||
|
|
75
|
+
value.length !== 2 ||
|
|
76
|
+
!value.every(Number.isFinite) ||
|
|
77
|
+
value[0] >= value[1]
|
|
78
|
+
) {
|
|
79
|
+
throw new RangeError(`${label} must be two increasing finite numbers.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateRequestedExtent(value) {
|
|
84
|
+
if (!isPlainObject(value)) {
|
|
85
|
+
throw new TypeError("2D bin extent must be an object.");
|
|
86
|
+
}
|
|
87
|
+
rejectUnknownKeys(value, EXTENT_KEYS, "2D bin extent");
|
|
88
|
+
validateExtentValue(value.x, "2D bin x extent");
|
|
89
|
+
validateExtentValue(value.y, "2D bin y extent");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeOutputFields(id, value, members) {
|
|
93
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
94
|
+
throw new TypeError("2D bin as must be an object.");
|
|
95
|
+
}
|
|
96
|
+
if (value !== undefined) rejectUnknownKeys(value, FIELD_KEYS, "2D bin as");
|
|
97
|
+
if (!members && value?.members !== undefined) {
|
|
98
|
+
throw new Error("2D bin as.members requires members: true.");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
x0: value?.x0 ?? `__${id}_x0`,
|
|
102
|
+
x1: value?.x1 ?? `__${id}_x1`,
|
|
103
|
+
y0: value?.y0 ?? `__${id}_y0`,
|
|
104
|
+
y1: value?.y1 ?? `__${id}_y1`,
|
|
105
|
+
count: value?.count ?? `__${id}_count`,
|
|
106
|
+
...(members ? { members: value?.members ?? `__${id}_members` } : {})
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateOutputFields(value, members) {
|
|
111
|
+
if (!isPlainObject(value)) {
|
|
112
|
+
throw new TypeError("2D bin as must be an object.");
|
|
113
|
+
}
|
|
114
|
+
rejectUnknownKeys(value, FIELD_KEYS, "2D bin as");
|
|
115
|
+
const required = ["x0", "x1", "y0", "y1", "count"];
|
|
116
|
+
if (members) required.push("members");
|
|
117
|
+
if (!members && value.members !== undefined) {
|
|
118
|
+
throw new Error("2D bin as.members requires members: true.");
|
|
119
|
+
}
|
|
120
|
+
const fields = required.map(role => requireField(
|
|
121
|
+
value[role],
|
|
122
|
+
`2D bin ${role} output field`
|
|
123
|
+
));
|
|
124
|
+
if (new Set(fields).size !== fields.length) {
|
|
125
|
+
throw new Error("2D bin output fields must be unique.");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateEdges(value, extent, count, axis) {
|
|
130
|
+
if (
|
|
131
|
+
!Array.isArray(value) ||
|
|
132
|
+
value.length !== count + 1 ||
|
|
133
|
+
!value.every(Number.isFinite)
|
|
134
|
+
) {
|
|
135
|
+
throw new TypeError(`Resolved 2D bin ${axis} edges are invalid.`);
|
|
136
|
+
}
|
|
137
|
+
if (value[0] !== extent[0] || value.at(-1) !== extent[1]) {
|
|
138
|
+
throw new Error(`Resolved 2D bin ${axis} edges must match its extent.`);
|
|
139
|
+
}
|
|
140
|
+
for (let index = 1; index < value.length; index += 1) {
|
|
141
|
+
if (value[index] <= value[index - 1]) {
|
|
142
|
+
throw new Error(`Resolved 2D bin ${axis} edges must be increasing.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateResolved(value, bins) {
|
|
148
|
+
if (!isPlainObject(value)) {
|
|
149
|
+
throw new TypeError("Resolved 2D bin state must be a plain object.");
|
|
150
|
+
}
|
|
151
|
+
rejectUnknownKeys(value, RESOLVED_KEYS, "resolved 2D bin");
|
|
152
|
+
if (!isPlainObject(value.extent) || !isPlainObject(value.edges)) {
|
|
153
|
+
throw new TypeError("Resolved 2D bin extent and edges must be objects.");
|
|
154
|
+
}
|
|
155
|
+
rejectUnknownKeys(value.extent, EXTENT_KEYS, "resolved 2D bin extent");
|
|
156
|
+
rejectUnknownKeys(value.edges, EXTENT_KEYS, "resolved 2D bin edges");
|
|
157
|
+
for (const axis of ["x", "y"]) {
|
|
158
|
+
validateExtentValue(
|
|
159
|
+
value.extent[axis],
|
|
160
|
+
`Resolved 2D bin ${axis} extent`,
|
|
161
|
+
{ allowAuto: false }
|
|
162
|
+
);
|
|
163
|
+
validateEdges(value.edges[axis], value.extent[axis], bins[axis], axis);
|
|
164
|
+
}
|
|
165
|
+
for (const property of ["eligibleCount", "occupiedCount"]) {
|
|
166
|
+
if (!Number.isInteger(value[property]) || value[property] < 0) {
|
|
167
|
+
throw new RangeError(`Resolved 2D bin ${property} must be a non-negative integer.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (value.occupiedCount > bins.x * bins.y) {
|
|
171
|
+
throw new RangeError("Resolved 2D bin occupiedCount exceeds the grid size.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateBin2DTransform(transform) {
|
|
176
|
+
if (!isPlainObject(transform)) {
|
|
177
|
+
throw new TypeError("2D bin transform must be a plain object.");
|
|
178
|
+
}
|
|
179
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "2D bin transform");
|
|
180
|
+
if (transform.type !== "bin2d") {
|
|
181
|
+
throw new Error(`Unsupported 2D bin transform "${transform.type}".`);
|
|
182
|
+
}
|
|
183
|
+
requireField(transform.x, "2D bin x field");
|
|
184
|
+
requireField(transform.y, "2D bin y field");
|
|
185
|
+
validateBins(transform.bins);
|
|
186
|
+
validateRequestedExtent(transform.extent);
|
|
187
|
+
if (typeof transform.includeEmpty !== "boolean") {
|
|
188
|
+
throw new TypeError("2D bin includeEmpty must be a boolean.");
|
|
189
|
+
}
|
|
190
|
+
if (typeof transform.members !== "boolean") {
|
|
191
|
+
throw new TypeError("2D bin members must be a boolean.");
|
|
192
|
+
}
|
|
193
|
+
validateOutputFields(transform.as, transform.members);
|
|
194
|
+
if (transform.resolved !== undefined) {
|
|
195
|
+
validateResolved(transform.resolved, transform.bins);
|
|
196
|
+
}
|
|
197
|
+
return transform;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function normalizeBin2DTransform({
|
|
201
|
+
id,
|
|
202
|
+
x,
|
|
203
|
+
y,
|
|
204
|
+
bins,
|
|
205
|
+
extent,
|
|
206
|
+
includeEmpty = false,
|
|
207
|
+
members = false,
|
|
208
|
+
as
|
|
209
|
+
} = {}) {
|
|
210
|
+
const normalizedBins = normalizeBins(bins);
|
|
211
|
+
const transform = {
|
|
212
|
+
type: "bin2d",
|
|
213
|
+
x,
|
|
214
|
+
y,
|
|
215
|
+
bins: normalizedBins,
|
|
216
|
+
extent: normalizeRequestedExtent(extent),
|
|
217
|
+
includeEmpty,
|
|
218
|
+
members,
|
|
219
|
+
as: normalizeOutputFields(id, as, members)
|
|
220
|
+
};
|
|
221
|
+
validateBin2DTransform(transform);
|
|
222
|
+
return cloneAndFreeze(transform);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveExtent(values, requested, axis) {
|
|
226
|
+
const minimum = Math.min(...values);
|
|
227
|
+
const maximum = Math.max(...values);
|
|
228
|
+
const extent = requested === "auto" ? [minimum, maximum] : requested;
|
|
229
|
+
if (extent[0] > minimum || extent[1] < maximum) {
|
|
230
|
+
throw new RangeError(
|
|
231
|
+
`2D bin ${axis} extent must contain every eligible value.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (extent[0] === extent[1]) {
|
|
235
|
+
throw new RangeError(`2D bin ${axis} extent must have positive span.`);
|
|
236
|
+
}
|
|
237
|
+
return [...extent];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function createEdges(extent, count) {
|
|
241
|
+
const [minimum, maximum] = extent;
|
|
242
|
+
const step = (maximum - minimum) / count;
|
|
243
|
+
return Array.from({ length: count + 1 }, (_, index) =>
|
|
244
|
+
index === count ? maximum : minimum + step * index
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function findBinIndex(value, edges) {
|
|
249
|
+
if (value === edges.at(-1)) return edges.length - 2;
|
|
250
|
+
let low = 0;
|
|
251
|
+
let high = edges.length - 1;
|
|
252
|
+
while (low + 1 < high) {
|
|
253
|
+
const middle = Math.floor((low + high) / 2);
|
|
254
|
+
if (value < edges[middle]) high = middle;
|
|
255
|
+
else low = middle;
|
|
256
|
+
}
|
|
257
|
+
return low;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function deriveBin2DRows(rows, transform) {
|
|
261
|
+
if (!Array.isArray(rows)) {
|
|
262
|
+
throw new TypeError("2D bin source rows must be an array.");
|
|
263
|
+
}
|
|
264
|
+
validateBin2DTransform(transform);
|
|
265
|
+
const eligible = rows.flatMap((row, index) =>
|
|
266
|
+
isPlainObject(row) &&
|
|
267
|
+
Number.isFinite(row[transform.x]) &&
|
|
268
|
+
Number.isFinite(row[transform.y])
|
|
269
|
+
? [{ index, row }]
|
|
270
|
+
: []
|
|
271
|
+
);
|
|
272
|
+
if (eligible.length === 0) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"2D bin requires at least one row with finite x and y values."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const extent = {
|
|
278
|
+
x: resolveExtent(
|
|
279
|
+
eligible.map(item => item.row[transform.x]),
|
|
280
|
+
transform.extent.x,
|
|
281
|
+
"x"
|
|
282
|
+
),
|
|
283
|
+
y: resolveExtent(
|
|
284
|
+
eligible.map(item => item.row[transform.y]),
|
|
285
|
+
transform.extent.y,
|
|
286
|
+
"y"
|
|
287
|
+
)
|
|
288
|
+
};
|
|
289
|
+
const edges = {
|
|
290
|
+
x: createEdges(extent.x, transform.bins.x),
|
|
291
|
+
y: createEdges(extent.y, transform.bins.y)
|
|
292
|
+
};
|
|
293
|
+
const buckets = Array.from(
|
|
294
|
+
{ length: transform.bins.x * transform.bins.y },
|
|
295
|
+
() => []
|
|
296
|
+
);
|
|
297
|
+
for (const item of eligible) {
|
|
298
|
+
const xIndex = findBinIndex(item.row[transform.x], edges.x);
|
|
299
|
+
const yIndex = findBinIndex(item.row[transform.y], edges.y);
|
|
300
|
+
buckets[yIndex * transform.bins.x + xIndex].push(item.index);
|
|
301
|
+
}
|
|
302
|
+
const values = [];
|
|
303
|
+
for (let yIndex = 0; yIndex < transform.bins.y; yIndex += 1) {
|
|
304
|
+
for (let xIndex = 0; xIndex < transform.bins.x; xIndex += 1) {
|
|
305
|
+
const memberIndexes = buckets[yIndex * transform.bins.x + xIndex];
|
|
306
|
+
if (!transform.includeEmpty && memberIndexes.length === 0) continue;
|
|
307
|
+
values.push({
|
|
308
|
+
[transform.as.x0]: edges.x[xIndex],
|
|
309
|
+
[transform.as.x1]: edges.x[xIndex + 1],
|
|
310
|
+
[transform.as.y0]: edges.y[yIndex],
|
|
311
|
+
[transform.as.y1]: edges.y[yIndex + 1],
|
|
312
|
+
[transform.as.count]: memberIndexes.length,
|
|
313
|
+
...(transform.members
|
|
314
|
+
? { [transform.as.members]: [...memberIndexes] }
|
|
315
|
+
: {})
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return cloneAndFreeze({
|
|
320
|
+
values,
|
|
321
|
+
resolved: {
|
|
322
|
+
extent,
|
|
323
|
+
edges,
|
|
324
|
+
eligibleCount: eligible.length,
|
|
325
|
+
occupiedCount: buckets.filter(bucket => bucket.length > 0).length
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function requestedBin2DTransform(transform) {
|
|
331
|
+
const { resolved: _resolved, ...requested } = transform;
|
|
332
|
+
return cloneAndFreeze(requested);
|
|
333
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
mapContinuousScaleValues,
|
|
4
|
+
mapOrdinalPositionValues,
|
|
5
|
+
readNominalField,
|
|
6
|
+
readQuantitativeField
|
|
7
|
+
} from "./scales/index.js";
|
|
8
|
+
import { buildAreaCurvePathCommands } from "./curveCommands.js";
|
|
9
|
+
import { buildLinearPathCommands } from "./pathCommands.js";
|
|
10
|
+
|
|
11
|
+
function sameKey(left, right) {
|
|
12
|
+
const leftEntries = Object.entries(left);
|
|
13
|
+
const rightEntries = Object.entries(right);
|
|
14
|
+
return leftEntries.length === rightEntries.length && leftEntries.every(
|
|
15
|
+
([field, value]) => Object.hasOwn(right, field) && Object.is(right[field], value)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function seriesSide(placement, splitValue) {
|
|
20
|
+
if (placement.split === undefined) return placement.side;
|
|
21
|
+
const domain = placement.split.domain ?? placement.resolvedSplitDomain;
|
|
22
|
+
const index = domain.findIndex(value => Object.is(value, splitValue));
|
|
23
|
+
if (index < 0) {
|
|
24
|
+
throw new Error(`Density split value "${splitValue}" is outside its domain.`);
|
|
25
|
+
}
|
|
26
|
+
if (placement.channel === "x") return index === 0 ? "left" : "right";
|
|
27
|
+
return index === 0 ? "top" : "bottom";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deriveCategoricalDensitySeries(rows, layer, transform) {
|
|
31
|
+
const placement = transform?.placement;
|
|
32
|
+
if (layer?.mark?.type !== "area" || placement?.type !== "category") {
|
|
33
|
+
throw new Error("Categorical density series require a category-placed area.");
|
|
34
|
+
}
|
|
35
|
+
const categoryEncoding = layer.encoding?.[placement.channel];
|
|
36
|
+
const valueChannel = placement.channel === "x" ? "y" : "x";
|
|
37
|
+
const valueEncoding = layer.encoding?.[valueChannel];
|
|
38
|
+
if (
|
|
39
|
+
categoryEncoding?.field !== placement.categoryField ||
|
|
40
|
+
!["nominal", "ordinal"].includes(categoryEncoding.fieldType) ||
|
|
41
|
+
valueEncoding?.field !== transform.as?.[0] ||
|
|
42
|
+
valueEncoding.fieldType !== "quantitative"
|
|
43
|
+
) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Categorical density area mark "${layer.id}" requires its category and value encodings.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const categories = readNominalField(rows, placement.categoryField);
|
|
49
|
+
const values = readQuantitativeField(rows, transform.as[0]);
|
|
50
|
+
const densities = readQuantitativeField(rows, transform.as[1]);
|
|
51
|
+
const splits = placement.split === undefined
|
|
52
|
+
? rows.map(() => undefined)
|
|
53
|
+
: readNominalField(rows, placement.split.field);
|
|
54
|
+
const series = [];
|
|
55
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
56
|
+
const key = {
|
|
57
|
+
...(transform.groupBy === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { [transform.groupBy]: categories[index] }),
|
|
60
|
+
...(placement.split === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { [placement.split.field]: splits[index] })
|
|
63
|
+
};
|
|
64
|
+
let current = series.find(item => sameKey(item.key, key));
|
|
65
|
+
if (current === undefined) {
|
|
66
|
+
current = {
|
|
67
|
+
category: categories[index],
|
|
68
|
+
split: splits[index],
|
|
69
|
+
key,
|
|
70
|
+
values: []
|
|
71
|
+
};
|
|
72
|
+
series.push(current);
|
|
73
|
+
}
|
|
74
|
+
current.values.push({
|
|
75
|
+
value: values[index],
|
|
76
|
+
density: densities[index]
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (series.length === 0) {
|
|
80
|
+
throw new Error(`Categorical density area mark "${layer.id}" has no values.`);
|
|
81
|
+
}
|
|
82
|
+
const resolvedSplitDomain = placement.split === undefined
|
|
83
|
+
? undefined
|
|
84
|
+
: transform.resolved?.splitDomain ?? placement.split.domain;
|
|
85
|
+
if (placement.split !== undefined && resolvedSplitDomain === undefined) {
|
|
86
|
+
throw new Error("Categorical density split requires a resolved two-value domain.");
|
|
87
|
+
}
|
|
88
|
+
return cloneAndFreeze({
|
|
89
|
+
mode: placement.channel === "x" ? "category-x" : "category-y",
|
|
90
|
+
placement: {
|
|
91
|
+
...placement,
|
|
92
|
+
...(resolvedSplitDomain === undefined ? {} : { resolvedSplitDomain })
|
|
93
|
+
},
|
|
94
|
+
series: series.map(item => ({
|
|
95
|
+
...item,
|
|
96
|
+
side: seriesSide({
|
|
97
|
+
...placement,
|
|
98
|
+
...(resolvedSplitDomain === undefined ? {} : { resolvedSplitDomain })
|
|
99
|
+
}, item.split),
|
|
100
|
+
values: item.values.sort((left, right) => left.value - right.value)
|
|
101
|
+
}))
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function maximumFor(derived, series) {
|
|
106
|
+
const candidates = derived.placement.width.resolve === "shared"
|
|
107
|
+
? derived.series
|
|
108
|
+
: derived.series.filter(candidate => Object.is(
|
|
109
|
+
candidate.category,
|
|
110
|
+
series.category
|
|
111
|
+
));
|
|
112
|
+
const maximum = Math.max(...candidates.flatMap(candidate =>
|
|
113
|
+
candidate.values.map(value => value.density)
|
|
114
|
+
));
|
|
115
|
+
if (!Number.isFinite(maximum) || maximum <= 0) {
|
|
116
|
+
throw new Error("Categorical density width requires a positive maximum.");
|
|
117
|
+
}
|
|
118
|
+
return maximum;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pathBoundaries(series, center, halfWidth, maximum, valueScale, channel) {
|
|
122
|
+
const valuePositions = mapContinuousScaleValues(
|
|
123
|
+
series.values.map(value => value.value),
|
|
124
|
+
valueScale
|
|
125
|
+
);
|
|
126
|
+
const widths = series.values.map(value => halfWidth * value.density / maximum);
|
|
127
|
+
const horizontal = channel === "x";
|
|
128
|
+
const direction = {
|
|
129
|
+
left: -1,
|
|
130
|
+
right: 1,
|
|
131
|
+
top: -1,
|
|
132
|
+
bottom: 1
|
|
133
|
+
}[series.side];
|
|
134
|
+
const point = (valuePosition, width) => horizontal
|
|
135
|
+
? { x: center + width, y: valuePosition }
|
|
136
|
+
: { x: valuePosition, y: center + width };
|
|
137
|
+
if (series.side === "both") {
|
|
138
|
+
return {
|
|
139
|
+
lower: valuePositions.map((value, index) => point(value, -widths[index])),
|
|
140
|
+
upper: valuePositions.map((value, index) => point(value, widths[index]))
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const baseline = valuePositions.map(value => point(value, 0));
|
|
144
|
+
const outer = valuePositions.map((value, index) =>
|
|
145
|
+
point(value, direction * widths[index])
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
lower: baseline,
|
|
149
|
+
upper: outer,
|
|
150
|
+
outline: [baseline[0], ...outer, baseline.at(-1)]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildCategoricalDensityPaths(
|
|
155
|
+
derived,
|
|
156
|
+
{ categoryScale, valueScale, curve = "linear" } = {}
|
|
157
|
+
) {
|
|
158
|
+
if (!Number.isFinite(categoryScale?.bandwidth) || categoryScale.bandwidth <= 0) {
|
|
159
|
+
throw new Error("Categorical density placement requires a positive band scale.");
|
|
160
|
+
}
|
|
161
|
+
const halfWidth = categoryScale.bandwidth * derived.placement.width.band / 2;
|
|
162
|
+
const centers = mapOrdinalPositionValues(
|
|
163
|
+
derived.series.map(series => series.category),
|
|
164
|
+
categoryScale
|
|
165
|
+
);
|
|
166
|
+
const commands = derived.series.map((series, index) => {
|
|
167
|
+
const boundaries = pathBoundaries(
|
|
168
|
+
series,
|
|
169
|
+
centers[index],
|
|
170
|
+
halfWidth,
|
|
171
|
+
maximumFor(derived, series),
|
|
172
|
+
valueScale,
|
|
173
|
+
derived.placement.channel
|
|
174
|
+
);
|
|
175
|
+
if (curve !== "linear") {
|
|
176
|
+
return buildAreaCurvePathCommands(
|
|
177
|
+
boundaries.lower,
|
|
178
|
+
boundaries.upper,
|
|
179
|
+
curve,
|
|
180
|
+
derived.placement.channel === "x" ? { independentAxis: "y" } : {}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return buildLinearPathCommands(
|
|
184
|
+
boundaries.outline ?? [
|
|
185
|
+
...boundaries.lower,
|
|
186
|
+
...[...boundaries.upper].reverse()
|
|
187
|
+
],
|
|
188
|
+
{ close: true }
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
return cloneAndFreeze(commands);
|
|
192
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getPositionChannelDefinition } from "../core/vocabulary.js";
|
|
2
2
|
|
|
3
|
-
const COORDINATE_TYPES = new Set(["cartesian", "polar"]);
|
|
3
|
+
const COORDINATE_TYPES = new Set(["cartesian", "polar", "parallel"]);
|
|
4
4
|
|
|
5
5
|
export function validateCoordinateType(value) {
|
|
6
6
|
if (!COORDINATE_TYPES.has(value)) {
|
|
@@ -267,7 +267,9 @@ export function buildCurvePathCommands(points, curve = "linear") {
|
|
|
267
267
|
const validatedPoints = validatePoints(points);
|
|
268
268
|
const validatedCurve = validateCurveInterpolation(curve);
|
|
269
269
|
if (validatedCurve === "linear" ||
|
|
270
|
-
(SMOOTH_CURVES.has(validatedCurve) &&
|
|
270
|
+
(SMOOTH_CURVES.has(validatedCurve) &&
|
|
271
|
+
validatedCurve !== "monotone" &&
|
|
272
|
+
points.length < 3)) {
|
|
271
273
|
return buildLinearPathCommands(validatedPoints);
|
|
272
274
|
}
|
|
273
275
|
if (validatedCurve.startsWith("step")) {
|