ggaction 0.0.2 → 0.0.4
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 -2
- package/README.md +95 -72
- package/package.json +15 -5
- package/src/actions/boxPlots/components.js +3 -3
- package/src/actions/boxPlots/edit.js +277 -0
- package/src/actions/boxPlots/index.js +2 -0
- package/src/actions/boxPlots/materialize.js +12 -1
- package/src/actions/boxPlots/options.js +25 -14
- package/src/actions/canvas/actions.js +21 -17
- package/src/actions/composition/actions.js +247 -0
- package/src/actions/composition/index.js +1 -0
- package/src/actions/coordinates/actions.js +2 -5
- package/src/actions/data/density.js +7 -1
- package/src/actions/data/filter.js +3 -1
- package/src/actions/data/regression.js +1 -1
- package/src/actions/encodings/appearance.js +12 -1
- package/src/actions/encodings/barWidth.js +5 -3
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +151 -0
- package/src/actions/encodings/color/layout.js +91 -0
- package/src/actions/encodings/color/policy.js +88 -0
- package/src/actions/encodings/index.js +4 -2
- package/src/actions/encodings/offset.js +91 -73
- package/src/actions/encodings/position/apply.js +66 -0
- package/src/actions/encodings/position/index.js +130 -0
- package/src/actions/encodings/position/policies/arc.js +26 -0
- package/src/actions/encodings/position/policies/bar.js +9 -5
- package/src/actions/encodings/position/policies/index.js +5 -1
- package/src/actions/encodings/position/policies/line.js +20 -3
- package/src/actions/encodings/position/resolve.js +58 -9
- package/src/actions/encodings/ranged.js +30 -8
- package/src/actions/encodings/shared.js +17 -21
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +73 -0
- package/src/actions/errorBands/create.js +37 -29
- package/src/actions/errorBands/edit.js +206 -0
- package/src/actions/errorBands/index.js +9 -0
- package/src/actions/errorBands/options.js +39 -0
- package/src/actions/errorBars/create.js +50 -43
- package/src/actions/errorBars/edit.js +130 -0
- package/src/actions/errorBars/index.js +3 -0
- package/src/actions/errorBars/options.js +52 -0
- package/src/actions/facets/actions.js +162 -0
- package/src/actions/facets/derive.js +200 -0
- package/src/actions/facets/guides.js +16 -0
- package/src/actions/facets/index.js +1 -0
- package/src/actions/facets/replay.js +69 -0
- package/src/actions/guides/applicability.js +113 -0
- package/src/actions/guides/axes/axes.js +70 -16
- package/src/actions/guides/axes/edit.js +126 -0
- package/src/actions/guides/axes/index.js +12 -0
- package/src/actions/guides/axes/labels.js +22 -19
- package/src/actions/guides/axes/remove.js +60 -0
- package/src/actions/guides/axes/tickGroups.js +7 -5
- package/src/actions/guides/axes/ticks.js +10 -5
- package/src/actions/guides/axes/titles.js +8 -6
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +6 -14
- package/src/actions/guides/guides.js +17 -75
- package/src/actions/guides/index.js +6 -0
- package/src/actions/guides/legends/categorical/actions.js +48 -9
- package/src/actions/guides/legends/categorical/components.js +16 -16
- package/src/actions/guides/legends/categorical/index.js +14 -14
- package/src/actions/guides/legends/categorical/layout.js +7 -3
- package/src/actions/guides/legends/categorical/options.js +2 -5
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +14 -14
- package/src/actions/guides/legends/continuous/common.js +7 -7
- package/src/actions/guides/legends/continuous/interval.js +6 -6
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +4 -26
- package/src/actions/guides/legends/focused.js +63 -0
- package/src/actions/guides/legends/index.js +4 -0
- package/src/actions/guides/legends/remove.js +46 -0
- package/src/actions/guides/legends/size.js +13 -15
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/axes/facade.js +219 -0
- package/src/actions/guides/polar/axes/index.js +55 -0
- package/src/actions/guides/polar/axes/labels.js +188 -0
- package/src/actions/guides/polar/axes/lines.js +113 -0
- package/src/actions/guides/polar/axes/shared.js +112 -0
- package/src/actions/guides/polar/axes/ticks.js +164 -0
- package/src/actions/guides/polar/axes/titles.js +165 -0
- package/src/actions/guides/polar/grids.js +252 -0
- package/src/actions/guides/polar/index.js +2 -0
- package/src/actions/guides/polar/resolve.js +269 -0
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/index.js +4 -0
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +239 -0
- package/src/actions/marks/arc/index.js +1 -0
- package/src/actions/marks/{area.js → area/actions.js} +51 -38
- package/src/actions/marks/area/index.js +4 -0
- package/src/actions/marks/bar/create.js +26 -5
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/bar/materialize.js +25 -31
- package/src/actions/marks/index.js +12 -4
- package/src/actions/marks/lifecycle.js +27 -0
- package/src/actions/marks/line/actions.js +373 -0
- package/src/actions/marks/line/index.js +1 -0
- package/src/actions/marks/{point.js → point/actions.js} +67 -108
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/rect/actions.js +165 -0
- package/src/actions/marks/rect/index.js +1 -0
- package/src/actions/marks/remove.js +214 -0
- package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
- package/src/actions/marks/rule/index.js +1 -0
- package/src/actions/marks/shared.js +156 -25
- package/src/actions/marks/text/actions.js +192 -0
- package/src/actions/marks/text/index.js +1 -0
- package/src/actions/primitives/createGraphics.js +13 -6
- package/src/actions/primitives/editGraphics.js +10 -3
- package/src/actions/primitives/semantic.js +26 -3
- package/src/actions/primitives/semanticValue.js +26 -4
- package/src/actions/regression/components.js +2 -2
- package/src/actions/regression/create.js +19 -2
- package/src/actions/regression/edit.js +247 -0
- package/src/actions/regression/index.js +3 -0
- package/src/actions/regression/resolve.js +3 -1
- package/src/actions/scales/consumers/common.js +91 -0
- package/src/actions/scales/consumers/families.js +47 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +15 -6
- package/src/actions/scales/edit.js +45 -22
- package/src/actions/scales/materialize.js +58 -409
- package/src/actions/selection/actions.js +38 -24
- package/src/actions/selection/index.js +2 -0
- package/src/actions/titles/actions.js +63 -2
- package/src/actions/titles/index.js +2 -0
- package/src/actions/titles/resolve.js +12 -17
- package/src/composition.js +10 -0
- package/src/core/ChartProgram.js +49 -65
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/core/validation.js +16 -0
- package/src/core/vocabulary.js +154 -4
- package/src/grammar/arcs.js +154 -0
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/bars/aggregate.js +3 -3
- package/src/grammar/bars/geometry.js +3 -3
- package/src/grammar/bars/policy.js +7 -1
- package/src/grammar/coordinates.js +3 -7
- package/src/grammar/density.js +21 -2
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/index.js +122 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/interval.js +2 -96
- package/src/grammar/lineSeries.js +112 -4
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/polar.js +102 -0
- package/src/grammar/polarGuides.js +243 -0
- package/src/grammar/polarLineCommands.js +43 -0
- package/src/grammar/polarPaths.js +161 -0
- package/src/grammar/positionCompatibility.js +18 -2
- package/src/grammar/rects.js +26 -0
- package/src/grammar/regression/derive.js +97 -0
- package/src/grammar/regression/index.js +22 -0
- package/src/grammar/regression/models.js +282 -0
- package/src/grammar/regression/parameters.js +106 -0
- package/src/grammar/rules.js +1 -1
- package/src/grammar/scales/color.js +4 -2
- package/src/grammar/scales/continuous.js +6 -1
- package/src/grammar/scales/fields.js +7 -0
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +1 -1
- package/src/grammar/schemas/graphic.js +1 -1
- package/src/grammar/schemas/graphicBounds.js +64 -9
- package/src/grammar/schemas/graphicTree.js +44 -10
- package/src/grammar/schemas/semanticPath.js +31 -9
- package/src/grammar/seriesLayout.js +13 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- package/src/grammar/transforms.js +1 -1
- package/src/index.js +1 -0
- package/src/layout/composition.js +310 -0
- package/src/layout/facets.js +131 -0
- package/src/layout/title.js +6 -0
- package/src/materialization/bars/aggregate.js +6 -3
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +47 -73
- package/src/materialization/encodings.js +14 -28
- package/src/materialization/facetGuides/index.js +2 -0
- package/src/materialization/facetGuides/legacyCategorical.js +125 -0
- package/src/materialization/facetGuides/placement.js +126 -0
- package/src/materialization/facetGuides/preparation.js +188 -0
- package/src/materialization/facets.js +214 -0
- package/src/materialization/graphicHierarchy.js +15 -5
- package/src/materialization/guides/resources.js +46 -0
- package/src/materialization/marks/capabilities.js +141 -0
- package/src/materialization/marks/index.js +105 -0
- package/src/materialization/marks/policies.js +87 -0
- package/src/materialization/planner.js +105 -12
- package/src/materialization/rect.js +138 -0
- package/src/materialization/rectConfig.js +38 -0
- package/src/materialization/rowEncoding.js +33 -0
- package/src/materialization/scaleGuideDependencies.js +94 -0
- package/src/materialization/scales/map.js +40 -0
- package/src/materialization/scales/policies/arc.js +37 -0
- package/src/materialization/scales/policies/bar.js +132 -0
- package/src/materialization/scales/policies/series.js +76 -0
- package/src/materialization/scales/resolve.js +326 -0
- package/src/materialization/selection/items/arc.js +29 -0
- package/src/materialization/selection/items/bar.js +255 -0
- package/src/materialization/selection/items/common.js +131 -0
- package/src/materialization/selection/items/index.js +6 -0
- package/src/materialization/selection/items/path.js +59 -0
- package/src/materialization/selection/items/point.js +55 -0
- package/src/materialization/selection/items/rect.js +23 -0
- package/src/materialization/selection/items/rule.js +25 -0
- package/src/materialization/selection/policies/arc.js +10 -0
- package/src/materialization/selection/policies/area.js +1 -1
- package/src/materialization/selection/policies/bar.js +1 -1
- package/src/materialization/selection/policies/index.js +5 -1
- package/src/materialization/selection/policies/line.js +1 -1
- package/src/materialization/selection/policies/point.js +1 -1
- package/src/materialization/selection/policies/rect.js +10 -0
- package/src/materialization/selection/policies/rule.js +1 -1
- package/src/materialization/selection/styles.js +25 -17
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/src/renderers/canvas/text.js +14 -1
- package/types/extension.d.ts +5 -1
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/encodings/color.js +0 -329
- package/src/actions/encodings/position.js +0 -176
- package/src/actions/marks/line.js +0 -248
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/marks.js +0 -110
- package/src/materialization/selection/items.js +0 -503
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { freezeOwned } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const KAPPA = 0.5522847498307936;
|
|
4
|
+
const MAX_ARC_SEGMENT = 90;
|
|
5
|
+
|
|
6
|
+
function validateFrame(frame) {
|
|
7
|
+
if (
|
|
8
|
+
frame === null ||
|
|
9
|
+
typeof frame !== "object" ||
|
|
10
|
+
![frame.centerX, frame.centerY, frame.availableRadius].every(Number.isFinite) ||
|
|
11
|
+
frame.availableRadius < 0
|
|
12
|
+
) {
|
|
13
|
+
throw new TypeError(
|
|
14
|
+
"Polar path frame requires finite centerX, centerY, and non-negative availableRadius."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return frame;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateRadius(value, label, frame) {
|
|
21
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
22
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
23
|
+
}
|
|
24
|
+
if (value > frame.availableRadius) {
|
|
25
|
+
throw new RangeError(`${label} must fit inside the Polar frame.`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pointAt(frame, theta, radius) {
|
|
31
|
+
const radians = theta * Math.PI / 180;
|
|
32
|
+
return {
|
|
33
|
+
x: frame.centerX + radius * Math.sin(radians),
|
|
34
|
+
y: frame.centerY - radius * Math.cos(radians)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function arcCommands(frame, radius, startTheta, endTheta) {
|
|
39
|
+
const sweep = endTheta - startTheta;
|
|
40
|
+
const segments = Math.ceil(Math.abs(sweep) / MAX_ARC_SEGMENT);
|
|
41
|
+
const delta = sweep / segments;
|
|
42
|
+
const commands = [];
|
|
43
|
+
for (let index = 0; index < segments; index += 1) {
|
|
44
|
+
const start = startTheta + delta * index;
|
|
45
|
+
const end = start + delta;
|
|
46
|
+
const startRadians = start * Math.PI / 180;
|
|
47
|
+
const endRadians = end * Math.PI / 180;
|
|
48
|
+
const deltaRadians = endRadians - startRadians;
|
|
49
|
+
const control = 4 / 3 * Math.tan(deltaRadians / 4);
|
|
50
|
+
const from = pointAt(frame, start, radius);
|
|
51
|
+
const to = pointAt(frame, end, radius);
|
|
52
|
+
const fromDerivative = {
|
|
53
|
+
x: radius * Math.cos(startRadians),
|
|
54
|
+
y: radius * Math.sin(startRadians)
|
|
55
|
+
};
|
|
56
|
+
const toDerivative = {
|
|
57
|
+
x: radius * Math.cos(endRadians),
|
|
58
|
+
y: radius * Math.sin(endRadians)
|
|
59
|
+
};
|
|
60
|
+
commands.push({
|
|
61
|
+
op: "C",
|
|
62
|
+
x1: from.x + control * fromDerivative.x,
|
|
63
|
+
y1: from.y + control * fromDerivative.y,
|
|
64
|
+
x2: to.x - control * toDerivative.x,
|
|
65
|
+
y2: to.y - control * toDerivative.y,
|
|
66
|
+
x: to.x,
|
|
67
|
+
y: to.y
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return commands;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildPolarCircleCommands(frame, radius) {
|
|
74
|
+
validateFrame(frame);
|
|
75
|
+
validateRadius(radius, "Polar circle radius", frame);
|
|
76
|
+
const control = radius * KAPPA;
|
|
77
|
+
const { centerX: cx, centerY: cy } = frame;
|
|
78
|
+
return freezeOwned([
|
|
79
|
+
{ op: "M", x: cx, y: cy - radius },
|
|
80
|
+
{
|
|
81
|
+
op: "C",
|
|
82
|
+
x1: cx + control,
|
|
83
|
+
y1: cy - radius,
|
|
84
|
+
x2: cx + radius,
|
|
85
|
+
y2: cy - control,
|
|
86
|
+
x: cx + radius,
|
|
87
|
+
y: cy
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
op: "C",
|
|
91
|
+
x1: cx + radius,
|
|
92
|
+
y1: cy + control,
|
|
93
|
+
x2: cx + control,
|
|
94
|
+
y2: cy + radius,
|
|
95
|
+
x: cx,
|
|
96
|
+
y: cy + radius
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
op: "C",
|
|
100
|
+
x1: cx - control,
|
|
101
|
+
y1: cy + radius,
|
|
102
|
+
x2: cx - radius,
|
|
103
|
+
y2: cy + control,
|
|
104
|
+
x: cx - radius,
|
|
105
|
+
y: cy
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
op: "C",
|
|
109
|
+
x1: cx - radius,
|
|
110
|
+
y1: cy - control,
|
|
111
|
+
x2: cx - control,
|
|
112
|
+
y2: cy - radius,
|
|
113
|
+
x: cx,
|
|
114
|
+
y: cy - radius
|
|
115
|
+
},
|
|
116
|
+
{ op: "Z" }
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildAnnularSectorCommands({
|
|
121
|
+
frame,
|
|
122
|
+
startTheta,
|
|
123
|
+
endTheta,
|
|
124
|
+
innerRadius = 0,
|
|
125
|
+
outerRadius,
|
|
126
|
+
padAngle = 0
|
|
127
|
+
}) {
|
|
128
|
+
validateFrame(frame);
|
|
129
|
+
if (![startTheta, endTheta].every(Number.isFinite)) {
|
|
130
|
+
throw new TypeError("Polar sector angles must be finite numbers.");
|
|
131
|
+
}
|
|
132
|
+
validateRadius(innerRadius, "Polar sector innerRadius", frame);
|
|
133
|
+
validateRadius(outerRadius, "Polar sector outerRadius", frame);
|
|
134
|
+
if (outerRadius <= innerRadius) {
|
|
135
|
+
throw new RangeError("Polar sector outerRadius must be greater than innerRadius.");
|
|
136
|
+
}
|
|
137
|
+
const sweep = endTheta - startTheta;
|
|
138
|
+
if (sweep === 0 || Math.abs(sweep) > 360) {
|
|
139
|
+
throw new RangeError("Polar sector sweep must be non-zero and at most 360 degrees.");
|
|
140
|
+
}
|
|
141
|
+
if (!Number.isFinite(padAngle) || padAngle < 0 || padAngle >= Math.abs(sweep)) {
|
|
142
|
+
throw new RangeError(
|
|
143
|
+
"Polar sector padAngle must be non-negative and smaller than its sweep."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const direction = Math.sign(sweep);
|
|
147
|
+
const start = startTheta + direction * padAngle / 2;
|
|
148
|
+
const end = endTheta - direction * padAngle / 2;
|
|
149
|
+
const commands = [
|
|
150
|
+
{ op: "M", ...pointAt(frame, start, outerRadius) },
|
|
151
|
+
...arcCommands(frame, outerRadius, start, end)
|
|
152
|
+
];
|
|
153
|
+
if (innerRadius === 0) {
|
|
154
|
+
commands.push({ op: "L", x: frame.centerX, y: frame.centerY });
|
|
155
|
+
} else {
|
|
156
|
+
commands.push({ op: "L", ...pointAt(frame, end, innerRadius) });
|
|
157
|
+
commands.push(...arcCommands(frame, innerRadius, end, start));
|
|
158
|
+
}
|
|
159
|
+
commands.push({ op: "Z" });
|
|
160
|
+
return freezeOwned(commands);
|
|
161
|
+
}
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
|
|
2
2
|
point: Object.freeze({
|
|
3
3
|
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
4
|
-
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
4
|
+
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
5
|
+
theta: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
6
|
+
radius: Object.freeze(["quantitative"])
|
|
5
7
|
}),
|
|
6
8
|
line: Object.freeze({
|
|
7
9
|
x: Object.freeze(["quantitative", "temporal"]),
|
|
8
|
-
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
10
|
+
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
11
|
+
theta: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
12
|
+
radius: Object.freeze(["quantitative"])
|
|
9
13
|
}),
|
|
10
14
|
area: Object.freeze({
|
|
11
15
|
x: Object.freeze(["quantitative", "temporal"]),
|
|
12
16
|
y: Object.freeze(["quantitative", "temporal"])
|
|
13
17
|
}),
|
|
18
|
+
arc: Object.freeze({
|
|
19
|
+
theta: Object.freeze(["ordinal", "nominal"]),
|
|
20
|
+
radius: Object.freeze(["quantitative"])
|
|
21
|
+
}),
|
|
14
22
|
rule: Object.freeze({
|
|
15
23
|
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
16
24
|
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
@@ -18,6 +26,14 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
|
|
|
18
26
|
bar: Object.freeze({
|
|
19
27
|
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
20
28
|
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
29
|
+
}),
|
|
30
|
+
text: Object.freeze({
|
|
31
|
+
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
32
|
+
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
33
|
+
}),
|
|
34
|
+
rect: Object.freeze({
|
|
35
|
+
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
36
|
+
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
21
37
|
})
|
|
22
38
|
});
|
|
23
39
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const RECT_MODES = Object.freeze({
|
|
2
|
+
discrete: "discrete",
|
|
3
|
+
ranged: "ranged"
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
function categorical(encoding) {
|
|
7
|
+
return ["nominal", "ordinal"].includes(encoding?.fieldType) &&
|
|
8
|
+
encoding.scale !== undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ranged(primary, secondary) {
|
|
12
|
+
return primary?.scale !== undefined &&
|
|
13
|
+
secondary?.scale === primary.scale &&
|
|
14
|
+
["quantitative", "temporal"].includes(primary.fieldType) &&
|
|
15
|
+
secondary.fieldType === primary.fieldType;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveRectMode(layer) {
|
|
19
|
+
if (layer?.mark?.type !== "rect") return undefined;
|
|
20
|
+
const { x, y, x2, y2 } = layer.encoding ?? {};
|
|
21
|
+
if (x2 === undefined && y2 === undefined && categorical(x) && categorical(y)) {
|
|
22
|
+
return RECT_MODES.discrete;
|
|
23
|
+
}
|
|
24
|
+
if (ranged(x, x2) && ranged(y, y2)) return RECT_MODES.ranged;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
readNominalField,
|
|
4
|
+
readQuantitativeField
|
|
5
|
+
} from "../scales/index.js";
|
|
6
|
+
import { fitRegressionGroup, predictRegressionAt } from "./models.js";
|
|
7
|
+
import {
|
|
8
|
+
normalizeRegressionParameters,
|
|
9
|
+
requireRegressionField
|
|
10
|
+
} from "./parameters.js";
|
|
11
|
+
|
|
12
|
+
export const REGRESSION_LOWER_FIELD = "__regression_ci_lower";
|
|
13
|
+
export const REGRESSION_UPPER_FIELD = "__regression_ci_upper";
|
|
14
|
+
|
|
15
|
+
export function deriveRegression(values, {
|
|
16
|
+
x,
|
|
17
|
+
y,
|
|
18
|
+
groupBy,
|
|
19
|
+
method,
|
|
20
|
+
degree,
|
|
21
|
+
span,
|
|
22
|
+
confidence,
|
|
23
|
+
interval
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (!Array.isArray(values)) {
|
|
26
|
+
throw new TypeError("Regression values must be an array.");
|
|
27
|
+
}
|
|
28
|
+
requireRegressionField(x, "Regression x field");
|
|
29
|
+
requireRegressionField(y, "Regression y field");
|
|
30
|
+
if (groupBy !== undefined) {
|
|
31
|
+
requireRegressionField(groupBy, "Regression groupBy field");
|
|
32
|
+
}
|
|
33
|
+
const parameters = normalizeRegressionParameters({
|
|
34
|
+
method,
|
|
35
|
+
degree,
|
|
36
|
+
span,
|
|
37
|
+
confidence,
|
|
38
|
+
interval
|
|
39
|
+
});
|
|
40
|
+
readQuantitativeField(values, x);
|
|
41
|
+
readQuantitativeField(values, y);
|
|
42
|
+
if (groupBy !== undefined) readNominalField(values, groupBy);
|
|
43
|
+
|
|
44
|
+
const groups = groupBy === undefined
|
|
45
|
+
? [undefined]
|
|
46
|
+
: [...new Set(values.map(row => row[groupBy]))];
|
|
47
|
+
const models = [];
|
|
48
|
+
const rows = [];
|
|
49
|
+
|
|
50
|
+
for (const group of groups) {
|
|
51
|
+
const groupRows = groupBy === undefined
|
|
52
|
+
? values
|
|
53
|
+
: values.filter(row => row[groupBy] === group);
|
|
54
|
+
const model = fitRegressionGroup(groupRows, {
|
|
55
|
+
x,
|
|
56
|
+
y,
|
|
57
|
+
group,
|
|
58
|
+
parameters
|
|
59
|
+
});
|
|
60
|
+
const xValues = [...new Set(groupRows.map(row => row[x]))]
|
|
61
|
+
.sort((left, right) => left - right);
|
|
62
|
+
models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
|
|
63
|
+
|
|
64
|
+
for (const xValue of xValues) {
|
|
65
|
+
const prediction = predictRegressionAt(model, xValue, parameters);
|
|
66
|
+
rows.push({
|
|
67
|
+
...(groupBy === undefined ? {} : { [groupBy]: group }),
|
|
68
|
+
[x]: xValue,
|
|
69
|
+
[y]: prediction.prediction,
|
|
70
|
+
...(parameters.method === "loess" ? {} : {
|
|
71
|
+
[REGRESSION_LOWER_FIELD]: prediction.prediction - prediction.margin,
|
|
72
|
+
[REGRESSION_UPPER_FIELD]: prediction.prediction + prediction.margin
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return cloneAndFreeze({
|
|
79
|
+
fields: {
|
|
80
|
+
x,
|
|
81
|
+
y,
|
|
82
|
+
...(groupBy === undefined ? {} : { group: groupBy }),
|
|
83
|
+
...(parameters.method === "loess" ? {} : {
|
|
84
|
+
lower: REGRESSION_LOWER_FIELD,
|
|
85
|
+
upper: REGRESSION_UPPER_FIELD
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
parameters,
|
|
89
|
+
groups,
|
|
90
|
+
models,
|
|
91
|
+
values: rows
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function deriveLinearRegression(values, options = {}) {
|
|
96
|
+
return deriveRegression(values, { ...options, method: "linear" });
|
|
97
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { studentTCriticalValue } from "../statistics/studentT.js";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
REGRESSION_LOWER_FIELD,
|
|
5
|
+
REGRESSION_UPPER_FIELD,
|
|
6
|
+
deriveLinearRegression,
|
|
7
|
+
deriveRegression
|
|
8
|
+
} from "./derive.js";
|
|
9
|
+
export {
|
|
10
|
+
normalizeRegressionParameters,
|
|
11
|
+
validateRegressionTransform
|
|
12
|
+
} from "./parameters.js";
|
|
13
|
+
|
|
14
|
+
export function studentTCritical(confidence, degreesOfFreedom) {
|
|
15
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {
|
|
16
|
+
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom <= 0) {
|
|
19
|
+
throw new RangeError("Student-t degrees of freedom must be positive.");
|
|
20
|
+
}
|
|
21
|
+
return studentTCriticalValue(confidence, degreesOfFreedom);
|
|
22
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { studentTCriticalValue } from "../statistics/studentT.js";
|
|
2
|
+
|
|
3
|
+
function fitLinearGroup(rows, { x, y, group, confidence }) {
|
|
4
|
+
const count = rows.length;
|
|
5
|
+
const groupLabel = group === undefined ? "all" : String(group);
|
|
6
|
+
if (count < 3) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`Regression group "${groupLabel}" requires at least three rows.`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const meanX = rows.reduce((sum, row) => sum + row[x], 0) / count;
|
|
12
|
+
const meanY = rows.reduce((sum, row) => sum + row[y], 0) / count;
|
|
13
|
+
let sxx = 0;
|
|
14
|
+
let sxy = 0;
|
|
15
|
+
for (const row of rows) {
|
|
16
|
+
const xDifference = row[x] - meanX;
|
|
17
|
+
sxx += xDifference ** 2;
|
|
18
|
+
sxy += xDifference * (row[y] - meanY);
|
|
19
|
+
}
|
|
20
|
+
if (sxx === 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Regression group "${groupLabel}" requires varying x values.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const slope = sxy / sxx;
|
|
26
|
+
const intercept = meanY - slope * meanX;
|
|
27
|
+
const residualSumSquares = rows.reduce((sum, row) => {
|
|
28
|
+
const residual = row[y] - (intercept + slope * row[x]);
|
|
29
|
+
return sum + residual ** 2;
|
|
30
|
+
}, 0);
|
|
31
|
+
const degreesOfFreedom = count - 2;
|
|
32
|
+
return {
|
|
33
|
+
count,
|
|
34
|
+
degreesOfFreedom,
|
|
35
|
+
meanX,
|
|
36
|
+
meanY,
|
|
37
|
+
sxx,
|
|
38
|
+
slope,
|
|
39
|
+
intercept,
|
|
40
|
+
residualSumSquares,
|
|
41
|
+
residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
|
|
42
|
+
critical: studentTCriticalValue(confidence, degreesOfFreedom)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function solveLinearSystem(matrix, vector) {
|
|
47
|
+
const size = matrix.length;
|
|
48
|
+
const augmented = matrix.map((row, index) => [...row, vector[index]]);
|
|
49
|
+
for (let column = 0; column < size; column += 1) {
|
|
50
|
+
let pivot = column;
|
|
51
|
+
for (let row = column + 1; row < size; row += 1) {
|
|
52
|
+
if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) {
|
|
53
|
+
pivot = row;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (Math.abs(augmented[pivot][column]) < 1e-12) {
|
|
57
|
+
throw new Error("Polynomial regression design is singular.");
|
|
58
|
+
}
|
|
59
|
+
[augmented[column], augmented[pivot]] = [
|
|
60
|
+
augmented[pivot], augmented[column]
|
|
61
|
+
];
|
|
62
|
+
const divisor = augmented[column][column];
|
|
63
|
+
for (let index = column; index <= size; index += 1) {
|
|
64
|
+
augmented[column][index] /= divisor;
|
|
65
|
+
}
|
|
66
|
+
for (let row = 0; row < size; row += 1) {
|
|
67
|
+
if (row === column) continue;
|
|
68
|
+
const factor = augmented[row][column];
|
|
69
|
+
for (let index = column; index <= size; index += 1) {
|
|
70
|
+
augmented[row][index] -= factor * augmented[column][index];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return augmented.map(row => row[size]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function invertSymmetricMatrix(matrix) {
|
|
78
|
+
return matrix.map((_, column) => solveLinearSystem(
|
|
79
|
+
matrix,
|
|
80
|
+
matrix.map((__, row) => row === column ? 1 : 0)
|
|
81
|
+
));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function dot(left, right) {
|
|
85
|
+
return left.reduce((sum, value, index) => sum + value * right[index], 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function binomial(n, k) {
|
|
89
|
+
let result = 1;
|
|
90
|
+
for (let index = 1; index <= k; index += 1) {
|
|
91
|
+
result *= (n - index + 1) / index;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function rawPolynomialCoefficients(coefficients, center, scale) {
|
|
97
|
+
return coefficients.map((_, degree) => coefficients.reduce(
|
|
98
|
+
(sum, coefficient, power) => power < degree
|
|
99
|
+
? sum
|
|
100
|
+
: sum + coefficient * binomial(power, degree) *
|
|
101
|
+
(-center) ** (power - degree) / scale ** power,
|
|
102
|
+
0
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
|
|
107
|
+
const count = rows.length;
|
|
108
|
+
const parameterCount = degree + 1;
|
|
109
|
+
const groupLabel = group === undefined ? "all" : String(group);
|
|
110
|
+
if (
|
|
111
|
+
count < degree + 2 ||
|
|
112
|
+
new Set(rows.map(row => row[x])).size < parameterCount
|
|
113
|
+
) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Polynomial regression group "${groupLabel}" requires at least ` +
|
|
116
|
+
`${degree + 2} rows and ${parameterCount} distinct x values.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const center = rows.reduce((sum, row) => sum + row[x], 0) / count;
|
|
120
|
+
const scale = Math.max(...rows.map(row => Math.abs(row[x] - center)));
|
|
121
|
+
if (!(scale > 0)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Regression group "${groupLabel}" requires varying x values.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const design = rows.map(row => {
|
|
127
|
+
const normalized = (row[x] - center) / scale;
|
|
128
|
+
return Array.from(
|
|
129
|
+
{ length: parameterCount },
|
|
130
|
+
(_, power) => normalized ** power
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
const normal = Array.from({ length: parameterCount }, (_, row) =>
|
|
134
|
+
Array.from({ length: parameterCount }, (_, column) =>
|
|
135
|
+
design.reduce(
|
|
136
|
+
(sum, basis) => sum + basis[row] * basis[column],
|
|
137
|
+
0
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
const response = Array.from({ length: parameterCount }, (_, column) =>
|
|
142
|
+
design.reduce(
|
|
143
|
+
(sum, basis, index) => sum + basis[column] * rows[index][y],
|
|
144
|
+
0
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
const normalizedCoefficients = solveLinearSystem(normal, response);
|
|
148
|
+
const inverse = invertSymmetricMatrix(normal);
|
|
149
|
+
const fitted = design.map(basis => dot(basis, normalizedCoefficients));
|
|
150
|
+
const residualSumSquares = fitted.reduce(
|
|
151
|
+
(sum, value, index) => sum + (rows[index][y] - value) ** 2,
|
|
152
|
+
0
|
|
153
|
+
);
|
|
154
|
+
const degreesOfFreedom = count - parameterCount;
|
|
155
|
+
const residualVariance = residualSumSquares / degreesOfFreedom;
|
|
156
|
+
return {
|
|
157
|
+
count,
|
|
158
|
+
degreesOfFreedom,
|
|
159
|
+
degree,
|
|
160
|
+
coefficients: rawPolynomialCoefficients(
|
|
161
|
+
normalizedCoefficients,
|
|
162
|
+
center,
|
|
163
|
+
scale
|
|
164
|
+
),
|
|
165
|
+
normalizedCoefficients,
|
|
166
|
+
center,
|
|
167
|
+
scale,
|
|
168
|
+
inverse,
|
|
169
|
+
residualSumSquares,
|
|
170
|
+
residualStandardError: Math.sqrt(residualVariance),
|
|
171
|
+
critical: studentTCriticalValue(confidence, degreesOfFreedom)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function evaluatePolynomial(model, xValue) {
|
|
176
|
+
const normalized = (xValue - model.center) / model.scale;
|
|
177
|
+
const basis = model.normalizedCoefficients.map(
|
|
178
|
+
(_, power) => normalized ** power
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
prediction: dot(basis, model.normalizedCoefficients),
|
|
182
|
+
leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function fitLoessGroup(rows, { x, y, group, span }) {
|
|
187
|
+
const groupLabel = group === undefined ? "all" : String(group);
|
|
188
|
+
if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`LOESS regression group "${groupLabel}" requires at least two rows ` +
|
|
191
|
+
"and varying x values."
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const neighborCount = Math.max(2, Math.ceil(span * rows.length));
|
|
195
|
+
const xValues = [...new Set(rows.map(row => row[x]))]
|
|
196
|
+
.sort((left, right) => left - right);
|
|
197
|
+
const fits = xValues.map(xValue => {
|
|
198
|
+
const neighbors = rows
|
|
199
|
+
.map((row, index) => ({
|
|
200
|
+
row,
|
|
201
|
+
index,
|
|
202
|
+
distance: Math.abs(row[x] - xValue)
|
|
203
|
+
}))
|
|
204
|
+
.sort((left, right) =>
|
|
205
|
+
left.distance - right.distance || left.index - right.index
|
|
206
|
+
)
|
|
207
|
+
.slice(0, neighborCount);
|
|
208
|
+
const radius = neighbors.at(-1).distance;
|
|
209
|
+
const weighted = neighbors.map(neighbor => ({
|
|
210
|
+
...neighbor,
|
|
211
|
+
weight: radius === 0
|
|
212
|
+
? 1
|
|
213
|
+
: (1 - (neighbor.distance / radius) ** 3) ** 3
|
|
214
|
+
}));
|
|
215
|
+
const totalWeight = weighted.reduce(
|
|
216
|
+
(sum, item) => sum + item.weight,
|
|
217
|
+
0
|
|
218
|
+
);
|
|
219
|
+
const meanDifference = weighted.reduce(
|
|
220
|
+
(sum, item) => sum + item.weight * (item.row[x] - xValue),
|
|
221
|
+
0
|
|
222
|
+
) / totalWeight;
|
|
223
|
+
const meanY = weighted.reduce(
|
|
224
|
+
(sum, item) => sum + item.weight * item.row[y],
|
|
225
|
+
0
|
|
226
|
+
) / totalWeight;
|
|
227
|
+
let variance = 0;
|
|
228
|
+
let covariance = 0;
|
|
229
|
+
for (const item of weighted) {
|
|
230
|
+
const difference = item.row[x] - xValue - meanDifference;
|
|
231
|
+
variance += item.weight * difference ** 2;
|
|
232
|
+
covariance += item.weight * difference * (item.row[y] - meanY);
|
|
233
|
+
}
|
|
234
|
+
const slope = variance === 0 ? 0 : covariance / variance;
|
|
235
|
+
return {
|
|
236
|
+
x: xValue,
|
|
237
|
+
prediction: meanY - slope * meanDifference,
|
|
238
|
+
neighborIndices: weighted.map(item => item.index)
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
return { count: rows.length, span, neighborCount, fits };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
245
|
+
if (parameters.method === "linear") {
|
|
246
|
+
return fitLinearGroup(rows, {
|
|
247
|
+
x,
|
|
248
|
+
y,
|
|
249
|
+
group,
|
|
250
|
+
confidence: parameters.confidence
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (parameters.method === "polynomial") {
|
|
254
|
+
return fitPolynomialGroup(rows, {
|
|
255
|
+
x,
|
|
256
|
+
y,
|
|
257
|
+
group,
|
|
258
|
+
confidence: parameters.confidence,
|
|
259
|
+
degree: parameters.degree
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return fitLoessGroup(rows, { x, y, group, span: parameters.span });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function predictRegressionAt(model, xValue, parameters) {
|
|
266
|
+
const polynomial = parameters.method === "polynomial"
|
|
267
|
+
? evaluatePolynomial(model, xValue)
|
|
268
|
+
: undefined;
|
|
269
|
+
const prediction = parameters.method === "linear"
|
|
270
|
+
? model.intercept + model.slope * xValue
|
|
271
|
+
: parameters.method === "polynomial"
|
|
272
|
+
? polynomial.prediction
|
|
273
|
+
: model.fits.find(fit => fit.x === xValue).prediction;
|
|
274
|
+
if (parameters.method === "loess") return { prediction };
|
|
275
|
+
const leverage = parameters.method === "linear"
|
|
276
|
+
? 1 / model.count + (xValue - model.meanX) ** 2 / model.sxx
|
|
277
|
+
: polynomial.leverage;
|
|
278
|
+
const standardError = model.residualStandardError * Math.sqrt(
|
|
279
|
+
leverage + (parameters.interval === "prediction" ? 1 : 0)
|
|
280
|
+
);
|
|
281
|
+
return { prediction, margin: model.critical * standardError };
|
|
282
|
+
}
|