ggaction 0.0.16 → 0.0.18
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 +66 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +10 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +20 -7
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -190
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +72 -7
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
package/src/grammar/polar.js
CHANGED
|
@@ -1,184 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { validateOptionObject } from "../core/validation.js";
|
|
3
|
-
import { validatePair } from "./scales/validation.js";
|
|
4
|
-
|
|
5
|
-
const POLAR_FRAME_OPTIONS = Object.freeze(["center", "radius"]);
|
|
6
|
-
const POLAR_CENTER_OPTIONS = Object.freeze(["x", "y"]);
|
|
7
|
-
const POLAR_RADIUS_OPTIONS = Object.freeze(["unit", "value"]);
|
|
8
|
-
const POLAR_RADIUS_UNITS = new Set(["fraction", "px"]);
|
|
9
|
-
|
|
10
|
-
function validateBounds(bounds) {
|
|
11
|
-
if (
|
|
12
|
-
!isPlainObject(bounds) ||
|
|
13
|
-
![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
|
|
14
|
-
bounds.width < 0 ||
|
|
15
|
-
bounds.height < 0
|
|
16
|
-
) {
|
|
17
|
-
throw new TypeError(
|
|
18
|
-
"Polar geometry requires finite, non-negative graphical bounds."
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
return bounds;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function finiteUnitInterval(value, label) {
|
|
25
|
-
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
26
|
-
throw new RangeError(`${label} must be a finite number from 0 through 1.`);
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function normalizePolarFrameOptions(value) {
|
|
32
|
-
if (value === "auto") return value;
|
|
33
|
-
if (!isPlainObject(value)) {
|
|
34
|
-
throw new TypeError('Polar frame must be "auto" or a plain object.');
|
|
35
|
-
}
|
|
36
|
-
validateOptionObject(value, POLAR_FRAME_OPTIONS, "polar frame");
|
|
37
|
-
const center = value.center ?? {};
|
|
38
|
-
if (!isPlainObject(center)) {
|
|
39
|
-
throw new TypeError("Polar frame center must be a plain object.");
|
|
40
|
-
}
|
|
41
|
-
validateOptionObject(center, POLAR_CENTER_OPTIONS, "polar frame center");
|
|
42
|
-
const radius = value.radius ?? { unit: "fraction", value: 1 };
|
|
43
|
-
if (!isPlainObject(radius)) {
|
|
44
|
-
throw new TypeError("Polar frame radius must be a plain object.");
|
|
45
|
-
}
|
|
46
|
-
validateOptionObject(radius, POLAR_RADIUS_OPTIONS, "polar frame radius", {
|
|
47
|
-
allowEmpty: false,
|
|
48
|
-
emptyError: TypeError
|
|
49
|
-
});
|
|
50
|
-
if (!POLAR_RADIUS_UNITS.has(radius.unit)) {
|
|
51
|
-
throw new Error(`Unknown Polar frame radius unit "${radius.unit}".`);
|
|
52
|
-
}
|
|
53
|
-
if (!Number.isFinite(radius.value) || radius.value <= 0) {
|
|
54
|
-
throw new RangeError("Polar frame radius must be a positive finite number.");
|
|
55
|
-
}
|
|
56
|
-
if (radius.unit === "fraction" && radius.value > 1) {
|
|
57
|
-
throw new RangeError("Polar frame radius fraction must not exceed 1.");
|
|
58
|
-
}
|
|
59
|
-
return cloneAndFreeze({
|
|
60
|
-
center: {
|
|
61
|
-
x: finiteUnitInterval(center.x ?? 0.5, "Polar frame center x"),
|
|
62
|
-
y: finiteUnitInterval(center.y ?? 0.5, "Polar frame center y")
|
|
63
|
-
},
|
|
64
|
-
radius: {
|
|
65
|
-
unit: radius.unit,
|
|
66
|
-
value: radius.value
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function resolvePolarFrame(bounds, requestedFrame = "auto") {
|
|
72
|
-
validateBounds(bounds);
|
|
73
|
-
if (requestedFrame === undefined || requestedFrame === "auto") {
|
|
74
|
-
return cloneAndFreeze({
|
|
75
|
-
centerX: bounds.x + bounds.width / 2,
|
|
76
|
-
centerY: bounds.y + bounds.height / 2,
|
|
77
|
-
availableRadius: Math.min(bounds.width, bounds.height) / 2
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
const requested = normalizePolarFrameOptions(requestedFrame);
|
|
81
|
-
const centerX = bounds.x + bounds.width * requested.center.x;
|
|
82
|
-
const centerY = bounds.y + bounds.height * requested.center.y;
|
|
83
|
-
const maximumRadius = Math.min(
|
|
84
|
-
centerX - bounds.x,
|
|
85
|
-
bounds.x + bounds.width - centerX,
|
|
86
|
-
centerY - bounds.y,
|
|
87
|
-
bounds.y + bounds.height - centerY
|
|
88
|
-
);
|
|
89
|
-
if (!Number.isFinite(maximumRadius) || maximumRadius <= 0) {
|
|
90
|
-
throw new RangeError(
|
|
91
|
-
"Polar frame center must leave a positive radius inside its bounds."
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const availableRadius = requested.radius.unit === "fraction"
|
|
95
|
-
? maximumRadius * requested.radius.value
|
|
96
|
-
: requested.radius.value;
|
|
97
|
-
if (availableRadius > maximumRadius) {
|
|
98
|
-
throw new RangeError(
|
|
99
|
-
`Polar frame radius ${availableRadius} exceeds the maximum radius ${maximumRadius}.`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
return cloneAndFreeze({
|
|
103
|
-
centerX,
|
|
104
|
-
centerY,
|
|
105
|
-
availableRadius
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function validateThetaRange(range) {
|
|
110
|
-
if (range === "auto") return range;
|
|
111
|
-
const validated = validatePair(range, "Theta scale range");
|
|
112
|
-
if (Math.abs(validated[1] - validated[0]) > 360) {
|
|
113
|
-
throw new RangeError("Theta scale range span must not exceed 360 degrees.");
|
|
114
|
-
}
|
|
115
|
-
return validated;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function validateRadialRange(range, availableRadius) {
|
|
119
|
-
if (range === "auto") return range;
|
|
120
|
-
const validated = validatePair(range, "Radius scale range");
|
|
121
|
-
if (validated.some(value => value < 0)) {
|
|
122
|
-
throw new RangeError("Radius scale range values must be non-negative.");
|
|
123
|
-
}
|
|
124
|
-
if (
|
|
125
|
-
availableRadius !== undefined &&
|
|
126
|
-
validated.some(value => value > availableRadius)
|
|
127
|
-
) {
|
|
128
|
-
throw new RangeError(
|
|
129
|
-
`Radius scale range must fit within the available radius ${availableRadius}.`
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
return validated;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function resolvePolarScaleRange(range, channel, bounds, frame) {
|
|
136
|
-
if (channel === "theta") {
|
|
137
|
-
return range === "auto"
|
|
138
|
-
? cloneAndFreeze([0, 360])
|
|
139
|
-
: validateThetaRange(range);
|
|
140
|
-
}
|
|
141
|
-
if (channel !== "radius") {
|
|
142
|
-
throw new Error(`Unknown Polar position channel "${channel}".`);
|
|
143
|
-
}
|
|
144
|
-
const resolvedFrame = frame ?? resolvePolarFrame(bounds);
|
|
145
|
-
return range === "auto"
|
|
146
|
-
? cloneAndFreeze([0, resolvedFrame.availableRadius])
|
|
147
|
-
: validateRadialRange(range, resolvedFrame.availableRadius);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function polarDirection(theta) {
|
|
151
|
-
if (!Number.isFinite(theta)) {
|
|
152
|
-
throw new TypeError("Polar theta must be a finite number of degrees.");
|
|
153
|
-
}
|
|
154
|
-
const angle = theta * Math.PI / 180;
|
|
155
|
-
return cloneAndFreeze({
|
|
156
|
-
x: Math.sin(angle),
|
|
157
|
-
y: -Math.cos(angle)
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function polarToCartesian({ theta, radius, frame }) {
|
|
162
|
-
const direction = polarDirection(theta);
|
|
163
|
-
if (!Number.isFinite(radius) || radius < 0) {
|
|
164
|
-
throw new TypeError("Polar radius must be a non-negative finite number.");
|
|
165
|
-
}
|
|
166
|
-
const resolvedFrame = frame?.availableRadius === undefined
|
|
167
|
-
? resolvePolarFrame(frame)
|
|
168
|
-
: frame;
|
|
169
|
-
if (
|
|
170
|
-
![resolvedFrame.centerX, resolvedFrame.centerY,
|
|
171
|
-
resolvedFrame.availableRadius].every(Number.isFinite)
|
|
172
|
-
) {
|
|
173
|
-
throw new TypeError("Polar frame must contain finite geometry.");
|
|
174
|
-
}
|
|
175
|
-
if (radius > resolvedFrame.availableRadius) {
|
|
176
|
-
throw new RangeError(
|
|
177
|
-
`Polar radius ${radius} exceeds the available radius ${resolvedFrame.availableRadius}.`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
return cloneAndFreeze({
|
|
181
|
-
x: resolvedFrame.centerX + radius * direction.x,
|
|
182
|
-
y: resolvedFrame.centerY + radius * direction.y
|
|
183
|
-
});
|
|
184
|
-
}
|
|
1
|
+
import{cloneAndFreeze as n,isPlainObject as l}from"../core/immutable.js";import{validateOptionObject as u}from"../core/validation.js";import{validatePair as c}from"./scales/validation.js";const w=Object.freeze(["center","radius","overflow"]),h=Object.freeze(["x","y"]),d=Object.freeze(["unit","value"]),p=new Set(["fraction","px"]);function v(e){if(!l(e)||![e.x,e.y,e.width,e.height].every(Number.isFinite)||e.width<0||e.height<0)throw new TypeError("Polar geometry requires finite, non-negative graphical bounds.");return e}function s(e,r){if(!Number.isFinite(e)||e<0||e>1)throw new RangeError(`${r} must be a finite number from 0 through 1.`);return e}function R(e){if(e==="auto")return e;if(!l(e))throw new TypeError('Polar frame must be "auto" or a plain object.');if(u(e,w,"polar frame"),e.overflow!==void 0&&!["error","allow"].includes(e.overflow))throw new Error("Polar frame overflow must be error or allow.");const r=e.center??{};if(!l(r))throw new TypeError("Polar frame center must be a plain object.");u(r,h,"polar frame center");const t=e.radius??{unit:"fraction",value:1};if(!l(t))throw new TypeError("Polar frame radius must be a plain object.");if(u(t,d,"polar frame radius",{allowEmpty:!1,emptyError:TypeError}),!p.has(t.unit))throw new Error(`Unknown Polar frame radius unit "${t.unit}".`);if(!Number.isFinite(t.value)||t.value<=0)throw new RangeError("Polar frame radius must be a positive finite number.");if(t.unit==="fraction"&&t.value>1)throw new RangeError("Polar frame radius fraction must not exceed 1.");return n({center:{x:s(r.x??.5,"Polar frame center x"),y:s(r.y??.5,"Polar frame center y")},radius:{unit:t.unit,value:t.value},...e.overflow==="allow"?{overflow:"allow"}:{}})}function m(e,r="auto"){if(v(e),r===void 0||r==="auto")return n({centerX:e.x+e.width/2,centerY:e.y+e.height/2,availableRadius:Math.min(e.width,e.height)/2});const t=R(r),a=e.x+e.width*t.center.x,i=e.y+e.height*t.center.y,o=Math.min(a-e.x,e.x+e.width-a,i-e.y,e.y+e.height-i);if(!Number.isFinite(o)||o<=0)throw new RangeError("Polar frame center must leave a positive radius inside its bounds.");const f=t.radius.unit==="fraction"?o*t.radius.value:t.radius.value;if(f>o&&t.overflow!=="allow")throw new RangeError(`Polar frame radius ${f} exceeds the maximum radius ${o}.`);return n({centerX:a,centerY:i,availableRadius:f})}function P(e){if(e==="auto")return e;const r=c(e,"Theta scale range");if(Math.abs(r[1]-r[0])>360)throw new RangeError("Theta scale range span must not exceed 360 degrees.");return r}function x(e,r){if(e==="auto")return e;const t=c(e,"Radius scale range");if(t.some(a=>a<0))throw new RangeError("Radius scale range values must be non-negative.");if(r!==void 0&&t.some(a=>a>r))throw new RangeError(`Radius scale range must fit within the available radius ${r}.`);return t}function T(e,r,t,a){if(r==="theta")return e==="auto"?n([0,360]):P(e);if(r!=="radius")throw new Error(`Unknown Polar position channel "${r}".`);const i=a??m(t);return e==="auto"?n([0,i.availableRadius]):x(e,i.availableRadius)}function y(e){if(!Number.isFinite(e))throw new TypeError("Polar theta must be a finite number of degrees.");const r=e*Math.PI/180;return n({x:Math.sin(r),y:-Math.cos(r)})}function b({theta:e,radius:r,frame:t}){const a=y(e);if(!Number.isFinite(r)||r<0)throw new TypeError("Polar radius must be a non-negative finite number.");const i=t?.availableRadius===void 0?m(t):t;if(![i.centerX,i.centerY,i.availableRadius].every(Number.isFinite))throw new TypeError("Polar frame must contain finite geometry.");if(r>i.availableRadius)throw new RangeError(`Polar radius ${r} exceeds the available radius ${i.availableRadius}.`);return n({x:i.centerX+r*a.x,y:i.centerY+r*a.y})}export{R as normalizePolarFrameOptions,y as polarDirection,b as polarToCartesian,m as resolvePolarFrame,T as resolvePolarScaleRange,x as validateRadialRange,P as validateThetaRange};
|
|
@@ -6,14 +6,14 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
|
|
|
6
6
|
radius: Object.freeze(["quantitative"])
|
|
7
7
|
}),
|
|
8
8
|
line: Object.freeze({
|
|
9
|
-
x: Object.freeze(["quantitative", "temporal"]),
|
|
9
|
+
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
10
10
|
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
11
11
|
theta: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
12
12
|
radius: Object.freeze(["quantitative"])
|
|
13
13
|
}),
|
|
14
14
|
area: Object.freeze({
|
|
15
|
-
x: Object.freeze(["quantitative", "temporal"]),
|
|
16
|
-
y: Object.freeze(["quantitative", "temporal"])
|
|
15
|
+
x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
|
|
16
|
+
y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
|
|
17
17
|
}),
|
|
18
18
|
arc: Object.freeze({
|
|
19
19
|
theta: Object.freeze(["quantitative", "ordinal", "nominal"]),
|
|
@@ -24,10 +24,13 @@ export function deriveRegression(values, {
|
|
|
24
24
|
method,
|
|
25
25
|
degree,
|
|
26
26
|
span,
|
|
27
|
+
robustIterations,
|
|
27
28
|
confidenceMethod,
|
|
28
29
|
level,
|
|
29
30
|
confidence,
|
|
30
|
-
interval
|
|
31
|
+
interval,
|
|
32
|
+
predict,
|
|
33
|
+
missing
|
|
31
34
|
} = {}) {
|
|
32
35
|
if (!Array.isArray(values)) {
|
|
33
36
|
throw new TypeError("Regression values must be an array.");
|
|
@@ -41,20 +44,35 @@ export function deriveRegression(values, {
|
|
|
41
44
|
method,
|
|
42
45
|
degree,
|
|
43
46
|
span,
|
|
47
|
+
robustIterations,
|
|
44
48
|
confidenceMethod,
|
|
45
49
|
level,
|
|
46
50
|
confidence,
|
|
47
51
|
interval
|
|
48
52
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
let missingRows = 0;
|
|
54
|
+
const eligibleValues = missing === undefined ? values : values.filter((row, index) => {
|
|
55
|
+
const xValue = row?.[x];
|
|
56
|
+
const yValue = row?.[y];
|
|
57
|
+
if (xValue === null || xValue === undefined || yValue === null || yValue === undefined) {
|
|
58
|
+
if (missing === "error") throw new TypeError(`Regression x/y pair is missing at row ${index}.`);
|
|
59
|
+
missingRows += 1;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (!Number.isFinite(xValue) || !Number.isFinite(yValue)) {
|
|
63
|
+
throw new TypeError(`Regression x/y fields must contain finite numbers at row ${index}.`);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
readQuantitativeField(eligibleValues, x);
|
|
68
|
+
readQuantitativeField(eligibleValues, y);
|
|
69
|
+
if (groupBy !== undefined) readNominalField(eligibleValues, groupBy);
|
|
52
70
|
|
|
53
71
|
const groupedRows = new Map();
|
|
54
72
|
if (groupBy === undefined) {
|
|
55
|
-
groupedRows.set(undefined,
|
|
73
|
+
groupedRows.set(undefined, eligibleValues);
|
|
56
74
|
} else {
|
|
57
|
-
for (const row of
|
|
75
|
+
for (const row of eligibleValues) {
|
|
58
76
|
const group = row[groupBy];
|
|
59
77
|
const rows = groupedRows.get(group) ?? [];
|
|
60
78
|
rows.push(row);
|
|
@@ -68,13 +86,29 @@ export function deriveRegression(values, {
|
|
|
68
86
|
|
|
69
87
|
for (const group of groups) {
|
|
70
88
|
const groupRows = groupedRows.get(group);
|
|
71
|
-
const
|
|
72
|
-
|
|
89
|
+
const observedX = [...new Set(groupRows.map(row => row[x]))]
|
|
90
|
+
.sort((left, right) => left - right);
|
|
91
|
+
const xValues = predict?.values ?? (predict?.domain === undefined
|
|
92
|
+
? observedX
|
|
93
|
+
: Array.from({ length: predict.steps }, (_, index) => {
|
|
94
|
+
if (index === 0) return predict.domain[0];
|
|
95
|
+
if (index === predict.steps - 1) return predict.domain[1];
|
|
96
|
+
const ratio = index / (predict.steps - 1);
|
|
97
|
+
return requireFiniteResult(
|
|
98
|
+
predict.domain[0] * (1 - ratio) + predict.domain[1] * ratio,
|
|
99
|
+
"Regression prediction grid"
|
|
100
|
+
);
|
|
101
|
+
}));
|
|
102
|
+
validateGeneratedItemLimit(rows.length + xValues.length, "Regression generated row count");
|
|
73
103
|
if (parameters.method === "polynomial") {
|
|
74
104
|
const size = parameters.degree + 1;
|
|
75
105
|
work += groupRows.length * size ** 2 + size ** 3;
|
|
76
106
|
} else if (parameters.method === "loess") {
|
|
77
|
-
|
|
107
|
+
const observedSet = new Set(observedX);
|
|
108
|
+
const newPredictions = predict === undefined ? 0 :
|
|
109
|
+
xValues.filter(value => !observedSet.has(value)).length;
|
|
110
|
+
work += groupRows.length *
|
|
111
|
+
(observedX.length * (1 + (parameters.robustIterations ?? 0)) + newPredictions) *
|
|
78
112
|
Math.ceil(Math.log2(groupRows.length + 1));
|
|
79
113
|
}
|
|
80
114
|
validateWorkLimit(work, "Regression computation");
|
|
@@ -84,8 +118,6 @@ export function deriveRegression(values, {
|
|
|
84
118
|
group,
|
|
85
119
|
parameters
|
|
86
120
|
});
|
|
87
|
-
const xValues = [...new Set(groupRows.map(row => row[x]))]
|
|
88
|
-
.sort((left, right) => left - right);
|
|
89
121
|
models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
|
|
90
122
|
|
|
91
123
|
for (const xValue of xValues) {
|
|
@@ -94,13 +126,13 @@ export function deriveRegression(values, {
|
|
|
94
126
|
prediction.prediction,
|
|
95
127
|
`Regression group "${group === undefined ? "all" : String(group)}" prediction`
|
|
96
128
|
);
|
|
97
|
-
const lower = parameters.method === "loess"
|
|
129
|
+
const lower = parameters.method === "loess" || parameters.interval === false
|
|
98
130
|
? undefined
|
|
99
131
|
: requireFiniteResult(
|
|
100
132
|
fitted - prediction.margin,
|
|
101
133
|
`Regression group "${group === undefined ? "all" : String(group)}" lower interval`
|
|
102
134
|
);
|
|
103
|
-
const upper = parameters.method === "loess"
|
|
135
|
+
const upper = parameters.method === "loess" || parameters.interval === false
|
|
104
136
|
? undefined
|
|
105
137
|
: requireFiniteResult(
|
|
106
138
|
fitted + prediction.margin,
|
|
@@ -110,7 +142,7 @@ export function deriveRegression(values, {
|
|
|
110
142
|
...(groupBy === undefined ? {} : { [groupBy]: group }),
|
|
111
143
|
[x]: xValue,
|
|
112
144
|
[y]: fitted,
|
|
113
|
-
...(parameters.method === "loess" ? {} : {
|
|
145
|
+
...(parameters.method === "loess" || parameters.interval === false ? {} : {
|
|
114
146
|
[REGRESSION_LOWER_FIELD]: lower,
|
|
115
147
|
[REGRESSION_UPPER_FIELD]: upper
|
|
116
148
|
})
|
|
@@ -123,7 +155,7 @@ export function deriveRegression(values, {
|
|
|
123
155
|
x,
|
|
124
156
|
y,
|
|
125
157
|
...(groupBy === undefined ? {} : { group: groupBy }),
|
|
126
|
-
...(parameters.method === "loess" ? {} : {
|
|
158
|
+
...(parameters.method === "loess" || parameters.interval === false ? {} : {
|
|
127
159
|
lower: REGRESSION_LOWER_FIELD,
|
|
128
160
|
upper: REGRESSION_UPPER_FIELD
|
|
129
161
|
})
|
|
@@ -131,7 +163,22 @@ export function deriveRegression(values, {
|
|
|
131
163
|
parameters,
|
|
132
164
|
groups,
|
|
133
165
|
models,
|
|
134
|
-
values: rows
|
|
166
|
+
values: rows,
|
|
167
|
+
...(missing === undefined ? {} : {
|
|
168
|
+
report: {
|
|
169
|
+
version: 1,
|
|
170
|
+
owner: { kind: "data", id: "pending" },
|
|
171
|
+
inputs: [],
|
|
172
|
+
units: [{
|
|
173
|
+
role: "fit",
|
|
174
|
+
group: {},
|
|
175
|
+
inputRows: values.length,
|
|
176
|
+
usedRows: eligibleValues.length,
|
|
177
|
+
excludedRows: missingRows,
|
|
178
|
+
excludedByReason: missingRows === 0 ? {} : { "missing-value": missingRows }
|
|
179
|
+
}]
|
|
180
|
+
}
|
|
181
|
+
})
|
|
135
182
|
});
|
|
136
183
|
}
|
|
137
184
|
|
|
@@ -58,14 +58,14 @@ function linearPrediction(model, xValue) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function fitLinearGroup(rows, {
|
|
61
|
-
x, y, group, confidenceMethod, level
|
|
61
|
+
x, y, group, confidenceMethod, level, interval
|
|
62
62
|
}) {
|
|
63
63
|
const count = rows.length;
|
|
64
64
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
65
65
|
const label = `Regression group "${groupLabel}"`;
|
|
66
|
-
if (count < 3) {
|
|
66
|
+
if (count < (interval === false ? 2 : 3)) {
|
|
67
67
|
throw new Error(
|
|
68
|
-
`${label} requires at least three rows.`
|
|
68
|
+
`${label} requires at least ${interval === false ? "two" : "three"} rows.`
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
const xValues = rows.map(row => row[x]);
|
|
@@ -148,6 +148,7 @@ function fitLinearGroup(rows, {
|
|
|
148
148
|
normalizedSxx
|
|
149
149
|
});
|
|
150
150
|
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
151
|
+
if (interval === false) return model;
|
|
151
152
|
const residualSumSquares = stableFiniteSquareSum(
|
|
152
153
|
rows.map(row => row[y] - linearPrediction(model, row[x])),
|
|
153
154
|
`${label} residual sum of squares`
|
|
@@ -238,19 +239,19 @@ function polynomialResponse(design, values, size, scale = 1) {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
function fitPolynomialGroup(rows, {
|
|
241
|
-
x, y, group, confidenceMethod, level, degree
|
|
242
|
+
x, y, group, confidenceMethod, level, degree, interval
|
|
242
243
|
}) {
|
|
243
244
|
const count = rows.length;
|
|
244
245
|
const parameterCount = degree + 1;
|
|
245
246
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
246
247
|
const label = `Polynomial regression group "${groupLabel}"`;
|
|
247
248
|
if (
|
|
248
|
-
count < degree + 2 ||
|
|
249
|
+
count < (interval === false ? parameterCount : degree + 2) ||
|
|
249
250
|
new Set(rows.map(row => row[x])).size < parameterCount
|
|
250
251
|
) {
|
|
251
252
|
throw new Error(
|
|
252
253
|
`${label} requires at least ` +
|
|
253
|
-
`${degree + 2} rows and ${parameterCount} distinct x values.`
|
|
254
|
+
`${interval === false ? parameterCount : degree + 2} rows and ${parameterCount} distinct x values.`
|
|
254
255
|
);
|
|
255
256
|
}
|
|
256
257
|
const xValues = rows.map(row => row[x]);
|
|
@@ -289,7 +290,7 @@ function fitPolynomialGroup(rows, {
|
|
|
289
290
|
`${label} coefficient ${index}`
|
|
290
291
|
))
|
|
291
292
|
: scaledCoefficients;
|
|
292
|
-
const inverse = invertSymmetricMatrix(normal);
|
|
293
|
+
const inverse = interval === false ? undefined : invertSymmetricMatrix(normal);
|
|
293
294
|
const fitted = design.map(basis => stableResponse
|
|
294
295
|
? restoreFiniteScale(
|
|
295
296
|
dot(basis, scaledCoefficients),
|
|
@@ -298,12 +299,12 @@ function fitPolynomialGroup(rows, {
|
|
|
298
299
|
)
|
|
299
300
|
: dot(basis, normalizedCoefficients)
|
|
300
301
|
);
|
|
301
|
-
const residualSumSquares = stableFiniteSquareSum(
|
|
302
|
+
const residualSumSquares = interval === false ? undefined : stableFiniteSquareSum(
|
|
302
303
|
fitted.map((value, index) => rows[index][y] - value),
|
|
303
304
|
`${label} residual sum of squares`
|
|
304
305
|
);
|
|
305
306
|
const degreesOfFreedom = count - parameterCount;
|
|
306
|
-
const residualVariance = residualSumSquares / degreesOfFreedom;
|
|
307
|
+
const residualVariance = interval === false ? undefined : residualSumSquares / degreesOfFreedom;
|
|
307
308
|
const model = {
|
|
308
309
|
count,
|
|
309
310
|
degreesOfFreedom,
|
|
@@ -316,13 +317,15 @@ function fitPolynomialGroup(rows, {
|
|
|
316
317
|
normalizedCoefficients,
|
|
317
318
|
center,
|
|
318
319
|
scale,
|
|
319
|
-
inverse,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
...(inverse === undefined ? {} : { inverse }),
|
|
321
|
+
...(interval === false ? {} : {
|
|
322
|
+
residualSumSquares,
|
|
323
|
+
residualStandardError: Math.sqrt(residualVariance),
|
|
324
|
+
critical: confidenceCriticalValue({
|
|
325
|
+
method: confidenceMethod,
|
|
326
|
+
level,
|
|
327
|
+
degreesOfFreedom
|
|
328
|
+
})
|
|
326
329
|
})
|
|
327
330
|
};
|
|
328
331
|
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
@@ -345,7 +348,9 @@ function evaluatePolynomial(model, xValue) {
|
|
|
345
348
|
model.responseScale,
|
|
346
349
|
"Polynomial regression prediction"
|
|
347
350
|
),
|
|
348
|
-
|
|
351
|
+
...(model.inverse === undefined ? {} : {
|
|
352
|
+
leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
|
|
353
|
+
})
|
|
349
354
|
};
|
|
350
355
|
}
|
|
351
356
|
|
|
@@ -371,7 +376,56 @@ function weightedPrediction(items, x, y, xValue, xScale = 1, yScale = 1) {
|
|
|
371
376
|
return meanY - (variance === 0 ? 0 : covariance / variance) * meanDifference;
|
|
372
377
|
}
|
|
373
378
|
|
|
374
|
-
function
|
|
379
|
+
function applyResidualWeights(neighbors, residualWeights) {
|
|
380
|
+
if (!neighbors.some(item => item.weight > 0)) {
|
|
381
|
+
neighbors = neighbors.map(item => ({ ...item, weight: 1 }));
|
|
382
|
+
}
|
|
383
|
+
if (residualWeights === undefined) return neighbors;
|
|
384
|
+
const weighted = neighbors.map(item => ({
|
|
385
|
+
...item, weight: item.weight * residualWeights[item.index]
|
|
386
|
+
}));
|
|
387
|
+
return weighted.some(item => item.weight > 0) ? weighted : neighbors;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function loessPrediction(rows, {
|
|
391
|
+
x, y, xValue, xScale, yScale, neighborCount, residualWeights, scaled = false
|
|
392
|
+
}, label) {
|
|
393
|
+
const rawDistances = rows.map(row => Math.abs(row[x] - xValue));
|
|
394
|
+
const scaledDistances = scaled || rawDistances.some(value => !Number.isFinite(value));
|
|
395
|
+
const neighbors = rows
|
|
396
|
+
.map((row, index) => ({
|
|
397
|
+
row,
|
|
398
|
+
index,
|
|
399
|
+
distance: scaledDistances
|
|
400
|
+
? Math.abs(row[x] / xScale - xValue / xScale)
|
|
401
|
+
: rawDistances[index]
|
|
402
|
+
}))
|
|
403
|
+
.sort((left, right) =>
|
|
404
|
+
left.distance - right.distance || left.index - right.index
|
|
405
|
+
)
|
|
406
|
+
.slice(0, neighborCount);
|
|
407
|
+
const radius = neighbors.at(-1).distance;
|
|
408
|
+
let weighted = neighbors.map(neighbor => ({
|
|
409
|
+
...neighbor,
|
|
410
|
+
weight: radius === 0
|
|
411
|
+
? 1
|
|
412
|
+
: (1 - (neighbor.distance / radius) ** 3) ** 3
|
|
413
|
+
}));
|
|
414
|
+
weighted = applyResidualWeights(weighted, residualWeights);
|
|
415
|
+
const ordinary = weightedPrediction(weighted, x, y, xValue);
|
|
416
|
+
return {
|
|
417
|
+
prediction: Number.isFinite(ordinary)
|
|
418
|
+
? ordinary
|
|
419
|
+
: restoreFiniteScale(
|
|
420
|
+
weightedPrediction(weighted, x, y, xValue, xScale, yScale),
|
|
421
|
+
yScale,
|
|
422
|
+
label
|
|
423
|
+
),
|
|
424
|
+
neighborIndices: weighted.map(item => item.index)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function fitLoessGroup(rows, { x, y, group, span, robustIterations = 0 }) {
|
|
375
429
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
376
430
|
const label = `LOESS regression group "${groupLabel}"`;
|
|
377
431
|
if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
|
|
@@ -385,42 +439,42 @@ function fitLoessGroup(rows, { x, y, group, span }) {
|
|
|
385
439
|
.sort((left, right) => left - right);
|
|
386
440
|
const xScale = maximumMagnitude(rows.map(row => row[x])) || 1;
|
|
387
441
|
const yScale = maximumMagnitude(rows.map(row => row[y])) || 1;
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
442
|
+
const fitAtObserved = residualWeights => xValues.map(xValue => ({
|
|
443
|
+
x: xValue,
|
|
444
|
+
...loessPrediction(rows, {
|
|
445
|
+
x, y, xValue, xScale, yScale, neighborCount, residualWeights
|
|
446
|
+
}, `${label} prediction`)
|
|
447
|
+
}));
|
|
448
|
+
let fits = fitAtObserved();
|
|
449
|
+
let residualWeights;
|
|
450
|
+
for (let iteration = 0; iteration < robustIterations; iteration += 1) {
|
|
451
|
+
const predictions = new Map(fits.map(fit => [fit.x, fit.prediction]));
|
|
452
|
+
const residuals = rows.map(row => Math.abs(row[y] / yScale - predictions.get(row[x]) / yScale));
|
|
453
|
+
const sorted = [...residuals].sort((a, b) => a - b);
|
|
454
|
+
const middle = Math.floor(sorted.length / 2);
|
|
455
|
+
const median = sorted.length % 2 === 0
|
|
456
|
+
? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
457
|
+
const tolerance = 64 * Number.EPSILON;
|
|
458
|
+
const cutoff = 6 * median;
|
|
459
|
+
const nextWeights = residuals.map(residual => {
|
|
460
|
+
if (cutoff <= tolerance) return residual <= tolerance ? 1 : 0;
|
|
461
|
+
const ratio = residual / cutoff;
|
|
462
|
+
return ratio >= 1 ? 0 : (1 - ratio * ratio) ** 2;
|
|
463
|
+
});
|
|
464
|
+
if (nextWeights.every((weight, index) => weight === (residualWeights?.[index] ?? 1))) break;
|
|
465
|
+
residualWeights = nextWeights;
|
|
466
|
+
fits = fitAtObserved(residualWeights);
|
|
467
|
+
}
|
|
468
|
+
return { count: rows.length, span, neighborCount, fits, rows, x, y, xScale, yScale,
|
|
469
|
+
...(robustIterations === 0 ? {} : { robustIterations, residualWeights: residualWeights ?? rows.map(() => 1) }) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function predictLoessAt(model, xValue) {
|
|
473
|
+
const found = model.fits.find(fit => fit.x === xValue);
|
|
474
|
+
if (found !== undefined) return found.prediction;
|
|
475
|
+
return loessPrediction(model.rows, {
|
|
476
|
+
...model, xValue, scaled: true
|
|
477
|
+
}, "LOESS regression prediction").prediction;
|
|
424
478
|
}
|
|
425
479
|
|
|
426
480
|
export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
@@ -430,7 +484,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
430
484
|
y,
|
|
431
485
|
group,
|
|
432
486
|
confidenceMethod: parameters.confidenceMethod,
|
|
433
|
-
level: parameters.level
|
|
487
|
+
level: parameters.level,
|
|
488
|
+
interval: parameters.interval
|
|
434
489
|
});
|
|
435
490
|
}
|
|
436
491
|
if (parameters.method === "polynomial") {
|
|
@@ -440,10 +495,12 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
440
495
|
group,
|
|
441
496
|
confidenceMethod: parameters.confidenceMethod,
|
|
442
497
|
level: parameters.level,
|
|
443
|
-
degree: parameters.degree
|
|
498
|
+
degree: parameters.degree,
|
|
499
|
+
interval: parameters.interval
|
|
444
500
|
});
|
|
445
501
|
}
|
|
446
|
-
return fitLoessGroup(rows, { x, y, group, span: parameters.span
|
|
502
|
+
return fitLoessGroup(rows, { x, y, group, span: parameters.span,
|
|
503
|
+
robustIterations: parameters.robustIterations });
|
|
447
504
|
}
|
|
448
505
|
|
|
449
506
|
export function predictRegressionAt(model, xValue, parameters) {
|
|
@@ -454,8 +511,8 @@ export function predictRegressionAt(model, xValue, parameters) {
|
|
|
454
511
|
? linearPrediction(model, xValue)
|
|
455
512
|
: parameters.method === "polynomial"
|
|
456
513
|
? polynomial.prediction
|
|
457
|
-
: model
|
|
458
|
-
if (parameters.method === "loess") return { prediction };
|
|
514
|
+
: predictLoessAt(model, xValue);
|
|
515
|
+
if (parameters.method === "loess" || parameters.interval === false) return { prediction };
|
|
459
516
|
const leverage = parameters.method === "linear"
|
|
460
517
|
? model.normalizedSxx !== undefined
|
|
461
518
|
? 1 / model.count + (
|