ggaction 0.0.17 → 0.0.19
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 +96 -0
- package/README.md +6 -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 +3 -3
- 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 +20 -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 +8 -7
- 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/box.js +2 -1
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- 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 +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- 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 +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- 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 +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- 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/guides/tickValues.js +8 -5
- 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 +10 -4
- package/src/actions/marks/bar/edit.js +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- 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/compositionState.js +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -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/axisTextPlacement.js +1 -0
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -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/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- 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 -197
- 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 +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/guideCollisions.js +9 -1
- 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 +22 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- 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/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -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 +7 -3
- 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 +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- 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/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- 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 +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyString } from "../core/validation.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import { validateUserId } from "../core/identifiers.js";
|
|
3
4
|
import { validateOptionObject } from "../core/validation.js";
|
|
@@ -18,12 +19,6 @@ const DIMENSION_SCALE_OPTIONS = Object.freeze([
|
|
|
18
19
|
"padding", "align", "unknown"
|
|
19
20
|
]);
|
|
20
21
|
|
|
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
22
|
|
|
28
23
|
export function validateParallelKeyField(value) {
|
|
29
24
|
return nonEmptyString(value, "Parallel key field");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
2
|
+
|
|
3
|
+
export const PARSE_TIME_FORMATS = Object.freeze([
|
|
4
|
+
"auto", "%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export function validateParseTimeFormat(format = "auto") {
|
|
8
|
+
if (!PARSE_TIME_FORMATS.includes(format)) {
|
|
9
|
+
throw new Error(`Unsupported parseTime format "${format}".`);
|
|
10
|
+
}
|
|
11
|
+
return format;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Explicit calendar formats are UTC and reject rollover, rather than guessing
|
|
15
|
+
// the day/month order. Invalid or nullish values remain missing data as null.
|
|
16
|
+
export function parseTimeValue(value, format = "auto") {
|
|
17
|
+
validateParseTimeFormat(format);
|
|
18
|
+
if (value === null || value === undefined) return null;
|
|
19
|
+
if (format === "auto") {
|
|
20
|
+
try { return normalizeTemporalValue(value); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof TypeError || error instanceof RangeError) return null;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (typeof value !== "string") return null;
|
|
27
|
+
const match = (format === "%Y-%m-%d"
|
|
28
|
+
? /^(\d{4})-(\d{1,2})-(\d{1,2})$/
|
|
29
|
+
: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/).exec(value.trim());
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
const numbers = match.slice(1).map(Number);
|
|
32
|
+
const [year, month, day] = format === "%Y-%m-%d" ? numbers
|
|
33
|
+
: format === "%d/%m/%Y" ? [numbers[2], numbers[1], numbers[0]]
|
|
34
|
+
: [numbers[2], numbers[0], numbers[1]];
|
|
35
|
+
const date = new Date(0);
|
|
36
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
37
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
38
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1
|
|
39
|
+
&& date.getUTCDate() === day && Number.isFinite(date.getTime())
|
|
40
|
+
? date.getTime() : null;
|
|
41
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
3
3
|
|
|
4
|
+
export const MAX_PATH_COMMANDS = 20_000;
|
|
5
|
+
|
|
6
|
+
export function validatePathCommandCount(count, label) {
|
|
7
|
+
return validateGeneratedItemLimit(count, label, MAX_PATH_COMMANDS);
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
const COMMAND_KEYS = Object.freeze({
|
|
5
11
|
M: Object.freeze(["op", "x", "y"]),
|
|
6
12
|
L: Object.freeze(["op", "x", "y"]),
|
|
@@ -35,14 +41,15 @@ function validateCommand(command, index) {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function validatePathCommands(commands) {
|
|
38
|
-
if (!Array.isArray(commands) || commands.length <
|
|
39
|
-
throw new TypeError("path.commands must contain at least
|
|
44
|
+
if (!Array.isArray(commands) || commands.length < 1) {
|
|
45
|
+
throw new TypeError("path.commands must contain at least one command.");
|
|
40
46
|
}
|
|
41
|
-
|
|
47
|
+
validatePathCommandCount(commands.length, "path.commands length");
|
|
42
48
|
commands.forEach(validateCommand);
|
|
43
49
|
if (commands[0].op !== "M") {
|
|
44
50
|
throw new Error("path.commands must start with M.");
|
|
45
51
|
}
|
|
52
|
+
if (commands.length === 1) return commands;
|
|
46
53
|
if (commands.slice(1).some(command => command.op === "M")) {
|
|
47
54
|
throw new Error("path.commands supports one subpath and only one initial M.");
|
|
48
55
|
}
|
|
@@ -79,7 +86,7 @@ export function buildLinearPathCommands(points, { close = false } = {}) {
|
|
|
79
86
|
if (typeof close !== "boolean") {
|
|
80
87
|
throw new TypeError("Linear path close must be a boolean.");
|
|
81
88
|
}
|
|
82
|
-
|
|
89
|
+
validatePathCommandCount(
|
|
83
90
|
points.length + Number(close),
|
|
84
91
|
"Linear path command count"
|
|
85
92
|
);
|
|
@@ -14,7 +14,9 @@ export const POINT_SHAPES = cloneAndFreeze([
|
|
|
14
14
|
"cross",
|
|
15
15
|
"star",
|
|
16
16
|
"hexagon",
|
|
17
|
-
"wye"
|
|
17
|
+
"wye",
|
|
18
|
+
"wedge",
|
|
19
|
+
"arrow"
|
|
18
20
|
]);
|
|
19
21
|
|
|
20
22
|
const SHAPE_SET = new Set(POINT_SHAPES);
|
|
@@ -98,7 +100,19 @@ function shapePolygon(shape) {
|
|
|
98
100
|
cross: rotate(plus, Math.PI / 4),
|
|
99
101
|
star: alternatingPolygon(5, 0.45),
|
|
100
102
|
hexagon: regularPolygon(6),
|
|
101
|
-
wye: alternatingPolygon(3, 0.28)
|
|
103
|
+
wye: alternatingPolygon(3, 0.28),
|
|
104
|
+
wedge: [
|
|
105
|
+
{ x: 0, y: -2 / Math.sqrt(3) },
|
|
106
|
+
{ x: -1 / 4, y: 1 / Math.sqrt(3) },
|
|
107
|
+
{ x: 1 / 4, y: 1 / Math.sqrt(3) }
|
|
108
|
+
],
|
|
109
|
+
// The directional arrow is anchored at its bounding-box center, not its centroid.
|
|
110
|
+
arrow: [
|
|
111
|
+
{ x: -1 / 7, y: 1 }, { x: 1 / 7, y: 1 },
|
|
112
|
+
{ x: 1 / 7, y: -1 / 8 }, { x: 2 / 5, y: -1 / 8 },
|
|
113
|
+
{ x: 0, y: -1 }, { x: -2 / 5, y: -1 / 8 },
|
|
114
|
+
{ x: -1 / 7, y: -1 / 8 }
|
|
115
|
+
]
|
|
102
116
|
}[shape];
|
|
103
117
|
}
|
|
104
118
|
|
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"]),
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
readNominalField,
|
|
4
4
|
readQuantitativeField
|
|
5
5
|
} from "../scales/index.js";
|
|
6
|
-
import { fitRegressionGroup, predictRegressionAt } from "./models.js";
|
|
6
|
+
import { fitRegressionGroup, predictRegressionAt, linearRegressionStatistics } from "./models.js";
|
|
7
7
|
import { requireFiniteResult } from "../numeric.js";
|
|
8
8
|
import {
|
|
9
9
|
validateGeneratedItemLimit,
|
|
@@ -24,6 +24,7 @@ export function deriveRegression(values, {
|
|
|
24
24
|
method,
|
|
25
25
|
degree,
|
|
26
26
|
span,
|
|
27
|
+
robustIterations,
|
|
27
28
|
confidenceMethod,
|
|
28
29
|
level,
|
|
29
30
|
confidence,
|
|
@@ -43,6 +44,7 @@ export function deriveRegression(values, {
|
|
|
43
44
|
method,
|
|
44
45
|
degree,
|
|
45
46
|
span,
|
|
47
|
+
robustIterations,
|
|
46
48
|
confidenceMethod,
|
|
47
49
|
level,
|
|
48
50
|
confidence,
|
|
@@ -79,6 +81,7 @@ export function deriveRegression(values, {
|
|
|
79
81
|
}
|
|
80
82
|
const groups = [...groupedRows.keys()];
|
|
81
83
|
const models = [];
|
|
84
|
+
const statistics = [];
|
|
82
85
|
const rows = [];
|
|
83
86
|
let work = 0;
|
|
84
87
|
|
|
@@ -102,7 +105,11 @@ export function deriveRegression(values, {
|
|
|
102
105
|
const size = parameters.degree + 1;
|
|
103
106
|
work += groupRows.length * size ** 2 + size ** 3;
|
|
104
107
|
} else if (parameters.method === "loess") {
|
|
105
|
-
|
|
108
|
+
const observedSet = new Set(observedX);
|
|
109
|
+
const newPredictions = predict === undefined ? 0 :
|
|
110
|
+
xValues.filter(value => !observedSet.has(value)).length;
|
|
111
|
+
work += groupRows.length *
|
|
112
|
+
(observedX.length * (1 + (parameters.robustIterations ?? 0)) + newPredictions) *
|
|
106
113
|
Math.ceil(Math.log2(groupRows.length + 1));
|
|
107
114
|
}
|
|
108
115
|
validateWorkLimit(work, "Regression computation");
|
|
@@ -113,6 +120,10 @@ export function deriveRegression(values, {
|
|
|
113
120
|
parameters
|
|
114
121
|
});
|
|
115
122
|
models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
|
|
123
|
+
if (parameters.method === "linear") statistics.push({
|
|
124
|
+
...(groupBy === undefined ? {} : { group }),
|
|
125
|
+
...linearRegressionStatistics(groupRows, model, { x, y })
|
|
126
|
+
});
|
|
116
127
|
|
|
117
128
|
for (const xValue of xValues) {
|
|
118
129
|
const prediction = predictRegressionAt(model, xValue, parameters);
|
|
@@ -157,6 +168,9 @@ export function deriveRegression(values, {
|
|
|
157
168
|
parameters,
|
|
158
169
|
groups,
|
|
159
170
|
models,
|
|
171
|
+
...(parameters.method === "linear" ? {
|
|
172
|
+
fitStatistics: { version: 1, method: "linear", groups: statistics }
|
|
173
|
+
} : {}),
|
|
160
174
|
values: rows,
|
|
161
175
|
...(missing === undefined ? {} : {
|
|
162
176
|
report: {
|
|
@@ -376,7 +376,56 @@ function weightedPrediction(items, x, y, xValue, xScale = 1, yScale = 1) {
|
|
|
376
376
|
return meanY - (variance === 0 ? 0 : covariance / variance) * meanDifference;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
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 }) {
|
|
380
429
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
381
430
|
const label = `LOESS regression group "${groupLabel}"`;
|
|
382
431
|
if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
|
|
@@ -390,67 +439,100 @@ function fitLoessGroup(rows, { x, y, group, span }) {
|
|
|
390
439
|
.sort((left, right) => left - right);
|
|
391
440
|
const xScale = maximumMagnitude(rows.map(row => row[x])) || 1;
|
|
392
441
|
const yScale = maximumMagnitude(rows.map(row => row[y])) || 1;
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
weightedPrediction(weighted, x, y, xValue, xScale, yScale),
|
|
422
|
-
yScale,
|
|
423
|
-
`${label} prediction`
|
|
424
|
-
),
|
|
425
|
-
neighborIndices: weighted.map(item => item.index)
|
|
426
|
-
};
|
|
427
|
-
});
|
|
428
|
-
return { count: rows.length, span, neighborCount, fits, rows, x, y, xScale, yScale };
|
|
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) }) };
|
|
429
470
|
}
|
|
430
471
|
|
|
431
472
|
function predictLoessAt(model, xValue) {
|
|
432
473
|
const found = model.fits.find(fit => fit.x === xValue);
|
|
433
474
|
if (found !== undefined) return found.prediction;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
475
|
+
return loessPrediction(model.rows, {
|
|
476
|
+
...model, xValue, scaled: true
|
|
477
|
+
}, "LOESS regression prediction").prediction;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function fitExponentialGroup(rows, { x, y, group }) {
|
|
481
|
+
const label = `Regression group "${group === undefined ? "all" : String(group)}"`;
|
|
482
|
+
if (rows.length < 2) throw new Error(`${label} requires at least two rows.`);
|
|
483
|
+
if (rows.some(row => !(row[y] > 0))) {
|
|
484
|
+
throw new RangeError("Exponential regression y values must be strictly positive.");
|
|
485
|
+
}
|
|
486
|
+
const xValues = rows.map(row => row[x]);
|
|
487
|
+
if (new Set(xValues).size < 2) throw new Error(`${label} requires varying x values.`);
|
|
488
|
+
const responseScale = maximumMagnitude(rows.map(row => row[y]));
|
|
489
|
+
const xMagnitude = maximumMagnitude(xValues) || 1;
|
|
490
|
+
const weights = rows.map(row => row[y] / responseScale);
|
|
491
|
+
const weightSum = weights.reduce((sum, value) => sum + value, 0);
|
|
492
|
+
const logs = rows.map(row => Math.log(row[y]));
|
|
493
|
+
const meanX = restoreFiniteScale(rows.reduce((sum, row, index) =>
|
|
494
|
+
sum + weights[index] * (row[x] / xMagnitude), 0) / weightSum,
|
|
495
|
+
xMagnitude, `${label} weighted x mean`);
|
|
496
|
+
const meanLogY = logs.reduce((sum, value, index) => sum + weights[index] * value, 0) / weightSum;
|
|
497
|
+
const [centeredX, xScale] = normalizedCoordinates(xValues, meanX);
|
|
498
|
+
const variance = centeredX.reduce((sum, value, index) => sum + weights[index] * value ** 2, 0);
|
|
499
|
+
if (variance === 0) throw new Error(`${label} requires varying x values.`);
|
|
500
|
+
const covariance = centeredX.reduce((sum, value, index) =>
|
|
501
|
+
sum + weights[index] * value * (logs[index] - meanLogY), 0);
|
|
502
|
+
const rate = restoreFiniteRatio(covariance / variance, 1, xScale, `${label} exponential rate`);
|
|
503
|
+
const logAmplitude = requireFiniteResult(meanLogY - rate * meanX, `${label} exponential log amplitude`);
|
|
504
|
+
const amplitude = requireFiniteResult(Math.exp(logAmplitude), `${label} exponential amplitude`);
|
|
505
|
+
if (amplitude === 0) throw new RangeError("Exponential regression amplitude must be positive and representable.");
|
|
506
|
+
return { count: rows.length, amplitude, rate, logAmplitude };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function linearRegressionStatistics(rows, model, { x, y }) {
|
|
510
|
+
const responseScale = maximumMagnitude(rows.map(row => row[y])) || 1;
|
|
511
|
+
const [coordinates, xScale] = normalizedCoordinates(rows.map(row => row[x]), model.meanX);
|
|
512
|
+
const normalizedSlope = model.normalizedResponseSlope ?? (
|
|
513
|
+
Number.isFinite(model.slope * xScale) && (model.slope * xScale !== 0 || model.slope === 0)
|
|
514
|
+
? model.slope * xScale / responseScale
|
|
515
|
+
: restoreFiniteRatio(model.slope, xScale, responseScale, "Linear regression normalized slope")
|
|
450
516
|
);
|
|
517
|
+
let residualSquares = 0;
|
|
518
|
+
let totalSquares = 0;
|
|
519
|
+
rows.forEach((row, index) => {
|
|
520
|
+
const difference = row[y] - model.meanY;
|
|
521
|
+
const centered = Number.isFinite(difference)
|
|
522
|
+
? difference / responseScale
|
|
523
|
+
: row[y] / responseScale - model.meanY / responseScale;
|
|
524
|
+
const residual = centered - normalizedSlope * coordinates[index];
|
|
525
|
+
totalSquares += centered ** 2;
|
|
526
|
+
residualSquares += residual ** 2;
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
count: rows.length, intercept: model.intercept, slope: model.slope,
|
|
530
|
+
rSquared: totalSquares === 0 ? null : 1 - residualSquares / totalSquares
|
|
531
|
+
};
|
|
451
532
|
}
|
|
452
533
|
|
|
453
534
|
export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
535
|
+
if (parameters.method === "exp") return fitExponentialGroup(rows, { x, y, group });
|
|
454
536
|
if (parameters.method === "linear") {
|
|
455
537
|
return fitLinearGroup(rows, {
|
|
456
538
|
x,
|
|
@@ -472,7 +554,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
472
554
|
interval: parameters.interval
|
|
473
555
|
});
|
|
474
556
|
}
|
|
475
|
-
return fitLoessGroup(rows, { x, y, group, span: parameters.span
|
|
557
|
+
return fitLoessGroup(rows, { x, y, group, span: parameters.span,
|
|
558
|
+
robustIterations: parameters.robustIterations });
|
|
476
559
|
}
|
|
477
560
|
|
|
478
561
|
export function predictRegressionAt(model, xValue, parameters) {
|
|
@@ -481,9 +564,14 @@ export function predictRegressionAt(model, xValue, parameters) {
|
|
|
481
564
|
: undefined;
|
|
482
565
|
const prediction = parameters.method === "linear"
|
|
483
566
|
? linearPrediction(model, xValue)
|
|
567
|
+
: parameters.method === "exp"
|
|
568
|
+
? requireFiniteResult(Math.exp(model.logAmplitude + model.rate * xValue), "Exponential regression prediction")
|
|
484
569
|
: parameters.method === "polynomial"
|
|
485
570
|
? polynomial.prediction
|
|
486
571
|
: predictLoessAt(model, xValue);
|
|
572
|
+
if (parameters.method === "exp" && prediction === 0) {
|
|
573
|
+
throw new RangeError("Exponential regression prediction must be positive and representable.");
|
|
574
|
+
}
|
|
487
575
|
if (parameters.method === "loess" || parameters.interval === false) return { prediction };
|
|
488
576
|
const leverage = parameters.method === "linear"
|
|
489
577
|
? model.normalizedSxx !== undefined
|