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,578 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
readNominalField,
|
|
4
|
+
readQuantitativeField
|
|
5
|
+
} from "./scales.js";
|
|
6
|
+
|
|
7
|
+
export const REGRESSION_LOWER_FIELD = "__regression_ci_lower";
|
|
8
|
+
export const REGRESSION_UPPER_FIELD = "__regression_ci_upper";
|
|
9
|
+
|
|
10
|
+
function requireField(field, label) {
|
|
11
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
12
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
13
|
+
}
|
|
14
|
+
return field;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateRegressionTransform(transform) {
|
|
18
|
+
const supported = [
|
|
19
|
+
"type", "method", "x", "y", "groupBy", "confidence", "interval",
|
|
20
|
+
"degree", "span"
|
|
21
|
+
];
|
|
22
|
+
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
23
|
+
if (unknown !== undefined) {
|
|
24
|
+
throw new Error(`Unknown regression transform property "${unknown}".`);
|
|
25
|
+
}
|
|
26
|
+
if (transform.type !== "regression") {
|
|
27
|
+
throw new Error(`Unsupported regression transform "${transform.type}".`);
|
|
28
|
+
}
|
|
29
|
+
requireField(transform.x, "Regression x field");
|
|
30
|
+
requireField(transform.y, "Regression y field");
|
|
31
|
+
if (transform.groupBy !== undefined) {
|
|
32
|
+
requireField(transform.groupBy, "Regression groupBy field");
|
|
33
|
+
}
|
|
34
|
+
const normalized = normalizeRegressionParameters({
|
|
35
|
+
method: transform.method,
|
|
36
|
+
degree: transform.degree,
|
|
37
|
+
span: transform.span,
|
|
38
|
+
confidence: transform.confidence,
|
|
39
|
+
interval: transform.interval
|
|
40
|
+
});
|
|
41
|
+
if (normalized.method === "loess") return transform;
|
|
42
|
+
if (!Number.isFinite(transform.confidence)) {
|
|
43
|
+
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
44
|
+
}
|
|
45
|
+
if (transform.interval === undefined) {
|
|
46
|
+
throw new Error(`Unsupported regression interval "${transform.interval}".`);
|
|
47
|
+
}
|
|
48
|
+
return transform;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function logGamma(value) {
|
|
52
|
+
const coefficients = [
|
|
53
|
+
676.5203681218851,
|
|
54
|
+
-1259.1392167224028,
|
|
55
|
+
771.3234287776531,
|
|
56
|
+
-176.6150291621406,
|
|
57
|
+
12.507343278686905,
|
|
58
|
+
-0.13857109526572012,
|
|
59
|
+
9.984369578019572e-6,
|
|
60
|
+
1.5056327351493116e-7
|
|
61
|
+
];
|
|
62
|
+
if (value < 0.5) {
|
|
63
|
+
return Math.log(Math.PI) -
|
|
64
|
+
Math.log(Math.sin(Math.PI * value)) -
|
|
65
|
+
logGamma(1 - value);
|
|
66
|
+
}
|
|
67
|
+
const shifted = value - 1;
|
|
68
|
+
let series = 0.9999999999998099;
|
|
69
|
+
for (let index = 0; index < coefficients.length; index += 1) {
|
|
70
|
+
series += coefficients[index] / (shifted + index + 1);
|
|
71
|
+
}
|
|
72
|
+
const base = shifted + coefficients.length - 0.5;
|
|
73
|
+
return 0.5 * Math.log(2 * Math.PI) +
|
|
74
|
+
(shifted + 0.5) * Math.log(base) - base + Math.log(series);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function betaContinuedFraction(a, b, x) {
|
|
78
|
+
const maxIterations = 200;
|
|
79
|
+
const epsilon = 3e-14;
|
|
80
|
+
const minimum = 1e-300;
|
|
81
|
+
const sum = a + b;
|
|
82
|
+
const aPlusOne = a + 1;
|
|
83
|
+
const aMinusOne = a - 1;
|
|
84
|
+
let c = 1;
|
|
85
|
+
let d = 1 - sum * x / aPlusOne;
|
|
86
|
+
if (Math.abs(d) < minimum) d = minimum;
|
|
87
|
+
d = 1 / d;
|
|
88
|
+
let result = d;
|
|
89
|
+
|
|
90
|
+
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
91
|
+
const even = iteration * 2;
|
|
92
|
+
let numerator = iteration * (b - iteration) * x /
|
|
93
|
+
((aMinusOne + even) * (a + even));
|
|
94
|
+
d = 1 + numerator * d;
|
|
95
|
+
if (Math.abs(d) < minimum) d = minimum;
|
|
96
|
+
c = 1 + numerator / c;
|
|
97
|
+
if (Math.abs(c) < minimum) c = minimum;
|
|
98
|
+
d = 1 / d;
|
|
99
|
+
result *= d * c;
|
|
100
|
+
|
|
101
|
+
numerator = -(a + iteration) * (sum + iteration) * x /
|
|
102
|
+
((a + even) * (aPlusOne + even));
|
|
103
|
+
d = 1 + numerator * d;
|
|
104
|
+
if (Math.abs(d) < minimum) d = minimum;
|
|
105
|
+
c = 1 + numerator / c;
|
|
106
|
+
if (Math.abs(c) < minimum) c = minimum;
|
|
107
|
+
d = 1 / d;
|
|
108
|
+
const delta = d * c;
|
|
109
|
+
result *= delta;
|
|
110
|
+
if (Math.abs(delta - 1) <= epsilon) return result;
|
|
111
|
+
}
|
|
112
|
+
throw new Error("Student-t calculation did not converge.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function regularizedIncompleteBeta(value, a, b) {
|
|
116
|
+
if (value === 0 || value === 1) return value;
|
|
117
|
+
const factor = Math.exp(
|
|
118
|
+
logGamma(a + b) - logGamma(a) - logGamma(b) +
|
|
119
|
+
a * Math.log(value) + b * Math.log1p(-value)
|
|
120
|
+
);
|
|
121
|
+
if (value < (a + 1) / (a + b + 2)) {
|
|
122
|
+
return factor * betaContinuedFraction(a, b, value) / a;
|
|
123
|
+
}
|
|
124
|
+
return 1 - factor * betaContinuedFraction(b, a, 1 - value) / b;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function studentTCdf(value, degreesOfFreedom) {
|
|
128
|
+
if (value === 0) return 0.5;
|
|
129
|
+
const ratio = degreesOfFreedom /
|
|
130
|
+
(degreesOfFreedom + value * value);
|
|
131
|
+
const tail = regularizedIncompleteBeta(
|
|
132
|
+
ratio,
|
|
133
|
+
degreesOfFreedom / 2,
|
|
134
|
+
0.5
|
|
135
|
+
) / 2;
|
|
136
|
+
return value > 0 ? 1 - tail : tail;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function studentTCritical(confidence, degreesOfFreedom) {
|
|
140
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {
|
|
141
|
+
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom <= 0) {
|
|
144
|
+
throw new RangeError("Student-t degrees of freedom must be positive.");
|
|
145
|
+
}
|
|
146
|
+
const probability = (1 + confidence) / 2;
|
|
147
|
+
let low = 0;
|
|
148
|
+
let high = 1;
|
|
149
|
+
while (studentTCdf(high, degreesOfFreedom) < probability) high *= 2;
|
|
150
|
+
for (let iteration = 0; iteration < 100; iteration += 1) {
|
|
151
|
+
const midpoint = (low + high) / 2;
|
|
152
|
+
if (studentTCdf(midpoint, degreesOfFreedom) < probability) low = midpoint;
|
|
153
|
+
else high = midpoint;
|
|
154
|
+
}
|
|
155
|
+
return (low + high) / 2;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function normalizeRegressionParameters({
|
|
159
|
+
method = "linear",
|
|
160
|
+
degree,
|
|
161
|
+
span,
|
|
162
|
+
confidence,
|
|
163
|
+
interval
|
|
164
|
+
} = {}) {
|
|
165
|
+
if (!["linear", "polynomial", "loess"].includes(method)) {
|
|
166
|
+
throw new Error(`Unsupported regression method "${method}".`);
|
|
167
|
+
}
|
|
168
|
+
if (method === "loess") {
|
|
169
|
+
if (degree !== undefined) {
|
|
170
|
+
throw new Error("Regression degree requires the polynomial method.");
|
|
171
|
+
}
|
|
172
|
+
if (confidence !== undefined || interval !== undefined) {
|
|
173
|
+
throw new Error("LOESS regression does not support confidence intervals.");
|
|
174
|
+
}
|
|
175
|
+
const resolvedSpan = span ?? 0.75;
|
|
176
|
+
if (!Number.isFinite(resolvedSpan) || resolvedSpan <= 0 || resolvedSpan > 1) {
|
|
177
|
+
throw new RangeError(
|
|
178
|
+
"Regression LOESS span must be greater than zero and at most one."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return cloneAndFreeze({ method, span: resolvedSpan });
|
|
182
|
+
}
|
|
183
|
+
if (span !== undefined) {
|
|
184
|
+
throw new Error("Regression span requires the loess method.");
|
|
185
|
+
}
|
|
186
|
+
const resolvedConfidence = confidence ?? 0.95;
|
|
187
|
+
if (
|
|
188
|
+
!Number.isFinite(resolvedConfidence) ||
|
|
189
|
+
resolvedConfidence <= 0 ||
|
|
190
|
+
resolvedConfidence >= 1
|
|
191
|
+
) {
|
|
192
|
+
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
193
|
+
}
|
|
194
|
+
const resolvedInterval = interval ?? "mean";
|
|
195
|
+
if (!["mean", "prediction"].includes(resolvedInterval)) {
|
|
196
|
+
throw new Error(`Unsupported regression interval "${resolvedInterval}".`);
|
|
197
|
+
}
|
|
198
|
+
if (method === "polynomial") {
|
|
199
|
+
const resolvedDegree = degree ?? 2;
|
|
200
|
+
if (!Number.isInteger(resolvedDegree) || resolvedDegree < 1) {
|
|
201
|
+
throw new RangeError(
|
|
202
|
+
"Regression polynomial degree must be a positive integer."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return cloneAndFreeze({
|
|
206
|
+
method,
|
|
207
|
+
degree: resolvedDegree,
|
|
208
|
+
confidence: resolvedConfidence,
|
|
209
|
+
interval: resolvedInterval
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (degree !== undefined) {
|
|
213
|
+
throw new Error("Regression degree requires the polynomial method.");
|
|
214
|
+
}
|
|
215
|
+
return cloneAndFreeze({
|
|
216
|
+
method,
|
|
217
|
+
confidence: resolvedConfidence,
|
|
218
|
+
interval: resolvedInterval
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function fitLinearGroup(rows, { x, y, group, confidence }) {
|
|
223
|
+
const count = rows.length;
|
|
224
|
+
const groupLabel = group === undefined ? "all" : String(group);
|
|
225
|
+
if (count < 3) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Regression group "${groupLabel}" requires at least three rows.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const meanX = rows.reduce((sum, row) => sum + row[x], 0) / count;
|
|
231
|
+
const meanY = rows.reduce((sum, row) => sum + row[y], 0) / count;
|
|
232
|
+
let sxx = 0;
|
|
233
|
+
let sxy = 0;
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
const xDifference = row[x] - meanX;
|
|
236
|
+
sxx += xDifference ** 2;
|
|
237
|
+
sxy += xDifference * (row[y] - meanY);
|
|
238
|
+
}
|
|
239
|
+
if (sxx === 0) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Regression group "${groupLabel}" requires varying x values.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const slope = sxy / sxx;
|
|
245
|
+
const intercept = meanY - slope * meanX;
|
|
246
|
+
const residualSumSquares = rows.reduce((sum, row) => {
|
|
247
|
+
const residual = row[y] - (intercept + slope * row[x]);
|
|
248
|
+
return sum + residual ** 2;
|
|
249
|
+
}, 0);
|
|
250
|
+
const degreesOfFreedom = count - 2;
|
|
251
|
+
return {
|
|
252
|
+
count,
|
|
253
|
+
degreesOfFreedom,
|
|
254
|
+
meanX,
|
|
255
|
+
meanY,
|
|
256
|
+
sxx,
|
|
257
|
+
slope,
|
|
258
|
+
intercept,
|
|
259
|
+
residualSumSquares,
|
|
260
|
+
residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
|
|
261
|
+
critical: studentTCritical(confidence, degreesOfFreedom)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function solveLinearSystem(matrix, vector) {
|
|
266
|
+
const size = matrix.length;
|
|
267
|
+
const augmented = matrix.map((row, index) => [...row, vector[index]]);
|
|
268
|
+
for (let column = 0; column < size; column += 1) {
|
|
269
|
+
let pivot = column;
|
|
270
|
+
for (let row = column + 1; row < size; row += 1) {
|
|
271
|
+
if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) {
|
|
272
|
+
pivot = row;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (Math.abs(augmented[pivot][column]) < 1e-12) {
|
|
276
|
+
throw new Error("Polynomial regression design is singular.");
|
|
277
|
+
}
|
|
278
|
+
[augmented[column], augmented[pivot]] = [
|
|
279
|
+
augmented[pivot], augmented[column]
|
|
280
|
+
];
|
|
281
|
+
const divisor = augmented[column][column];
|
|
282
|
+
for (let index = column; index <= size; index += 1) {
|
|
283
|
+
augmented[column][index] /= divisor;
|
|
284
|
+
}
|
|
285
|
+
for (let row = 0; row < size; row += 1) {
|
|
286
|
+
if (row === column) continue;
|
|
287
|
+
const factor = augmented[row][column];
|
|
288
|
+
for (let index = column; index <= size; index += 1) {
|
|
289
|
+
augmented[row][index] -= factor * augmented[column][index];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return augmented.map(row => row[size]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function invertSymmetricMatrix(matrix) {
|
|
297
|
+
return matrix.map((_, column) => solveLinearSystem(
|
|
298
|
+
matrix,
|
|
299
|
+
matrix.map((__, row) => row === column ? 1 : 0)
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function dot(left, right) {
|
|
304
|
+
return left.reduce((sum, value, index) => sum + value * right[index], 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function binomial(n, k) {
|
|
308
|
+
let result = 1;
|
|
309
|
+
for (let index = 1; index <= k; index += 1) {
|
|
310
|
+
result *= (n - index + 1) / index;
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function rawPolynomialCoefficients(coefficients, center, scale) {
|
|
316
|
+
return coefficients.map((_, degree) => coefficients.reduce(
|
|
317
|
+
(sum, coefficient, power) => power < degree
|
|
318
|
+
? sum
|
|
319
|
+
: sum + coefficient * binomial(power, degree) *
|
|
320
|
+
(-center) ** (power - degree) / scale ** power,
|
|
321
|
+
0
|
|
322
|
+
));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
|
|
326
|
+
const count = rows.length;
|
|
327
|
+
const parameterCount = degree + 1;
|
|
328
|
+
const groupLabel = group === undefined ? "all" : String(group);
|
|
329
|
+
if (
|
|
330
|
+
count < degree + 2 ||
|
|
331
|
+
new Set(rows.map(row => row[x])).size < parameterCount
|
|
332
|
+
) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Polynomial regression group "${groupLabel}" requires at least ` +
|
|
335
|
+
`${degree + 2} rows and ${parameterCount} distinct x values.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const center = rows.reduce((sum, row) => sum + row[x], 0) / count;
|
|
339
|
+
const scale = Math.max(...rows.map(row => Math.abs(row[x] - center)));
|
|
340
|
+
if (!(scale > 0)) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Regression group "${groupLabel}" requires varying x values.`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const design = rows.map(row => {
|
|
346
|
+
const normalized = (row[x] - center) / scale;
|
|
347
|
+
return Array.from(
|
|
348
|
+
{ length: parameterCount },
|
|
349
|
+
(_, power) => normalized ** power
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
const normal = Array.from({ length: parameterCount }, (_, row) =>
|
|
353
|
+
Array.from({ length: parameterCount }, (_, column) =>
|
|
354
|
+
design.reduce(
|
|
355
|
+
(sum, basis) => sum + basis[row] * basis[column],
|
|
356
|
+
0
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
const response = Array.from({ length: parameterCount }, (_, column) =>
|
|
361
|
+
design.reduce(
|
|
362
|
+
(sum, basis, index) => sum + basis[column] * rows[index][y],
|
|
363
|
+
0
|
|
364
|
+
)
|
|
365
|
+
);
|
|
366
|
+
const normalizedCoefficients = solveLinearSystem(normal, response);
|
|
367
|
+
const inverse = invertSymmetricMatrix(normal);
|
|
368
|
+
const fitted = design.map(basis => dot(basis, normalizedCoefficients));
|
|
369
|
+
const residualSumSquares = fitted.reduce(
|
|
370
|
+
(sum, value, index) => sum + (rows[index][y] - value) ** 2,
|
|
371
|
+
0
|
|
372
|
+
);
|
|
373
|
+
const degreesOfFreedom = count - parameterCount;
|
|
374
|
+
const residualVariance = residualSumSquares / degreesOfFreedom;
|
|
375
|
+
return {
|
|
376
|
+
count,
|
|
377
|
+
degreesOfFreedom,
|
|
378
|
+
degree,
|
|
379
|
+
coefficients: rawPolynomialCoefficients(
|
|
380
|
+
normalizedCoefficients,
|
|
381
|
+
center,
|
|
382
|
+
scale
|
|
383
|
+
),
|
|
384
|
+
normalizedCoefficients,
|
|
385
|
+
center,
|
|
386
|
+
scale,
|
|
387
|
+
inverse,
|
|
388
|
+
residualSumSquares,
|
|
389
|
+
residualStandardError: Math.sqrt(residualVariance),
|
|
390
|
+
critical: studentTCritical(confidence, degreesOfFreedom)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function evaluatePolynomial(model, xValue) {
|
|
395
|
+
const normalized = (xValue - model.center) / model.scale;
|
|
396
|
+
const basis = model.normalizedCoefficients.map(
|
|
397
|
+
(_, power) => normalized ** power
|
|
398
|
+
);
|
|
399
|
+
return {
|
|
400
|
+
prediction: dot(basis, model.normalizedCoefficients),
|
|
401
|
+
leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function fitLoessGroup(rows, { x, y, group, span }) {
|
|
406
|
+
const groupLabel = group === undefined ? "all" : String(group);
|
|
407
|
+
if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`LOESS regression group "${groupLabel}" requires at least two rows ` +
|
|
410
|
+
"and varying x values."
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const neighborCount = Math.max(2, Math.ceil(span * rows.length));
|
|
414
|
+
const xValues = [...new Set(rows.map(row => row[x]))]
|
|
415
|
+
.sort((left, right) => left - right);
|
|
416
|
+
const fits = xValues.map(xValue => {
|
|
417
|
+
const neighbors = rows
|
|
418
|
+
.map((row, index) => ({
|
|
419
|
+
row,
|
|
420
|
+
index,
|
|
421
|
+
distance: Math.abs(row[x] - xValue)
|
|
422
|
+
}))
|
|
423
|
+
.sort((left, right) =>
|
|
424
|
+
left.distance - right.distance || left.index - right.index
|
|
425
|
+
)
|
|
426
|
+
.slice(0, neighborCount);
|
|
427
|
+
const radius = neighbors.at(-1).distance;
|
|
428
|
+
const weighted = neighbors.map(neighbor => ({
|
|
429
|
+
...neighbor,
|
|
430
|
+
weight: radius === 0
|
|
431
|
+
? 1
|
|
432
|
+
: (1 - (neighbor.distance / radius) ** 3) ** 3
|
|
433
|
+
}));
|
|
434
|
+
const totalWeight = weighted.reduce(
|
|
435
|
+
(sum, item) => sum + item.weight,
|
|
436
|
+
0
|
|
437
|
+
);
|
|
438
|
+
const meanDifference = weighted.reduce(
|
|
439
|
+
(sum, item) => sum + item.weight * (item.row[x] - xValue),
|
|
440
|
+
0
|
|
441
|
+
) / totalWeight;
|
|
442
|
+
const meanY = weighted.reduce(
|
|
443
|
+
(sum, item) => sum + item.weight * item.row[y],
|
|
444
|
+
0
|
|
445
|
+
) / totalWeight;
|
|
446
|
+
let variance = 0;
|
|
447
|
+
let covariance = 0;
|
|
448
|
+
for (const item of weighted) {
|
|
449
|
+
const difference = item.row[x] - xValue - meanDifference;
|
|
450
|
+
variance += item.weight * difference ** 2;
|
|
451
|
+
covariance += item.weight * difference * (item.row[y] - meanY);
|
|
452
|
+
}
|
|
453
|
+
const slope = variance === 0 ? 0 : covariance / variance;
|
|
454
|
+
return {
|
|
455
|
+
x: xValue,
|
|
456
|
+
prediction: meanY - slope * meanDifference,
|
|
457
|
+
neighborIndices: weighted.map(item => item.index)
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
return { count: rows.length, span, neighborCount, fits };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function deriveRegression(values, {
|
|
464
|
+
x,
|
|
465
|
+
y,
|
|
466
|
+
groupBy,
|
|
467
|
+
method,
|
|
468
|
+
degree,
|
|
469
|
+
span,
|
|
470
|
+
confidence,
|
|
471
|
+
interval
|
|
472
|
+
} = {}) {
|
|
473
|
+
if (!Array.isArray(values)) {
|
|
474
|
+
throw new TypeError("Regression values must be an array.");
|
|
475
|
+
}
|
|
476
|
+
requireField(x, "Regression x field");
|
|
477
|
+
requireField(y, "Regression y field");
|
|
478
|
+
if (groupBy !== undefined) requireField(groupBy, "Regression groupBy field");
|
|
479
|
+
const parameters = normalizeRegressionParameters({
|
|
480
|
+
method,
|
|
481
|
+
degree,
|
|
482
|
+
span,
|
|
483
|
+
confidence,
|
|
484
|
+
interval
|
|
485
|
+
});
|
|
486
|
+
readQuantitativeField(values, x);
|
|
487
|
+
readQuantitativeField(values, y);
|
|
488
|
+
if (groupBy !== undefined) readNominalField(values, groupBy);
|
|
489
|
+
|
|
490
|
+
const groups = groupBy === undefined
|
|
491
|
+
? [undefined]
|
|
492
|
+
: [...new Set(values.map(row => row[groupBy]))];
|
|
493
|
+
const models = [];
|
|
494
|
+
const rows = [];
|
|
495
|
+
|
|
496
|
+
for (const group of groups) {
|
|
497
|
+
const groupRows = groupBy === undefined
|
|
498
|
+
? values
|
|
499
|
+
: values.filter(row => row[groupBy] === group);
|
|
500
|
+
const model = parameters.method === "linear"
|
|
501
|
+
? fitLinearGroup(groupRows, {
|
|
502
|
+
x,
|
|
503
|
+
y,
|
|
504
|
+
group,
|
|
505
|
+
confidence: parameters.confidence
|
|
506
|
+
})
|
|
507
|
+
: parameters.method === "polynomial"
|
|
508
|
+
? fitPolynomialGroup(groupRows, {
|
|
509
|
+
x,
|
|
510
|
+
y,
|
|
511
|
+
group,
|
|
512
|
+
confidence: parameters.confidence,
|
|
513
|
+
degree: parameters.degree
|
|
514
|
+
})
|
|
515
|
+
: fitLoessGroup(groupRows, {
|
|
516
|
+
x,
|
|
517
|
+
y,
|
|
518
|
+
group,
|
|
519
|
+
span: parameters.span
|
|
520
|
+
});
|
|
521
|
+
const xValues = [...new Set(groupRows.map(row => row[x]))]
|
|
522
|
+
.sort((left, right) => left - right);
|
|
523
|
+
models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
|
|
524
|
+
|
|
525
|
+
for (const xValue of xValues) {
|
|
526
|
+
const polynomial = parameters.method === "polynomial"
|
|
527
|
+
? evaluatePolynomial(model, xValue)
|
|
528
|
+
: undefined;
|
|
529
|
+
const prediction = parameters.method === "linear"
|
|
530
|
+
? model.intercept + model.slope * xValue
|
|
531
|
+
: parameters.method === "polynomial"
|
|
532
|
+
? polynomial.prediction
|
|
533
|
+
: model.fits.find(fit => fit.x === xValue).prediction;
|
|
534
|
+
const leverage = parameters.method === "linear"
|
|
535
|
+
? 1 / model.count + (xValue - model.meanX) ** 2 / model.sxx
|
|
536
|
+
: parameters.method === "polynomial"
|
|
537
|
+
? polynomial.leverage
|
|
538
|
+
: undefined;
|
|
539
|
+
const standardError = parameters.method === "loess"
|
|
540
|
+
? undefined
|
|
541
|
+
: model.residualStandardError * Math.sqrt(
|
|
542
|
+
leverage + (parameters.interval === "prediction" ? 1 : 0)
|
|
543
|
+
);
|
|
544
|
+
const margin = parameters.method === "loess"
|
|
545
|
+
? undefined
|
|
546
|
+
: model.critical * standardError;
|
|
547
|
+
rows.push({
|
|
548
|
+
...(groupBy === undefined ? {} : { [groupBy]: group }),
|
|
549
|
+
[x]: xValue,
|
|
550
|
+
[y]: prediction,
|
|
551
|
+
...(parameters.method === "loess" ? {} : {
|
|
552
|
+
[REGRESSION_LOWER_FIELD]: prediction - margin,
|
|
553
|
+
[REGRESSION_UPPER_FIELD]: prediction + margin
|
|
554
|
+
})
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return cloneAndFreeze({
|
|
560
|
+
fields: {
|
|
561
|
+
x,
|
|
562
|
+
y,
|
|
563
|
+
...(groupBy === undefined ? {} : { group: groupBy }),
|
|
564
|
+
...(parameters.method === "loess" ? {} : {
|
|
565
|
+
lower: REGRESSION_LOWER_FIELD,
|
|
566
|
+
upper: REGRESSION_UPPER_FIELD
|
|
567
|
+
})
|
|
568
|
+
},
|
|
569
|
+
parameters,
|
|
570
|
+
groups,
|
|
571
|
+
models,
|
|
572
|
+
values: rows
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function deriveLinearRegression(values, options = {}) {
|
|
577
|
+
return deriveRegression(values, { ...options, method: "linear" });
|
|
578
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateNonEmptyString,
|
|
3
|
+
validateNonNegativeFinite
|
|
4
|
+
} from "../core/validation.js";
|
|
5
|
+
|
|
6
|
+
export function validateRuleStroke(value, label = "encodeStroke") {
|
|
7
|
+
return validateNonEmptyString(value, label);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateRuleStrokeWidth(value, label = "encodeStrokeWidth") {
|
|
11
|
+
return validateNonNegativeFinite(value, label);
|
|
12
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
isNominalValue,
|
|
4
|
+
normalizeTemporalValue,
|
|
5
|
+
readNominalField,
|
|
6
|
+
readQuantitativeField,
|
|
7
|
+
readTemporalField,
|
|
8
|
+
validateSemanticFieldType
|
|
9
|
+
} from "./scales.js";
|
|
10
|
+
|
|
11
|
+
const ENDPOINT_CHANNELS = Object.freeze(["x", "y", "x2", "y2"]);
|
|
12
|
+
const VALUE_CHANNELS = Object.freeze([
|
|
13
|
+
...ENDPOINT_CHANNELS,
|
|
14
|
+
"strokeDash",
|
|
15
|
+
"opacity"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const RULE_MODES = Object.freeze({
|
|
19
|
+
x: "vertical-span",
|
|
20
|
+
y: "horizontal-span",
|
|
21
|
+
"x,y,y2": "vertical-interval",
|
|
22
|
+
"x,y,x2": "horizontal-interval",
|
|
23
|
+
"x,y,x2,y2": "diagonal-interval"
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function resolveRuleMode(layer) {
|
|
27
|
+
if (layer?.mark?.type !== "rule") return undefined;
|
|
28
|
+
const signature = ENDPOINT_CHANNELS
|
|
29
|
+
.filter(channel => layer.encoding?.[channel] !== undefined)
|
|
30
|
+
.join(",");
|
|
31
|
+
return RULE_MODES[signature];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readField(rows, encoding) {
|
|
35
|
+
if (["nominal", "ordinal"].includes(encoding.fieldType)) {
|
|
36
|
+
return readNominalField(rows, encoding.field);
|
|
37
|
+
}
|
|
38
|
+
if (encoding.fieldType === "temporal") {
|
|
39
|
+
return readTemporalField(rows, encoding.field);
|
|
40
|
+
}
|
|
41
|
+
return readQuantitativeField(rows, encoding.field);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeRuleDatum(value, fieldType, channel) {
|
|
45
|
+
validateSemanticFieldType(fieldType);
|
|
46
|
+
if (fieldType === "quantitative") {
|
|
47
|
+
if (!Number.isFinite(value)) {
|
|
48
|
+
throw new TypeError(`Rule ${channel} datum must be a finite number.`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
if (fieldType === "temporal") {
|
|
53
|
+
return normalizeTemporalValue(value, `${channel} datum`, 0);
|
|
54
|
+
}
|
|
55
|
+
if (!isNominalValue(value)) {
|
|
56
|
+
throw new TypeError(`Rule ${channel} datum must be a nominal value.`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function deriveRuleValues(rows, layer) {
|
|
62
|
+
const encodings = Object.fromEntries(
|
|
63
|
+
VALUE_CHANNELS
|
|
64
|
+
.map(channel => [channel, layer.encoding?.[channel]])
|
|
65
|
+
.filter(([channel, encoding]) =>
|
|
66
|
+
encoding !== undefined &&
|
|
67
|
+
(ENDPOINT_CHANNELS.includes(channel) || Object.hasOwn(encoding, "field"))
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
const hasField = Object.values(encodings).some(encoding =>
|
|
71
|
+
Object.hasOwn(encoding, "field")
|
|
72
|
+
);
|
|
73
|
+
const length = hasField ? rows.length : 1;
|
|
74
|
+
const values = {};
|
|
75
|
+
|
|
76
|
+
for (const [channel, encoding] of Object.entries(encodings)) {
|
|
77
|
+
if (Object.hasOwn(encoding, "field")) {
|
|
78
|
+
values[channel] = readField(rows, encoding);
|
|
79
|
+
} else {
|
|
80
|
+
const datum = normalizeRuleDatum(
|
|
81
|
+
encoding.datum,
|
|
82
|
+
encoding.fieldType,
|
|
83
|
+
channel
|
|
84
|
+
);
|
|
85
|
+
values[channel] = Array.from({ length }, () => datum);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return cloneAndFreeze({ length, values });
|
|
90
|
+
}
|