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,356 @@
|
|
|
1
|
+
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { buildLinearPathCommands } from "./pathCommands.js";
|
|
3
|
+
|
|
4
|
+
export const CURVE_INTERPOLATIONS = Object.freeze([
|
|
5
|
+
"linear",
|
|
6
|
+
"step",
|
|
7
|
+
"step-before",
|
|
8
|
+
"step-after",
|
|
9
|
+
"basis",
|
|
10
|
+
"cardinal",
|
|
11
|
+
"monotone",
|
|
12
|
+
"natural"
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const SMOOTH_CURVES = new Set([
|
|
16
|
+
"basis",
|
|
17
|
+
"cardinal",
|
|
18
|
+
"monotone",
|
|
19
|
+
"natural"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function ownCommands(commands) {
|
|
23
|
+
return freezeOwned(commands.map(command => freezeOwned(command)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validatePoints(points) {
|
|
27
|
+
if (
|
|
28
|
+
!Array.isArray(points) ||
|
|
29
|
+
points.length < 2 ||
|
|
30
|
+
!points.every(point =>
|
|
31
|
+
isPlainObject(point) &&
|
|
32
|
+
Object.keys(point).length === 2 &&
|
|
33
|
+
Number.isFinite(point.x) &&
|
|
34
|
+
Number.isFinite(point.y)
|
|
35
|
+
)
|
|
36
|
+
) {
|
|
37
|
+
throw new TypeError(
|
|
38
|
+
"Curve points must contain at least two finite { x, y } objects."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return points;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateCurveInterpolation(curve) {
|
|
45
|
+
if (!CURVE_INTERPOLATIONS.includes(curve)) {
|
|
46
|
+
throw new Error(`Unsupported curve interpolation "${curve}".`);
|
|
47
|
+
}
|
|
48
|
+
return curve;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildStepCommands(points, mode) {
|
|
52
|
+
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
53
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
54
|
+
const previous = points[index - 1];
|
|
55
|
+
const current = points[index];
|
|
56
|
+
if (mode === "step") {
|
|
57
|
+
const midpoint = (previous.x + current.x) / 2;
|
|
58
|
+
commands.push(
|
|
59
|
+
{ op: "L", x: midpoint, y: previous.y },
|
|
60
|
+
{ op: "L", x: midpoint, y: current.y },
|
|
61
|
+
{ op: "L", x: current.x, y: current.y }
|
|
62
|
+
);
|
|
63
|
+
} else if (mode === "step-before") {
|
|
64
|
+
commands.push(
|
|
65
|
+
{ op: "L", x: previous.x, y: current.y },
|
|
66
|
+
{ op: "L", x: current.x, y: current.y }
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
commands.push(
|
|
70
|
+
{ op: "L", x: current.x, y: previous.y },
|
|
71
|
+
{ op: "L", x: current.x, y: current.y }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return ownCommands(commands);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cubicLine(from, to) {
|
|
79
|
+
return {
|
|
80
|
+
op: "C",
|
|
81
|
+
x1: from.x + (to.x - from.x) / 3,
|
|
82
|
+
y1: from.y + (to.y - from.y) / 3,
|
|
83
|
+
x2: from.x + (to.x - from.x) * 2 / 3,
|
|
84
|
+
y2: from.y + (to.y - from.y) * 2 / 3,
|
|
85
|
+
x: to.x,
|
|
86
|
+
y: to.y
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildBasisCommands(points) {
|
|
91
|
+
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
92
|
+
let previous = points[0];
|
|
93
|
+
let current = points[1];
|
|
94
|
+
let endpoint = {
|
|
95
|
+
x: (5 * previous.x + current.x) / 6,
|
|
96
|
+
y: (5 * previous.y + current.y) / 6
|
|
97
|
+
};
|
|
98
|
+
commands.push(cubicLine(previous, endpoint));
|
|
99
|
+
|
|
100
|
+
for (let index = 2; index < points.length; index += 1) {
|
|
101
|
+
const next = points[index];
|
|
102
|
+
endpoint = {
|
|
103
|
+
x: (previous.x + 4 * current.x + next.x) / 6,
|
|
104
|
+
y: (previous.y + 4 * current.y + next.y) / 6
|
|
105
|
+
};
|
|
106
|
+
commands.push({
|
|
107
|
+
op: "C",
|
|
108
|
+
x1: (2 * previous.x + current.x) / 3,
|
|
109
|
+
y1: (2 * previous.y + current.y) / 3,
|
|
110
|
+
x2: (previous.x + 2 * current.x) / 3,
|
|
111
|
+
y2: (previous.y + 2 * current.y) / 3,
|
|
112
|
+
x: endpoint.x,
|
|
113
|
+
y: endpoint.y
|
|
114
|
+
});
|
|
115
|
+
previous = current;
|
|
116
|
+
current = next;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
endpoint = {
|
|
120
|
+
x: (previous.x + 5 * current.x) / 6,
|
|
121
|
+
y: (previous.y + 5 * current.y) / 6
|
|
122
|
+
};
|
|
123
|
+
commands.push({
|
|
124
|
+
op: "C",
|
|
125
|
+
x1: (2 * previous.x + current.x) / 3,
|
|
126
|
+
y1: (2 * previous.y + current.y) / 3,
|
|
127
|
+
x2: (previous.x + 2 * current.x) / 3,
|
|
128
|
+
y2: (previous.y + 2 * current.y) / 3,
|
|
129
|
+
x: endpoint.x,
|
|
130
|
+
y: endpoint.y
|
|
131
|
+
});
|
|
132
|
+
commands.push(cubicLine(endpoint, current));
|
|
133
|
+
return ownCommands(commands);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildCardinalCommands(points) {
|
|
137
|
+
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
138
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
139
|
+
const before = points[index - 1] ?? points[index];
|
|
140
|
+
const current = points[index];
|
|
141
|
+
const next = points[index + 1];
|
|
142
|
+
const after = points[index + 2] ?? next;
|
|
143
|
+
commands.push({
|
|
144
|
+
op: "C",
|
|
145
|
+
x1: current.x + (next.x - before.x) / 6,
|
|
146
|
+
y1: current.y + (next.y - before.y) / 6,
|
|
147
|
+
x2: next.x - (after.x - current.x) / 6,
|
|
148
|
+
y2: next.y - (after.y - current.y) / 6,
|
|
149
|
+
x: next.x,
|
|
150
|
+
y: next.y
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return ownCommands(commands);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function monotoneTangents(points) {
|
|
157
|
+
const intervals = points.slice(1).map((point, index) => {
|
|
158
|
+
const previous = points[index];
|
|
159
|
+
const width = point.x - previous.x;
|
|
160
|
+
if (!(width > 0)) {
|
|
161
|
+
throw new Error("Monotone curve requires strictly increasing x values.");
|
|
162
|
+
}
|
|
163
|
+
return { width, slope: (point.y - previous.y) / width };
|
|
164
|
+
});
|
|
165
|
+
const tangents = [intervals[0].slope];
|
|
166
|
+
|
|
167
|
+
for (let index = 1; index < points.length - 1; index += 1) {
|
|
168
|
+
const previous = intervals[index - 1];
|
|
169
|
+
const next = intervals[index];
|
|
170
|
+
if (
|
|
171
|
+
previous.slope === 0 ||
|
|
172
|
+
next.slope === 0 ||
|
|
173
|
+
Math.sign(previous.slope) !== Math.sign(next.slope)
|
|
174
|
+
) {
|
|
175
|
+
tangents.push(0);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const previousWeight = 2 * next.width + previous.width;
|
|
179
|
+
const nextWeight = next.width + 2 * previous.width;
|
|
180
|
+
tangents.push(
|
|
181
|
+
(previousWeight + nextWeight) /
|
|
182
|
+
(previousWeight / previous.slope + nextWeight / next.slope)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
tangents.push(intervals.at(-1).slope);
|
|
186
|
+
return tangents;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildMonotoneCommands(points) {
|
|
190
|
+
const tangents = monotoneTangents(points);
|
|
191
|
+
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
192
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
193
|
+
const previous = points[index - 1];
|
|
194
|
+
const current = points[index];
|
|
195
|
+
const third = (current.x - previous.x) / 3;
|
|
196
|
+
commands.push({
|
|
197
|
+
op: "C",
|
|
198
|
+
x1: previous.x + third,
|
|
199
|
+
y1: previous.y + tangents[index - 1] * third,
|
|
200
|
+
x2: current.x - third,
|
|
201
|
+
y2: current.y - tangents[index] * third,
|
|
202
|
+
x: current.x,
|
|
203
|
+
y: current.y
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return ownCommands(commands);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function solveFirstControlPoints(values) {
|
|
210
|
+
const segmentCount = values.length - 1;
|
|
211
|
+
const diagonal = Array(segmentCount).fill(4);
|
|
212
|
+
const right = Array(segmentCount);
|
|
213
|
+
diagonal[0] = 2;
|
|
214
|
+
diagonal[segmentCount - 1] = 7;
|
|
215
|
+
right[0] = values[0] + 2 * values[1];
|
|
216
|
+
for (let index = 1; index < segmentCount - 1; index += 1) {
|
|
217
|
+
right[index] = 4 * values[index] + 2 * values[index + 1];
|
|
218
|
+
}
|
|
219
|
+
right[segmentCount - 1] =
|
|
220
|
+
8 * values[segmentCount - 1] + values[segmentCount];
|
|
221
|
+
|
|
222
|
+
for (let index = 1; index < segmentCount; index += 1) {
|
|
223
|
+
const lower = index === segmentCount - 1 ? 2 : 1;
|
|
224
|
+
const factor = lower / diagonal[index - 1];
|
|
225
|
+
diagonal[index] -= factor;
|
|
226
|
+
right[index] -= factor * right[index - 1];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const controls = Array(segmentCount);
|
|
230
|
+
controls[segmentCount - 1] =
|
|
231
|
+
right[segmentCount - 1] / diagonal[segmentCount - 1];
|
|
232
|
+
for (let index = segmentCount - 2; index >= 0; index -= 1) {
|
|
233
|
+
controls[index] = (right[index] - controls[index + 1]) / diagonal[index];
|
|
234
|
+
}
|
|
235
|
+
return controls;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildNaturalCommands(points) {
|
|
239
|
+
const x = points.map(point => point.x);
|
|
240
|
+
const y = points.map(point => point.y);
|
|
241
|
+
const firstX = solveFirstControlPoints(x);
|
|
242
|
+
const firstY = solveFirstControlPoints(y);
|
|
243
|
+
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
244
|
+
const last = points.length - 2;
|
|
245
|
+
|
|
246
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
247
|
+
const secondX = index === last
|
|
248
|
+
? (x[index + 1] + firstX[index]) / 2
|
|
249
|
+
: 2 * x[index + 1] - firstX[index + 1];
|
|
250
|
+
const secondY = index === last
|
|
251
|
+
? (y[index + 1] + firstY[index]) / 2
|
|
252
|
+
: 2 * y[index + 1] - firstY[index + 1];
|
|
253
|
+
commands.push({
|
|
254
|
+
op: "C",
|
|
255
|
+
x1: firstX[index],
|
|
256
|
+
y1: firstY[index],
|
|
257
|
+
x2: secondX,
|
|
258
|
+
y2: secondY,
|
|
259
|
+
x: x[index + 1],
|
|
260
|
+
y: y[index + 1]
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return ownCommands(commands);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function buildCurvePathCommands(points, curve = "linear") {
|
|
267
|
+
const validatedPoints = validatePoints(points);
|
|
268
|
+
const validatedCurve = validateCurveInterpolation(curve);
|
|
269
|
+
if (validatedCurve === "linear" ||
|
|
270
|
+
(SMOOTH_CURVES.has(validatedCurve) && points.length < 3)) {
|
|
271
|
+
return buildLinearPathCommands(validatedPoints);
|
|
272
|
+
}
|
|
273
|
+
if (validatedCurve.startsWith("step")) {
|
|
274
|
+
return buildStepCommands(validatedPoints, validatedCurve);
|
|
275
|
+
}
|
|
276
|
+
if (validatedCurve === "basis") return buildBasisCommands(validatedPoints);
|
|
277
|
+
if (validatedCurve === "cardinal") {
|
|
278
|
+
return buildCardinalCommands(validatedPoints);
|
|
279
|
+
}
|
|
280
|
+
if (validatedCurve === "monotone") {
|
|
281
|
+
return buildMonotoneCommands(validatedPoints);
|
|
282
|
+
}
|
|
283
|
+
return buildNaturalCommands(validatedPoints);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function reverseOpenPathCommands(commands) {
|
|
287
|
+
const endpoints = commands.map(command => ({ x: command.x, y: command.y }));
|
|
288
|
+
const reversed = [
|
|
289
|
+
{ op: "M", x: endpoints.at(-1).x, y: endpoints.at(-1).y }
|
|
290
|
+
];
|
|
291
|
+
for (let index = commands.length - 1; index > 0; index -= 1) {
|
|
292
|
+
const command = commands[index];
|
|
293
|
+
const endpoint = endpoints[index - 1];
|
|
294
|
+
if (command.op === "C") {
|
|
295
|
+
reversed.push({
|
|
296
|
+
op: "C",
|
|
297
|
+
x1: command.x2,
|
|
298
|
+
y1: command.y2,
|
|
299
|
+
x2: command.x1,
|
|
300
|
+
y2: command.y1,
|
|
301
|
+
x: endpoint.x,
|
|
302
|
+
y: endpoint.y
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
reversed.push({ op: "L", x: endpoint.x, y: endpoint.y });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return reversed;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function transposeCommands(commands) {
|
|
312
|
+
return commands.map(command => command.op === "C"
|
|
313
|
+
? {
|
|
314
|
+
op: "C",
|
|
315
|
+
x1: command.y1,
|
|
316
|
+
y1: command.x1,
|
|
317
|
+
x2: command.y2,
|
|
318
|
+
y2: command.x2,
|
|
319
|
+
x: command.y,
|
|
320
|
+
y: command.x
|
|
321
|
+
}
|
|
322
|
+
: command.op === "Z"
|
|
323
|
+
? command
|
|
324
|
+
: { op: command.op, x: command.y, y: command.x });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildOrientedCurvePathCommands(points, curve, independentAxis) {
|
|
328
|
+
if (independentAxis === "x") return buildCurvePathCommands(points, curve);
|
|
329
|
+
if (independentAxis !== "y") {
|
|
330
|
+
throw new Error(`Unsupported curve independent axis "${independentAxis}".`);
|
|
331
|
+
}
|
|
332
|
+
const transposed = points.map(point => ({ x: point.y, y: point.x }));
|
|
333
|
+
return ownCommands(transposeCommands(buildCurvePathCommands(transposed, curve)));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function buildAreaCurvePathCommands(
|
|
337
|
+
lowerPoints,
|
|
338
|
+
upperPoints,
|
|
339
|
+
curve = "linear",
|
|
340
|
+
{ independentAxis = "x" } = {}
|
|
341
|
+
) {
|
|
342
|
+
const lower = buildOrientedCurvePathCommands(
|
|
343
|
+
lowerPoints,
|
|
344
|
+
curve,
|
|
345
|
+
independentAxis
|
|
346
|
+
);
|
|
347
|
+
const upper = reverseOpenPathCommands(
|
|
348
|
+
buildOrientedCurvePathCommands(upperPoints, curve, independentAxis)
|
|
349
|
+
);
|
|
350
|
+
return ownCommands([
|
|
351
|
+
...lower,
|
|
352
|
+
{ op: "L", x: upper[0].x, y: upper[0].y },
|
|
353
|
+
...upper.slice(1),
|
|
354
|
+
{ op: "Z" }
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import { isNominalValue } from "./scales.js";
|
|
3
|
+
|
|
4
|
+
const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
|
|
5
|
+
|
|
6
|
+
export const DENSITY_KERNELS = Object.freeze([
|
|
7
|
+
"gaussian",
|
|
8
|
+
"epanechnikov",
|
|
9
|
+
"uniform",
|
|
10
|
+
"triangular"
|
|
11
|
+
]);
|
|
12
|
+
export const DENSITY_NORMALIZATIONS = Object.freeze(["unit", "count"]);
|
|
13
|
+
|
|
14
|
+
export function validateDensityKernel(value) {
|
|
15
|
+
if (!DENSITY_KERNELS.includes(value)) {
|
|
16
|
+
throw new Error(`Unsupported density kernel "${value}".`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateDensityNormalization(value) {
|
|
22
|
+
if (!DENSITY_NORMALIZATIONS.includes(value)) {
|
|
23
|
+
throw new Error(`Unsupported density normalization "${value}".`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireField(value, label) {
|
|
29
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
30
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateDensityTransform(transform) {
|
|
36
|
+
const supported = [
|
|
37
|
+
"type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
|
|
38
|
+
"resolve", "kernel", "normalization"
|
|
39
|
+
];
|
|
40
|
+
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
41
|
+
if (unknown !== undefined) {
|
|
42
|
+
throw new Error(`Unknown density transform property "${unknown}".`);
|
|
43
|
+
}
|
|
44
|
+
if (transform.type !== "density") {
|
|
45
|
+
throw new Error(`Unsupported density transform "${transform.type}".`);
|
|
46
|
+
}
|
|
47
|
+
requireField(transform.field, "Density field");
|
|
48
|
+
if (transform.groupBy !== undefined) {
|
|
49
|
+
requireField(transform.groupBy, "Density groupBy");
|
|
50
|
+
}
|
|
51
|
+
validateDensityKernel(transform.kernel ?? "gaussian");
|
|
52
|
+
validateDensityNormalization(transform.normalization ?? "unit");
|
|
53
|
+
if (
|
|
54
|
+
transform.bandwidth !== "auto" &&
|
|
55
|
+
(!Number.isFinite(transform.bandwidth) || transform.bandwidth <= 0)
|
|
56
|
+
) {
|
|
57
|
+
throw new RangeError(
|
|
58
|
+
"Density bandwidth must be a positive finite number or auto."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
transform.extent !== "auto" &&
|
|
63
|
+
(!Array.isArray(transform.extent) ||
|
|
64
|
+
transform.extent.length !== 2 ||
|
|
65
|
+
!transform.extent.every(Number.isFinite) ||
|
|
66
|
+
transform.extent[0] >= transform.extent[1])
|
|
67
|
+
) {
|
|
68
|
+
throw new RangeError(
|
|
69
|
+
"Density extent must be an ascending pair of finite numbers or auto."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!Number.isInteger(transform.steps) || transform.steps < 2) {
|
|
73
|
+
throw new RangeError("Density steps must be an integer of at least 2.");
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
!Array.isArray(transform.as) ||
|
|
77
|
+
transform.as.length !== 2 ||
|
|
78
|
+
!transform.as.every(value => typeof value === "string" && value.length > 0) ||
|
|
79
|
+
transform.as[0] === transform.as[1]
|
|
80
|
+
) {
|
|
81
|
+
throw new TypeError("Density as must contain two distinct non-empty fields.");
|
|
82
|
+
}
|
|
83
|
+
const collisions = new Set([
|
|
84
|
+
transform.field,
|
|
85
|
+
transform.groupBy
|
|
86
|
+
].filter(Boolean));
|
|
87
|
+
if (transform.as.some(value => collisions.has(value))) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Density output fields must not collide with source or group fields."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (transform.resolve !== "shared") {
|
|
93
|
+
throw new Error(`Unsupported density resolve "${transform.resolve}".`);
|
|
94
|
+
}
|
|
95
|
+
return transform;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function quantile(sortedValues, probability) {
|
|
99
|
+
const index = (sortedValues.length - 1) * probability;
|
|
100
|
+
const lower = Math.floor(index);
|
|
101
|
+
const upper = Math.ceil(index);
|
|
102
|
+
if (lower === upper) return sortedValues[lower];
|
|
103
|
+
const ratio = index - lower;
|
|
104
|
+
return sortedValues[lower] * (1 - ratio) + sortedValues[upper] * ratio;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sampleDeviation(values) {
|
|
108
|
+
if (values.length < 2) return 0;
|
|
109
|
+
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
110
|
+
const sumSquares = values.reduce(
|
|
111
|
+
(sum, value) => sum + (value - mean) ** 2,
|
|
112
|
+
0
|
|
113
|
+
);
|
|
114
|
+
return Math.sqrt(sumSquares / (values.length - 1));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function estimateDensityBandwidth(values) {
|
|
118
|
+
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
119
|
+
throw new TypeError("Density bandwidth values must be finite numbers.");
|
|
120
|
+
}
|
|
121
|
+
if (values.length < 2) {
|
|
122
|
+
throw new Error("Density auto bandwidth requires at least two values.");
|
|
123
|
+
}
|
|
124
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
125
|
+
const deviation = sampleDeviation(sorted);
|
|
126
|
+
const interquartileRange = quantile(sorted, 0.75) - quantile(sorted, 0.25);
|
|
127
|
+
const robustDeviation = interquartileRange / 1.34;
|
|
128
|
+
const spread = robustDeviation > 0
|
|
129
|
+
? Math.min(deviation, robustDeviation)
|
|
130
|
+
: deviation;
|
|
131
|
+
const bandwidth = 1.06 * spread * sorted.length ** -0.2;
|
|
132
|
+
if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
|
|
133
|
+
throw new Error("Density auto bandwidth requires varying finite values.");
|
|
134
|
+
}
|
|
135
|
+
return bandwidth;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveBandwidth(value, sourceValues) {
|
|
139
|
+
if (value === undefined || value === "auto") {
|
|
140
|
+
return estimateDensityBandwidth(sourceValues);
|
|
141
|
+
}
|
|
142
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
143
|
+
throw new RangeError(
|
|
144
|
+
"Density bandwidth must be a positive finite number or auto."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveExtent(value, sourceValues) {
|
|
151
|
+
if (value === undefined || value === "auto") {
|
|
152
|
+
const extent = [Math.min(...sourceValues), Math.max(...sourceValues)];
|
|
153
|
+
if (extent[0] === extent[1]) {
|
|
154
|
+
throw new Error("Density observed extent requires varying finite values.");
|
|
155
|
+
}
|
|
156
|
+
return extent;
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
!Array.isArray(value) ||
|
|
160
|
+
value.length !== 2 ||
|
|
161
|
+
!value.every(Number.isFinite) ||
|
|
162
|
+
value[0] >= value[1]
|
|
163
|
+
) {
|
|
164
|
+
throw new RangeError(
|
|
165
|
+
"Density extent must be an ascending pair of finite numbers or auto."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return [...value];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveOutputFields(as, field, groupBy) {
|
|
172
|
+
const resolved = as ?? [`${field}_value`, `${field}_density`];
|
|
173
|
+
if (
|
|
174
|
+
!Array.isArray(resolved) ||
|
|
175
|
+
resolved.length !== 2 ||
|
|
176
|
+
!resolved.every(value => typeof value === "string" && value.length > 0) ||
|
|
177
|
+
resolved[0] === resolved[1]
|
|
178
|
+
) {
|
|
179
|
+
throw new TypeError("Density as must contain two distinct non-empty fields.");
|
|
180
|
+
}
|
|
181
|
+
const collisions = new Set([field, groupBy].filter(Boolean));
|
|
182
|
+
if (resolved.some(value => collisions.has(value))) {
|
|
183
|
+
throw new Error("Density output fields must not collide with source or group fields.");
|
|
184
|
+
}
|
|
185
|
+
return [...resolved];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const KERNEL_FUNCTIONS = Object.freeze({
|
|
189
|
+
gaussian(value) {
|
|
190
|
+
return Math.exp(-0.5 * value ** 2) / SQRT_TWO_PI;
|
|
191
|
+
},
|
|
192
|
+
epanechnikov(value) {
|
|
193
|
+
return Math.abs(value) <= 1 ? 0.75 * (1 - value ** 2) : 0;
|
|
194
|
+
},
|
|
195
|
+
uniform(value) {
|
|
196
|
+
return Math.abs(value) <= 1 ? 0.5 : 0;
|
|
197
|
+
},
|
|
198
|
+
triangular(value) {
|
|
199
|
+
return Math.abs(value) <= 1 ? 1 - Math.abs(value) : 0;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
function estimateAt(sample, values, bandwidth, kernel, normalization) {
|
|
204
|
+
const sum = values.reduce(
|
|
205
|
+
(total, value) => total + KERNEL_FUNCTIONS[kernel](
|
|
206
|
+
(sample - value) / bandwidth
|
|
207
|
+
),
|
|
208
|
+
0
|
|
209
|
+
);
|
|
210
|
+
const denominator = normalization === "unit"
|
|
211
|
+
? values.length * bandwidth
|
|
212
|
+
: bandwidth;
|
|
213
|
+
return sum / denominator;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function deriveKernelDensity(values, {
|
|
217
|
+
field,
|
|
218
|
+
groupBy,
|
|
219
|
+
bandwidth = "auto",
|
|
220
|
+
extent = "auto",
|
|
221
|
+
steps = 100,
|
|
222
|
+
kernel = "gaussian",
|
|
223
|
+
normalization = "unit",
|
|
224
|
+
as
|
|
225
|
+
} = {}) {
|
|
226
|
+
if (!Array.isArray(values)) {
|
|
227
|
+
throw new TypeError("Density values must be an array.");
|
|
228
|
+
}
|
|
229
|
+
const sourceField = requireField(field, "Density field");
|
|
230
|
+
const groupField = groupBy === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: requireField(groupBy, "Density groupBy");
|
|
233
|
+
if (!Number.isInteger(steps) || steps < 2) {
|
|
234
|
+
throw new RangeError("Density steps must be an integer of at least 2.");
|
|
235
|
+
}
|
|
236
|
+
const outputFields = resolveOutputFields(as, sourceField, groupField);
|
|
237
|
+
const resolvedKernel = validateDensityKernel(kernel);
|
|
238
|
+
const resolvedNormalization = validateDensityNormalization(normalization);
|
|
239
|
+
const validRows = values.filter(row =>
|
|
240
|
+
row !== null &&
|
|
241
|
+
typeof row === "object" &&
|
|
242
|
+
Number.isFinite(row[sourceField]) &&
|
|
243
|
+
(groupField === undefined || isNominalValue(row[groupField]))
|
|
244
|
+
);
|
|
245
|
+
if (validRows.length === 0) {
|
|
246
|
+
throw new Error("Density requires at least one valid field/group row.");
|
|
247
|
+
}
|
|
248
|
+
const sourceValues = validRows.map(row => row[sourceField]);
|
|
249
|
+
const resolvedBandwidth = resolveBandwidth(bandwidth, sourceValues);
|
|
250
|
+
const resolvedExtent = resolveExtent(extent, sourceValues);
|
|
251
|
+
const groups = groupField === undefined
|
|
252
|
+
? [undefined]
|
|
253
|
+
: [...new Set(validRows.map(row => row[groupField]))];
|
|
254
|
+
const sampleStep = (resolvedExtent[1] - resolvedExtent[0]) / (steps - 1);
|
|
255
|
+
const samples = Array.from(
|
|
256
|
+
{ length: steps },
|
|
257
|
+
(_, index) => index === steps - 1
|
|
258
|
+
? resolvedExtent[1]
|
|
259
|
+
: resolvedExtent[0] + sampleStep * index
|
|
260
|
+
);
|
|
261
|
+
const rows = [];
|
|
262
|
+
for (const group of groups) {
|
|
263
|
+
const groupValues = validRows
|
|
264
|
+
.filter(row => groupField === undefined || row[groupField] === group)
|
|
265
|
+
.map(row => row[sourceField]);
|
|
266
|
+
for (const sample of samples) {
|
|
267
|
+
rows.push({
|
|
268
|
+
...(groupField === undefined ? {} : { [groupField]: group }),
|
|
269
|
+
[outputFields[0]]: sample,
|
|
270
|
+
[outputFields[1]]: estimateAt(
|
|
271
|
+
sample,
|
|
272
|
+
groupValues,
|
|
273
|
+
resolvedBandwidth,
|
|
274
|
+
resolvedKernel,
|
|
275
|
+
resolvedNormalization
|
|
276
|
+
)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return cloneAndFreeze({
|
|
281
|
+
fields: {
|
|
282
|
+
source: sourceField,
|
|
283
|
+
...(groupField === undefined ? {} : { group: groupField }),
|
|
284
|
+
value: outputFields[0],
|
|
285
|
+
density: outputFields[1]
|
|
286
|
+
},
|
|
287
|
+
groups,
|
|
288
|
+
bandwidth: resolvedBandwidth,
|
|
289
|
+
kernel: resolvedKernel,
|
|
290
|
+
normalization: resolvedNormalization,
|
|
291
|
+
extent: resolvedExtent,
|
|
292
|
+
steps,
|
|
293
|
+
samples,
|
|
294
|
+
values: rows
|
|
295
|
+
});
|
|
296
|
+
}
|