ggaction 0.0.9 → 0.0.11
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 +84 -0
- package/README.md +5 -4
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/extension-authoring.md +248 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +19 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +8 -24
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +24 -5
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +78 -27
- package/src/actions/errorBars/edit.js +81 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +5 -3
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +53 -53
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/extension.js +8 -1
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +58 -11
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +117 -61
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +10 -14
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +2 -1
- package/src/materialization/facetGuides/placement.js +34 -19
- package/src/materialization/facetGuides/preparation.js +9 -5
- package/src/materialization/facets.js +39 -27
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +5 -2
- package/src/materialization/marks/index.js +24 -4
- package/src/materialization/scales/policies/bar.js +61 -59
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +4 -5
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/extension.d.ts +19 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +509 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
2
3
|
|
|
3
4
|
const COMMAND_KEYS = Object.freeze({
|
|
4
5
|
M: Object.freeze(["op", "x", "y"]),
|
|
@@ -37,6 +38,7 @@ export function validatePathCommands(commands) {
|
|
|
37
38
|
if (!Array.isArray(commands) || commands.length < 2) {
|
|
38
39
|
throw new TypeError("path.commands must contain at least two commands.");
|
|
39
40
|
}
|
|
41
|
+
validateGeneratedItemLimit(commands.length, "path.commands length");
|
|
40
42
|
commands.forEach(validateCommand);
|
|
41
43
|
if (commands[0].op !== "M") {
|
|
42
44
|
throw new Error("path.commands must start with M.");
|
|
@@ -77,6 +79,10 @@ export function buildLinearPathCommands(points, { close = false } = {}) {
|
|
|
77
79
|
if (typeof close !== "boolean") {
|
|
78
80
|
throw new TypeError("Linear path close must be a boolean.");
|
|
79
81
|
}
|
|
82
|
+
validateGeneratedItemLimit(
|
|
83
|
+
points.length + Number(close),
|
|
84
|
+
"Linear path command count"
|
|
85
|
+
);
|
|
80
86
|
const commands = [
|
|
81
87
|
{ op: "M", x: points[0].x, y: points[0].y },
|
|
82
88
|
...points.slice(1).map(point => ({ op: "L", x: point.x, y: point.y }))
|
|
@@ -27,7 +27,7 @@ function validateRadius(value, label, frame) {
|
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
30
|
+
export function resolvePolarPoint(frame, theta, radius) {
|
|
31
31
|
const radians = theta * Math.PI / 180;
|
|
32
32
|
return {
|
|
33
33
|
x: frame.centerX + radius * Math.sin(radians),
|
|
@@ -47,8 +47,8 @@ function arcCommands(frame, radius, startTheta, endTheta) {
|
|
|
47
47
|
const endRadians = end * Math.PI / 180;
|
|
48
48
|
const deltaRadians = endRadians - startRadians;
|
|
49
49
|
const control = 4 / 3 * Math.tan(deltaRadians / 4);
|
|
50
|
-
const from =
|
|
51
|
-
const to =
|
|
50
|
+
const from = resolvePolarPoint(frame, start, radius);
|
|
51
|
+
const to = resolvePolarPoint(frame, end, radius);
|
|
52
52
|
const fromDerivative = {
|
|
53
53
|
x: radius * Math.cos(startRadians),
|
|
54
54
|
y: radius * Math.sin(startRadians)
|
|
@@ -147,13 +147,13 @@ export function buildAnnularSectorCommands({
|
|
|
147
147
|
const start = startTheta + direction * padAngle / 2;
|
|
148
148
|
const end = endTheta - direction * padAngle / 2;
|
|
149
149
|
const commands = [
|
|
150
|
-
{ op: "M", ...
|
|
150
|
+
{ op: "M", ...resolvePolarPoint(frame, start, outerRadius) },
|
|
151
151
|
...arcCommands(frame, outerRadius, start, end)
|
|
152
152
|
];
|
|
153
153
|
if (innerRadius === 0) {
|
|
154
154
|
commands.push({ op: "L", x: frame.centerX, y: frame.centerY });
|
|
155
155
|
} else {
|
|
156
|
-
commands.push({ op: "L", ...
|
|
156
|
+
commands.push({ op: "L", ...resolvePolarPoint(frame, end, innerRadius) });
|
|
157
157
|
commands.push(...arcCommands(frame, innerRadius, end, start));
|
|
158
158
|
}
|
|
159
159
|
commands.push({ op: "Z" });
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
readQuantitativeField
|
|
5
5
|
} from "../scales/index.js";
|
|
6
6
|
import { fitRegressionGroup, predictRegressionAt } from "./models.js";
|
|
7
|
+
import { requireFiniteResult } from "../numeric.js";
|
|
8
|
+
import {
|
|
9
|
+
validateGeneratedItemLimit,
|
|
10
|
+
validateWorkLimit
|
|
11
|
+
} from "../../core/validation.js";
|
|
7
12
|
import {
|
|
8
13
|
normalizeRegressionParameters,
|
|
9
14
|
requireRegressionField
|
|
@@ -41,16 +46,34 @@ export function deriveRegression(values, {
|
|
|
41
46
|
readQuantitativeField(values, y);
|
|
42
47
|
if (groupBy !== undefined) readNominalField(values, groupBy);
|
|
43
48
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
const groupedRows = new Map();
|
|
50
|
+
if (groupBy === undefined) {
|
|
51
|
+
groupedRows.set(undefined, values);
|
|
52
|
+
} else {
|
|
53
|
+
for (const row of values) {
|
|
54
|
+
const group = row[groupBy];
|
|
55
|
+
const rows = groupedRows.get(group) ?? [];
|
|
56
|
+
rows.push(row);
|
|
57
|
+
groupedRows.set(group, rows);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const groups = [...groupedRows.keys()];
|
|
47
61
|
const models = [];
|
|
48
62
|
const rows = [];
|
|
63
|
+
let work = 0;
|
|
49
64
|
|
|
50
65
|
for (const group of groups) {
|
|
51
|
-
const groupRows =
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
const groupRows = groupedRows.get(group);
|
|
67
|
+
const xCount = new Set(groupRows.map(row => row[x])).size;
|
|
68
|
+
validateGeneratedItemLimit(rows.length + xCount, "Regression generated row count");
|
|
69
|
+
if (parameters.method === "polynomial") {
|
|
70
|
+
const size = parameters.degree + 1;
|
|
71
|
+
work += groupRows.length * size ** 2 + size ** 3;
|
|
72
|
+
} else if (parameters.method === "loess") {
|
|
73
|
+
work += groupRows.length * xCount *
|
|
74
|
+
Math.ceil(Math.log2(groupRows.length + 1));
|
|
75
|
+
}
|
|
76
|
+
validateWorkLimit(work, "Regression computation");
|
|
54
77
|
const model = fitRegressionGroup(groupRows, {
|
|
55
78
|
x,
|
|
56
79
|
y,
|
|
@@ -63,13 +86,29 @@ export function deriveRegression(values, {
|
|
|
63
86
|
|
|
64
87
|
for (const xValue of xValues) {
|
|
65
88
|
const prediction = predictRegressionAt(model, xValue, parameters);
|
|
89
|
+
const fitted = requireFiniteResult(
|
|
90
|
+
prediction.prediction,
|
|
91
|
+
`Regression group "${group === undefined ? "all" : String(group)}" prediction`
|
|
92
|
+
);
|
|
93
|
+
const lower = parameters.method === "loess"
|
|
94
|
+
? undefined
|
|
95
|
+
: requireFiniteResult(
|
|
96
|
+
fitted - prediction.margin,
|
|
97
|
+
`Regression group "${group === undefined ? "all" : String(group)}" lower interval`
|
|
98
|
+
);
|
|
99
|
+
const upper = parameters.method === "loess"
|
|
100
|
+
? undefined
|
|
101
|
+
: requireFiniteResult(
|
|
102
|
+
fitted + prediction.margin,
|
|
103
|
+
`Regression group "${group === undefined ? "all" : String(group)}" upper interval`
|
|
104
|
+
);
|
|
66
105
|
rows.push({
|
|
67
106
|
...(groupBy === undefined ? {} : { [groupBy]: group }),
|
|
68
107
|
[x]: xValue,
|
|
69
|
-
[y]:
|
|
108
|
+
[y]: fitted,
|
|
70
109
|
...(parameters.method === "loess" ? {} : {
|
|
71
|
-
[REGRESSION_LOWER_FIELD]:
|
|
72
|
-
[REGRESSION_UPPER_FIELD]:
|
|
110
|
+
[REGRESSION_LOWER_FIELD]: lower,
|
|
111
|
+
[REGRESSION_UPPER_FIELD]: upper
|
|
73
112
|
})
|
|
74
113
|
});
|
|
75
114
|
}
|
|
@@ -1,15 +1,75 @@
|
|
|
1
1
|
import { studentTCriticalValue } from "../statistics/studentT.js";
|
|
2
|
+
import {
|
|
3
|
+
maximumMagnitude,
|
|
4
|
+
requireFiniteResult,
|
|
5
|
+
restoreFiniteScale,
|
|
6
|
+
stableFiniteMean,
|
|
7
|
+
stableFiniteSquareSum
|
|
8
|
+
} from "../numeric.js";
|
|
9
|
+
|
|
10
|
+
function restoreFiniteRatio(value, numeratorScale, denominatorScale, label) {
|
|
11
|
+
if (value === 0) return 0;
|
|
12
|
+
return requireFiniteResult(
|
|
13
|
+
Math.sign(value) * Math.exp(
|
|
14
|
+
Math.log(Math.abs(value)) +
|
|
15
|
+
Math.log(numeratorScale) -
|
|
16
|
+
Math.log(denominatorScale)
|
|
17
|
+
),
|
|
18
|
+
label
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizedOffset(value, center, scale, byDifference = true) {
|
|
23
|
+
return byDifference
|
|
24
|
+
? (value - center) / scale
|
|
25
|
+
: value / scale - center / scale;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizedCoordinates(values, center) {
|
|
29
|
+
const differences = values.map(value => value - center);
|
|
30
|
+
const byDifference = differences.every(Number.isFinite);
|
|
31
|
+
const scale = byDifference
|
|
32
|
+
? maximumMagnitude(differences)
|
|
33
|
+
: Math.max(maximumMagnitude(values), Math.abs(center));
|
|
34
|
+
return [
|
|
35
|
+
values.map(value => normalizedOffset(value, center, scale, byDifference)),
|
|
36
|
+
scale,
|
|
37
|
+
byDifference
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizedLinearX(model, value) {
|
|
42
|
+
return normalizedOffset(
|
|
43
|
+
value, model.meanX, model.xScale, model.normalizedXByDifference !== false
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function linearPrediction(model, xValue) {
|
|
48
|
+
if (model.normalizedSxx === undefined) {
|
|
49
|
+
return model.intercept + model.slope * xValue;
|
|
50
|
+
}
|
|
51
|
+
return restoreFiniteScale(
|
|
52
|
+
model.normalizedMeanY +
|
|
53
|
+
model.normalizedResponseSlope * normalizedLinearX(model, xValue),
|
|
54
|
+
model.responseScale,
|
|
55
|
+
"Linear regression prediction"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
2
58
|
|
|
3
59
|
function fitLinearGroup(rows, { x, y, group, confidence }) {
|
|
4
60
|
const count = rows.length;
|
|
5
61
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
62
|
+
const label = `Regression group "${groupLabel}"`;
|
|
6
63
|
if (count < 3) {
|
|
7
64
|
throw new Error(
|
|
8
|
-
|
|
65
|
+
`${label} requires at least three rows.`
|
|
9
66
|
);
|
|
10
67
|
}
|
|
11
|
-
const
|
|
12
|
-
const
|
|
68
|
+
const xValues = rows.map(row => row[x]);
|
|
69
|
+
const yValues = rows.map(row => row[y]);
|
|
70
|
+
const ordinaryYTotal = yValues.reduce((sum, value) => sum + value, 0);
|
|
71
|
+
const meanX = stableFiniteMean(xValues, "Linear regression x mean");
|
|
72
|
+
const meanY = stableFiniteMean(yValues, "Linear regression y mean");
|
|
13
73
|
let sxx = 0;
|
|
14
74
|
let sxy = 0;
|
|
15
75
|
for (const row of rows) {
|
|
@@ -19,28 +79,81 @@ function fitLinearGroup(rows, { x, y, group, confidence }) {
|
|
|
19
79
|
}
|
|
20
80
|
if (sxx === 0) {
|
|
21
81
|
throw new Error(
|
|
22
|
-
|
|
82
|
+
`${label} requires varying x values.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const stableLinear = !Number.isFinite(ordinaryYTotal) ||
|
|
86
|
+
!Number.isFinite(sxx) || !Number.isFinite(sxy);
|
|
87
|
+
let normalizedXByDifference = true;
|
|
88
|
+
let xScale;
|
|
89
|
+
let responseScale;
|
|
90
|
+
let normalizedMeanY;
|
|
91
|
+
let normalizedResponseSlope;
|
|
92
|
+
let normalizedSxx;
|
|
93
|
+
if (stableLinear) {
|
|
94
|
+
let normalizedX;
|
|
95
|
+
[normalizedX, xScale, normalizedXByDifference] =
|
|
96
|
+
normalizedCoordinates(xValues, meanX);
|
|
97
|
+
xScale ||= 1;
|
|
98
|
+
responseScale = maximumMagnitude(yValues) || 1;
|
|
99
|
+
normalizedMeanY = meanY / responseScale;
|
|
100
|
+
normalizedSxx = normalizedX.reduce(
|
|
101
|
+
(sum, value) => sum + value ** 2,
|
|
102
|
+
0
|
|
103
|
+
);
|
|
104
|
+
if (normalizedSxx === 0) {
|
|
105
|
+
throw new Error(`${label} requires varying x values.`);
|
|
106
|
+
}
|
|
107
|
+
normalizedResponseSlope = rows.reduce(
|
|
108
|
+
(sum, row, index) => sum + normalizedX[index] * (
|
|
109
|
+
row[y] / responseScale - normalizedMeanY
|
|
110
|
+
),
|
|
111
|
+
0
|
|
112
|
+
) / normalizedSxx;
|
|
113
|
+
}
|
|
114
|
+
const slope = stableLinear
|
|
115
|
+
? restoreFiniteRatio(
|
|
116
|
+
normalizedResponseSlope, responseScale, xScale, `${label} slope`
|
|
117
|
+
)
|
|
118
|
+
: requireFiniteResult(sxy / sxx, `${label} slope`);
|
|
119
|
+
let intercept = meanY - slope * meanX;
|
|
120
|
+
if (!Number.isFinite(intercept) && stableLinear) {
|
|
121
|
+
intercept = restoreFiniteScale(
|
|
122
|
+
normalizedMeanY + normalizedResponseSlope * normalizedOffset(
|
|
123
|
+
0, meanX, xScale, normalizedXByDifference
|
|
124
|
+
),
|
|
125
|
+
responseScale,
|
|
126
|
+
`${label} intercept`
|
|
23
127
|
);
|
|
24
128
|
}
|
|
25
|
-
|
|
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);
|
|
129
|
+
requireFiniteResult(intercept, `${label} intercept`);
|
|
31
130
|
const degreesOfFreedom = count - 2;
|
|
32
|
-
|
|
131
|
+
const model = {
|
|
33
132
|
count,
|
|
34
133
|
degreesOfFreedom,
|
|
35
134
|
meanX,
|
|
36
135
|
meanY,
|
|
37
|
-
sxx,
|
|
38
136
|
slope,
|
|
39
|
-
intercept
|
|
137
|
+
intercept
|
|
138
|
+
};
|
|
139
|
+
if (Number.isFinite(sxx)) model.sxx = sxx;
|
|
140
|
+
if (stableLinear) Object.assign(model, {
|
|
141
|
+
xScale,
|
|
142
|
+
responseScale,
|
|
143
|
+
normalizedMeanY,
|
|
144
|
+
normalizedResponseSlope,
|
|
145
|
+
normalizedSxx
|
|
146
|
+
});
|
|
147
|
+
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
148
|
+
const residualSumSquares = stableFiniteSquareSum(
|
|
149
|
+
rows.map(row => row[y] - linearPrediction(model, row[x])),
|
|
150
|
+
`${label} residual sum of squares`
|
|
151
|
+
);
|
|
152
|
+
return Object.assign(model, {
|
|
40
153
|
residualSumSquares,
|
|
41
154
|
residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
|
|
42
155
|
critical: studentTCriticalValue(confidence, degreesOfFreedom)
|
|
43
|
-
};
|
|
156
|
+
});
|
|
44
157
|
}
|
|
45
158
|
|
|
46
159
|
function solveLinearSystem(matrix, vector) {
|
|
@@ -94,11 +207,25 @@ function binomial(n, k) {
|
|
|
94
207
|
}
|
|
95
208
|
|
|
96
209
|
function rawPolynomialCoefficients(coefficients, center, scale) {
|
|
97
|
-
return coefficients.map((_, degree) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
210
|
+
return coefficients.map((_, degree) => {
|
|
211
|
+
const sum = stable => coefficients.reduce(
|
|
212
|
+
(total, coefficient, power) => power < degree
|
|
213
|
+
? total
|
|
214
|
+
: total + coefficient * binomial(power, degree) *
|
|
215
|
+
(stable ? -center / scale : -center) ** (power - degree) /
|
|
216
|
+
scale ** (stable ? degree : power),
|
|
217
|
+
0
|
|
218
|
+
);
|
|
219
|
+
const direct = sum(false);
|
|
220
|
+
return requireFiniteResult(Number.isFinite(direct) ? direct :
|
|
221
|
+
sum(true), `Polynomial regression coefficient ${degree}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function polynomialResponse(design, values, size, scale = 1) {
|
|
226
|
+
return Array.from({ length: size }, (_, column) => design.reduce(
|
|
227
|
+
(sum, basis, index) =>
|
|
228
|
+
sum + basis[column] * (values[index] / scale),
|
|
102
229
|
0
|
|
103
230
|
));
|
|
104
231
|
}
|
|
@@ -107,29 +234,30 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
|
|
|
107
234
|
const count = rows.length;
|
|
108
235
|
const parameterCount = degree + 1;
|
|
109
236
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
237
|
+
const label = `Polynomial regression group "${groupLabel}"`;
|
|
110
238
|
if (
|
|
111
239
|
count < degree + 2 ||
|
|
112
240
|
new Set(rows.map(row => row[x])).size < parameterCount
|
|
113
241
|
) {
|
|
114
242
|
throw new Error(
|
|
115
|
-
|
|
243
|
+
`${label} requires at least ` +
|
|
116
244
|
`${degree + 2} rows and ${parameterCount} distinct x values.`
|
|
117
245
|
);
|
|
118
246
|
}
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
247
|
+
const xValues = rows.map(row => row[x]);
|
|
248
|
+
const yValues = rows.map(row => row[y]);
|
|
249
|
+
const center = stableFiniteMean(xValues, "Polynomial regression x center");
|
|
250
|
+
const [normalizedX, scale, normalizedXByDifference] =
|
|
251
|
+
normalizedCoordinates(xValues, center);
|
|
252
|
+
if (!(scale > 0) || !Number.isFinite(scale)) {
|
|
122
253
|
throw new Error(
|
|
123
|
-
`Regression group "${groupLabel}" requires varying x values.`
|
|
254
|
+
`Regression group "${groupLabel}" requires finite varying x values.`
|
|
124
255
|
);
|
|
125
256
|
}
|
|
126
|
-
const design =
|
|
127
|
-
const normalized = (row[x] - center) / scale;
|
|
128
|
-
return Array.from(
|
|
257
|
+
const design = normalizedX.map(normalized => Array.from(
|
|
129
258
|
{ length: parameterCount },
|
|
130
259
|
(_, power) => normalized ** power
|
|
131
|
-
);
|
|
132
|
-
});
|
|
260
|
+
));
|
|
133
261
|
const normal = Array.from({ length: parameterCount }, (_, row) =>
|
|
134
262
|
Array.from({ length: parameterCount }, (_, column) =>
|
|
135
263
|
design.reduce(
|
|
@@ -138,22 +266,36 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
|
|
|
138
266
|
)
|
|
139
267
|
)
|
|
140
268
|
);
|
|
141
|
-
const response =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
const
|
|
269
|
+
const response = polynomialResponse(design, yValues, parameterCount);
|
|
270
|
+
const stableResponse = response.some(value => !Number.isFinite(value));
|
|
271
|
+
const responseScale = stableResponse ? maximumMagnitude(yValues) || 1 : undefined;
|
|
272
|
+
const resolvedResponse = stableResponse
|
|
273
|
+
? polynomialResponse(design, yValues, parameterCount, responseScale)
|
|
274
|
+
: response;
|
|
275
|
+
const scaledCoefficients = solveLinearSystem(normal, resolvedResponse);
|
|
276
|
+
const normalizedCoefficients = stableResponse
|
|
277
|
+
? scaledCoefficients.map((coefficient, index) => restoreFiniteScale(
|
|
278
|
+
coefficient,
|
|
279
|
+
responseScale,
|
|
280
|
+
`${label} coefficient ${index}`
|
|
281
|
+
))
|
|
282
|
+
: scaledCoefficients;
|
|
148
283
|
const inverse = invertSymmetricMatrix(normal);
|
|
149
|
-
const fitted = design.map(basis =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
284
|
+
const fitted = design.map(basis => stableResponse
|
|
285
|
+
? restoreFiniteScale(
|
|
286
|
+
dot(basis, scaledCoefficients),
|
|
287
|
+
responseScale,
|
|
288
|
+
`${label} prediction`
|
|
289
|
+
)
|
|
290
|
+
: dot(basis, normalizedCoefficients)
|
|
291
|
+
);
|
|
292
|
+
const residualSumSquares = stableFiniteSquareSum(
|
|
293
|
+
fitted.map((value, index) => rows[index][y] - value),
|
|
294
|
+
`${label} residual sum of squares`
|
|
153
295
|
);
|
|
154
296
|
const degreesOfFreedom = count - parameterCount;
|
|
155
297
|
const residualVariance = residualSumSquares / degreesOfFreedom;
|
|
156
|
-
|
|
298
|
+
const model = {
|
|
157
299
|
count,
|
|
158
300
|
degreesOfFreedom,
|
|
159
301
|
degree,
|
|
@@ -170,36 +312,76 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
|
|
|
170
312
|
residualStandardError: Math.sqrt(residualVariance),
|
|
171
313
|
critical: studentTCriticalValue(confidence, degreesOfFreedom)
|
|
172
314
|
};
|
|
315
|
+
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
316
|
+
if (stableResponse) Object.assign(model, { responseScale, scaledCoefficients });
|
|
317
|
+
return model;
|
|
173
318
|
}
|
|
174
319
|
|
|
175
320
|
function evaluatePolynomial(model, xValue) {
|
|
176
|
-
const normalized = (
|
|
321
|
+
const normalized = normalizedOffset(
|
|
322
|
+
xValue, model.center, model.scale, model.normalizedXByDifference !== false
|
|
323
|
+
);
|
|
177
324
|
const basis = model.normalizedCoefficients.map(
|
|
178
325
|
(_, power) => normalized ** power
|
|
179
326
|
);
|
|
180
327
|
return {
|
|
181
|
-
prediction:
|
|
328
|
+
prediction: model.responseScale === undefined
|
|
329
|
+
? dot(basis, model.normalizedCoefficients)
|
|
330
|
+
: restoreFiniteScale(
|
|
331
|
+
dot(basis, model.scaledCoefficients),
|
|
332
|
+
model.responseScale,
|
|
333
|
+
"Polynomial regression prediction"
|
|
334
|
+
),
|
|
182
335
|
leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
|
|
183
336
|
};
|
|
184
337
|
}
|
|
185
338
|
|
|
339
|
+
function weightedPrediction(items, x, y, xValue, xScale = 1, yScale = 1) {
|
|
340
|
+
const difference = item => item.row[x] / xScale - xValue / xScale;
|
|
341
|
+
let totalWeight = 0;
|
|
342
|
+
let meanDifference = 0;
|
|
343
|
+
let meanY = 0;
|
|
344
|
+
for (const item of items) {
|
|
345
|
+
totalWeight += item.weight;
|
|
346
|
+
meanDifference += item.weight * difference(item);
|
|
347
|
+
meanY += item.weight * item.row[y] / yScale;
|
|
348
|
+
}
|
|
349
|
+
meanDifference /= totalWeight;
|
|
350
|
+
meanY /= totalWeight;
|
|
351
|
+
let variance = 0;
|
|
352
|
+
let covariance = 0;
|
|
353
|
+
for (const item of items) {
|
|
354
|
+
const centered = difference(item) - meanDifference;
|
|
355
|
+
variance += item.weight * centered ** 2;
|
|
356
|
+
covariance += item.weight * centered * (item.row[y] / yScale - meanY);
|
|
357
|
+
}
|
|
358
|
+
return meanY - (variance === 0 ? 0 : covariance / variance) * meanDifference;
|
|
359
|
+
}
|
|
360
|
+
|
|
186
361
|
function fitLoessGroup(rows, { x, y, group, span }) {
|
|
187
362
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
363
|
+
const label = `LOESS regression group "${groupLabel}"`;
|
|
188
364
|
if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
|
|
189
365
|
throw new Error(
|
|
190
|
-
|
|
366
|
+
`${label} requires at least two rows ` +
|
|
191
367
|
"and varying x values."
|
|
192
368
|
);
|
|
193
369
|
}
|
|
194
370
|
const neighborCount = Math.max(2, Math.ceil(span * rows.length));
|
|
195
371
|
const xValues = [...new Set(rows.map(row => row[x]))]
|
|
196
372
|
.sort((left, right) => left - right);
|
|
373
|
+
const xScale = maximumMagnitude(rows.map(row => row[x])) || 1;
|
|
374
|
+
const yScale = maximumMagnitude(rows.map(row => row[y])) || 1;
|
|
197
375
|
const fits = xValues.map(xValue => {
|
|
376
|
+
const rawDistances = rows.map(row => Math.abs(row[x] - xValue));
|
|
377
|
+
const scaledDistances = rawDistances.some(value => !Number.isFinite(value));
|
|
198
378
|
const neighbors = rows
|
|
199
379
|
.map((row, index) => ({
|
|
200
380
|
row,
|
|
201
381
|
index,
|
|
202
|
-
distance:
|
|
382
|
+
distance: scaledDistances
|
|
383
|
+
? Math.abs(row[x] / xScale - xValue / xScale)
|
|
384
|
+
: rawDistances[index]
|
|
203
385
|
}))
|
|
204
386
|
.sort((left, right) =>
|
|
205
387
|
left.distance - right.distance || left.index - right.index
|
|
@@ -212,29 +394,16 @@ function fitLoessGroup(rows, { x, y, group, span }) {
|
|
|
212
394
|
? 1
|
|
213
395
|
: (1 - (neighbor.distance / radius) ** 3) ** 3
|
|
214
396
|
}));
|
|
215
|
-
const
|
|
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;
|
|
397
|
+
const ordinary = weightedPrediction(weighted, x, y, xValue);
|
|
235
398
|
return {
|
|
236
399
|
x: xValue,
|
|
237
|
-
prediction:
|
|
400
|
+
prediction: Number.isFinite(ordinary)
|
|
401
|
+
? ordinary
|
|
402
|
+
: restoreFiniteScale(
|
|
403
|
+
weightedPrediction(weighted, x, y, xValue, xScale, yScale),
|
|
404
|
+
yScale,
|
|
405
|
+
`${label} prediction`
|
|
406
|
+
),
|
|
238
407
|
neighborIndices: weighted.map(item => item.index)
|
|
239
408
|
};
|
|
240
409
|
});
|
|
@@ -267,16 +436,26 @@ export function predictRegressionAt(model, xValue, parameters) {
|
|
|
267
436
|
? evaluatePolynomial(model, xValue)
|
|
268
437
|
: undefined;
|
|
269
438
|
const prediction = parameters.method === "linear"
|
|
270
|
-
? model
|
|
439
|
+
? linearPrediction(model, xValue)
|
|
271
440
|
: parameters.method === "polynomial"
|
|
272
441
|
? polynomial.prediction
|
|
273
442
|
: model.fits.find(fit => fit.x === xValue).prediction;
|
|
274
443
|
if (parameters.method === "loess") return { prediction };
|
|
275
444
|
const leverage = parameters.method === "linear"
|
|
276
|
-
?
|
|
445
|
+
? model.normalizedSxx !== undefined
|
|
446
|
+
? 1 / model.count + (
|
|
447
|
+
normalizedLinearX(model, xValue)
|
|
448
|
+
) ** 2 / model.normalizedSxx
|
|
449
|
+
: 1 / model.count + (xValue - model.meanX) ** 2 / model.sxx
|
|
277
450
|
: polynomial.leverage;
|
|
278
451
|
const standardError = model.residualStandardError * Math.sqrt(
|
|
279
452
|
leverage + (parameters.interval === "prediction" ? 1 : 0)
|
|
280
453
|
);
|
|
281
|
-
return {
|
|
454
|
+
return {
|
|
455
|
+
prediction: requireFiniteResult(prediction, "Regression prediction"),
|
|
456
|
+
margin: requireFiniteResult(
|
|
457
|
+
model.critical * standardError,
|
|
458
|
+
"Regression interval margin"
|
|
459
|
+
)
|
|
460
|
+
};
|
|
282
461
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
import { validateGeneratedItemLimit } from "../../core/validation.js";
|
|
3
|
+
|
|
4
|
+
const MAX_POLYNOMIAL_DEGREE = 32;
|
|
2
5
|
|
|
3
6
|
export function requireRegressionField(field, label) {
|
|
4
7
|
if (typeof field !== "string" || field.length === 0) {
|
|
@@ -54,6 +57,11 @@ export function normalizeRegressionParameters({
|
|
|
54
57
|
"Regression polynomial degree must be a positive integer."
|
|
55
58
|
);
|
|
56
59
|
}
|
|
60
|
+
validateGeneratedItemLimit(
|
|
61
|
+
resolvedDegree,
|
|
62
|
+
"Regression polynomial degree",
|
|
63
|
+
MAX_POLYNOMIAL_DEGREE
|
|
64
|
+
);
|
|
57
65
|
return cloneAndFreeze({
|
|
58
66
|
method,
|
|
59
67
|
degree: resolvedDegree,
|