ggaction 0.0.3 → 0.0.5
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 +58 -3
- package/README.md +117 -83
- package/package.json +27 -11
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +31 -35
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +8 -1
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +137 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +29 -2
- package/src/actions/data/regression.js +1 -1
- package/src/actions/data/window.js +50 -0
- package/src/actions/encodings/appearance.js +1 -1
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +167 -0
- package/src/actions/encodings/color/layout.js +99 -0
- package/src/actions/encodings/color/policy.js +88 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +193 -88
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -2
- package/src/actions/encodings/offset.js +1 -1
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/{position.js → position/index.js} +21 -8
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +25 -3
- package/src/actions/encodings/ranged.js +5 -3
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/options.js +1 -1
- package/src/actions/errorBars/options.js +1 -1
- package/src/actions/facets/actions.js +2 -3
- package/src/actions/facets/derive.js +35 -5
- package/src/actions/facets/guides.js +1 -1
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +153 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +12 -7
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/ticks.js +1 -1
- package/src/actions/guides/axes/titles.js +12 -9
- package/src/actions/guides/grids/resolve.js +1 -1
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +48 -11
- package/src/actions/guides/legends/categorical/layout.js +4 -2
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +7 -20
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +6 -31
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/strokeWidth.js +170 -0
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
- package/src/actions/guides/polar/index.js +1 -1
- package/src/actions/guides/polar/resolve.js +1 -1
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/index.js +6 -0
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +2 -2
- package/src/actions/marks/area/actions.js +61 -137
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +100 -81
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +85 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
- package/src/actions/marks/rect/actions.js +4 -1
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +26 -4
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +20 -5
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +20 -22
- package/src/actions/scales/consumers/common.js +109 -0
- package/src/actions/scales/consumers/families.js +58 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +32 -2
- package/src/actions/scales/edit.js +8 -206
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +3 -3
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +28 -3
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/ChartProgram.js +22 -236
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +7 -2
- package/src/grammar/arcs.js +50 -13
- package/src/grammar/areaSeries.js +42 -6
- package/src/grammar/bars/aggregate.js +1 -1
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +253 -27
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/{facets.js → facets/index.js} +8 -6
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/interval.js +2 -96
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +70 -1
- package/src/grammar/paint.js +88 -0
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/polarLineCommands.js +1 -1
- 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 +2 -1
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/scales/color.js +4 -2
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/transforms.js +101 -12
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/bars/aggregate.js +1 -1
- package/src/materialization/bars/grouped.js +1 -1
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +15 -101
- package/src/materialization/encodings.js +2 -2
- 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 +1 -1
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +161 -0
- package/src/materialization/marks/index.js +108 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +91 -0
- package/src/materialization/rect.js +1 -1
- package/src/materialization/rowEncoding.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +102 -0
- package/src/materialization/scales/map.js +1 -1
- package/src/materialization/scales/policies/series.js +1 -1
- package/src/materialization/scales/resolve.js +3 -1
- package/src/materialization/selection/items/bar.js +1 -1
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +21 -10
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/renderers/canvas/text.js +14 -1
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +10 -1
- package/types/index.d.ts +76 -0
- package/types/program.d.ts +632 -10
- package/src/actions/encodings/color.js +0 -424
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/facetGuides.js +0 -426
- package/src/materialization/marks.js +0 -316
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
mapContinuousScaleValues,
|
|
6
|
+
mapOrdinalPositionValues,
|
|
7
|
+
validateSemanticFieldType
|
|
8
|
+
} from "./scales/index.js";
|
|
9
|
+
|
|
10
|
+
const DIMENSION_OPTIONS = Object.freeze([
|
|
11
|
+
"field", "fieldType", "title", "scale"
|
|
12
|
+
]);
|
|
13
|
+
const FIELD_TYPES = new Set(["quantitative", "ordinal"]);
|
|
14
|
+
const MISSING_POLICIES = new Set(["break", "drop-row", "error"]);
|
|
15
|
+
const DIMENSION_SCALE_OPTIONS = Object.freeze([
|
|
16
|
+
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
17
|
+
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
18
|
+
"padding", "align", "unknown"
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function nonEmptyString(value, label) {
|
|
22
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
23
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateParallelKeyField(value) {
|
|
29
|
+
return nonEmptyString(value, "Parallel key field");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function inferFieldType(rows, field) {
|
|
33
|
+
const values = rows
|
|
34
|
+
.map(row => row[field])
|
|
35
|
+
.filter(value => value !== undefined && value !== null);
|
|
36
|
+
if (values.length === 0) {
|
|
37
|
+
throw new Error(`Cannot infer Parallel field type for "${field}".`);
|
|
38
|
+
}
|
|
39
|
+
if (values.every(Number.isFinite)) return "quantitative";
|
|
40
|
+
if (values.every(value => typeof value === "string")) return "ordinal";
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Parallel dimension "${field}" requires an explicit compatible fieldType.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateScaleOptions(value, index) {
|
|
47
|
+
if (value === undefined) return {};
|
|
48
|
+
if (!isPlainObject(value)) {
|
|
49
|
+
throw new TypeError(`Parallel dimension ${index} scale must be a plain object.`);
|
|
50
|
+
}
|
|
51
|
+
validateOptionObject(
|
|
52
|
+
value,
|
|
53
|
+
DIMENSION_SCALE_OPTIONS,
|
|
54
|
+
`Parallel dimension ${index} scale`
|
|
55
|
+
);
|
|
56
|
+
return { ...value };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateParallelMissingPolicy(value) {
|
|
60
|
+
if (!MISSING_POLICIES.has(value)) {
|
|
61
|
+
throw new Error(`Unsupported Parallel missing policy "${value}".`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateParallelDimensions(dimensions, { normalized = false } = {}) {
|
|
67
|
+
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
68
|
+
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
69
|
+
}
|
|
70
|
+
const fields = new Set();
|
|
71
|
+
for (const [index, dimension] of dimensions.entries()) {
|
|
72
|
+
if (!isPlainObject(dimension)) {
|
|
73
|
+
throw new TypeError(`Parallel dimension ${index} must be an object.`);
|
|
74
|
+
}
|
|
75
|
+
const supported = normalized
|
|
76
|
+
? ["field", "fieldType", "title", "scale"]
|
|
77
|
+
: DIMENSION_OPTIONS;
|
|
78
|
+
validateOptionObject(dimension, supported, `Parallel dimension ${index}`);
|
|
79
|
+
const field = nonEmptyString(
|
|
80
|
+
dimension.field,
|
|
81
|
+
`Parallel dimension ${index} field`
|
|
82
|
+
);
|
|
83
|
+
if (fields.has(field)) {
|
|
84
|
+
throw new Error("Parallel dimensions must use unique fields.");
|
|
85
|
+
}
|
|
86
|
+
fields.add(field);
|
|
87
|
+
validateSemanticFieldType(dimension.fieldType);
|
|
88
|
+
if (!FIELD_TYPES.has(dimension.fieldType)) {
|
|
89
|
+
throw new Error(`Unsupported Parallel field type "${dimension.fieldType}".`);
|
|
90
|
+
}
|
|
91
|
+
nonEmptyString(dimension.title, `Parallel dimension ${index} title`);
|
|
92
|
+
validateUserId(dimension.scale, `Parallel dimension ${index} scale id`);
|
|
93
|
+
}
|
|
94
|
+
return dimensions;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function normalizeParallelDimensions(rows, dimensions, target) {
|
|
98
|
+
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
99
|
+
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
100
|
+
}
|
|
101
|
+
const normalized = dimensions.map((input, index) => {
|
|
102
|
+
const dimension = typeof input === "string" ? { field: input } : input;
|
|
103
|
+
if (!isPlainObject(dimension)) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`Parallel dimension ${index} must be a field string or plain object.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
validateOptionObject(
|
|
109
|
+
dimension,
|
|
110
|
+
DIMENSION_OPTIONS,
|
|
111
|
+
`Parallel dimension ${index}`
|
|
112
|
+
);
|
|
113
|
+
const field = nonEmptyString(
|
|
114
|
+
dimension.field,
|
|
115
|
+
`Parallel dimension ${index} field`
|
|
116
|
+
);
|
|
117
|
+
const fieldType = dimension.fieldType ?? inferFieldType(rows, field);
|
|
118
|
+
if (!FIELD_TYPES.has(fieldType)) {
|
|
119
|
+
throw new Error(`Unsupported Parallel field type "${fieldType}".`);
|
|
120
|
+
}
|
|
121
|
+
const scale = validateScaleOptions(dimension.scale, index);
|
|
122
|
+
return {
|
|
123
|
+
field,
|
|
124
|
+
fieldType,
|
|
125
|
+
title: dimension.title ?? field,
|
|
126
|
+
scale: `${target}-parallel-${index}`,
|
|
127
|
+
scaleOptions: scale
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
const fields = normalized.map(dimension => dimension.field);
|
|
131
|
+
if (new Set(fields).size !== fields.length) {
|
|
132
|
+
throw new Error("Parallel dimensions must use unique fields.");
|
|
133
|
+
}
|
|
134
|
+
return cloneAndFreeze(normalized);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validDimensionValue(value, fieldType) {
|
|
138
|
+
return fieldType === "quantitative"
|
|
139
|
+
? Number.isFinite(value)
|
|
140
|
+
: typeof value === "string" || Number.isFinite(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function commandsFromVertices(vertices) {
|
|
144
|
+
const commands = [];
|
|
145
|
+
let fragment = [];
|
|
146
|
+
const flush = () => {
|
|
147
|
+
if (fragment.length >= 2) {
|
|
148
|
+
fragment.forEach((point, index) => commands.push({
|
|
149
|
+
op: index === 0 ? "M" : "L",
|
|
150
|
+
x: point.x,
|
|
151
|
+
y: point.y
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
fragment = [];
|
|
155
|
+
};
|
|
156
|
+
for (const vertex of vertices) {
|
|
157
|
+
if (vertex === undefined) flush();
|
|
158
|
+
else fragment.push(vertex);
|
|
159
|
+
}
|
|
160
|
+
flush();
|
|
161
|
+
return commands;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function validateParallelRows(rows, dimensions, { key, missing }) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
rows.forEach((row, index) => {
|
|
167
|
+
if (key !== undefined) {
|
|
168
|
+
const value = row[key];
|
|
169
|
+
if (value === undefined || value === null || value === "") {
|
|
170
|
+
throw new Error(`Parallel key is missing at row ${index}.`);
|
|
171
|
+
}
|
|
172
|
+
const token = `${typeof value}:${String(value)}`;
|
|
173
|
+
if (seen.has(token)) throw new Error(`Duplicate Parallel key "${value}".`);
|
|
174
|
+
seen.add(token);
|
|
175
|
+
}
|
|
176
|
+
const incomplete = dimensions.some(dimension =>
|
|
177
|
+
!validDimensionValue(row[dimension.field], dimension.fieldType)
|
|
178
|
+
);
|
|
179
|
+
if (incomplete && missing === "error") {
|
|
180
|
+
throw new Error(`Parallel row ${index} has a missing dimension.`);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function materializeParallelRows(rows, dimensions, resolvedScales, bounds, {
|
|
186
|
+
key,
|
|
187
|
+
missing
|
|
188
|
+
}) {
|
|
189
|
+
validateParallelRows(rows, dimensions, { key, missing });
|
|
190
|
+
const step = bounds.width / (dimensions.length - 1);
|
|
191
|
+
const items = [];
|
|
192
|
+
rows.forEach((row, sourceRowIndex) => {
|
|
193
|
+
const vertices = dimensions.map((dimension, index) => {
|
|
194
|
+
const value = row[dimension.field];
|
|
195
|
+
if (!validDimensionValue(value, dimension.fieldType)) return undefined;
|
|
196
|
+
const scale = resolvedScales[dimension.scale];
|
|
197
|
+
const mapped = dimension.fieldType === "quantitative"
|
|
198
|
+
? mapContinuousScaleValues([value], scale)[0]
|
|
199
|
+
: mapOrdinalPositionValues([value], scale)[0];
|
|
200
|
+
return Number.isFinite(mapped)
|
|
201
|
+
? { x: bounds.x + step * index, y: mapped, value, dimension: dimension.field }
|
|
202
|
+
: undefined;
|
|
203
|
+
});
|
|
204
|
+
const incomplete = vertices.some(vertex => vertex === undefined);
|
|
205
|
+
if (incomplete && missing === "drop-row") return;
|
|
206
|
+
items.push({
|
|
207
|
+
key: key === undefined ? `source:${sourceRowIndex}` : row[key],
|
|
208
|
+
sourceRowIndex,
|
|
209
|
+
commands: commandsFromVertices(vertices)
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
return cloneAndFreeze(items);
|
|
213
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const PATH_ORDER_DIRECTIONS = Object.freeze([
|
|
4
|
+
"ascending",
|
|
5
|
+
"descending"
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function validatePathOrderDirection(value) {
|
|
9
|
+
if (!PATH_ORDER_DIRECTIONS.includes(value)) {
|
|
10
|
+
throw new Error(`Unsupported path order "${value}".`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function stableOrderPathValues(values, orderValues, order = "ascending") {
|
|
16
|
+
validatePathOrderDirection(order);
|
|
17
|
+
if (!Array.isArray(values) || !Array.isArray(orderValues)) {
|
|
18
|
+
throw new TypeError("Path ordering requires value and order arrays.");
|
|
19
|
+
}
|
|
20
|
+
if (values.length !== orderValues.length) {
|
|
21
|
+
throw new Error("Path value and order arrays must have equal length.");
|
|
22
|
+
}
|
|
23
|
+
const direction = order === "ascending" ? 1 : -1;
|
|
24
|
+
const ordered = values.map((value, sourceIndex) => {
|
|
25
|
+
const orderValue = orderValues[sourceIndex];
|
|
26
|
+
if (!Number.isFinite(orderValue)) {
|
|
27
|
+
throw new TypeError("Path order values must be finite numbers.");
|
|
28
|
+
}
|
|
29
|
+
return { value, orderValue, sourceIndex };
|
|
30
|
+
}).sort((left, right) =>
|
|
31
|
+
direction * (left.orderValue - right.orderValue) ||
|
|
32
|
+
left.sourceIndex - right.sourceIndex
|
|
33
|
+
);
|
|
34
|
+
return cloneAndFreeze(ordered.map(item => item.value));
|
|
35
|
+
}
|
|
@@ -162,3 +162,27 @@ export function createPointShapeGraphic({
|
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
export function resolvePointShapeExtent({ shape, area, strokeWidth = 0 }) {
|
|
167
|
+
const validated = validatePointShape(shape);
|
|
168
|
+
if (!Number.isFinite(area) || area < 0) {
|
|
169
|
+
throw new TypeError("Point shape area must be finite and non-negative.");
|
|
170
|
+
}
|
|
171
|
+
if (!Number.isFinite(strokeWidth) || strokeWidth < 0) {
|
|
172
|
+
throw new TypeError("Point shape strokeWidth must be finite and non-negative.");
|
|
173
|
+
}
|
|
174
|
+
const strokeExtent = strokeWidth / 2;
|
|
175
|
+
if (validated === "circle") {
|
|
176
|
+
const radius = Math.sqrt(area / Math.PI) + strokeExtent;
|
|
177
|
+
return cloneAndFreeze({ x: radius, y: radius });
|
|
178
|
+
}
|
|
179
|
+
if (validated === "square") {
|
|
180
|
+
const halfSide = Math.sqrt(area) / 2 + strokeExtent;
|
|
181
|
+
return cloneAndFreeze({ x: halfSide, y: halfSide });
|
|
182
|
+
}
|
|
183
|
+
const points = normalizePolygon(shapePolygon(validated), 0, 0, area);
|
|
184
|
+
return cloneAndFreeze({
|
|
185
|
+
x: Math.max(0, ...points.map(point => Math.abs(point.x))) + strokeExtent,
|
|
186
|
+
y: Math.max(0, ...points.map(point => Math.abs(point.y))) + strokeExtent
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -3,7 +3,7 @@ import { polarToCartesian } from "./polar.js";
|
|
|
3
3
|
import {
|
|
4
4
|
mapContinuousScaleValues,
|
|
5
5
|
mapOrdinalPositionValues
|
|
6
|
-
} from "./scales.js";
|
|
6
|
+
} from "./scales/index.js";
|
|
7
7
|
|
|
8
8
|
function mappedTheta(values, fieldType, scale) {
|
|
9
9
|
return ["nominal", "ordinal"].includes(fieldType)
|
|
@@ -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
|
+
}
|