ggaction 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +5 -4
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/extension-authoring.md +248 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +19 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +8 -24
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +24 -5
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +78 -27
- package/src/actions/errorBars/edit.js +81 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +5 -3
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +53 -53
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/extension.js +8 -1
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +58 -11
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +117 -61
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +10 -14
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +2 -1
- package/src/materialization/facetGuides/placement.js +34 -19
- package/src/materialization/facetGuides/preparation.js +9 -5
- package/src/materialization/facets.js +39 -27
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +5 -2
- package/src/materialization/marks/index.js +24 -4
- package/src/materialization/scales/policies/bar.js +61 -59
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +4 -5
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/extension.d.ts +19 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +509 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
package/src/core/textMetrics.js
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
1
1
|
function codePointWidth(codePoint) {
|
|
2
2
|
if (/\s/u.test(codePoint)) return 0.28;
|
|
3
|
-
if (/[
|
|
3
|
+
if (/[iIl.,:;!'|]/u.test(codePoint)) return 0.27;
|
|
4
4
|
if (/[mwMW@#%&]/u.test(codePoint)) return 0.82;
|
|
5
|
-
if (/[A-
|
|
6
|
-
if (/[0-9]/u.test(codePoint)) return 0.53;
|
|
5
|
+
if (/[A-Z0-9]/u.test(codePoint)) return 0.61;
|
|
7
6
|
if (/[-_]/u.test(codePoint)) return 0.34;
|
|
8
7
|
if (codePoint.codePointAt(0) > 0x7f) return 1;
|
|
9
8
|
return 0.47;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
export function
|
|
11
|
+
export function formatVisibleText(value) {
|
|
12
|
+
return String(value) || "(empty)";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function textBoundsIntersect(first, second) {
|
|
16
|
+
return first.left < second.right && first.right > second.left &&
|
|
17
|
+
first.top < second.bottom && first.bottom > second.top;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function textBoundsFitCanvas(bounds, canvas) {
|
|
21
|
+
const epsilon = 1e-9;
|
|
22
|
+
return bounds.left >= -epsilon && bounds.right <= canvas.width + epsilon &&
|
|
23
|
+
bounds.top >= -epsilon && bounds.bottom <= canvas.height + epsilon;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function measureTextWidth(
|
|
27
|
+
text,
|
|
28
|
+
{ fontSize, fontFamily, fontWeight } = {}
|
|
29
|
+
) {
|
|
13
30
|
if (typeof text !== "string") {
|
|
14
31
|
throw new TypeError("Text measurement requires a string.");
|
|
15
32
|
}
|
|
16
33
|
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
17
34
|
throw new RangeError("Text measurement requires a positive fontSize.");
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
let joined = false;
|
|
37
|
+
let width = 0;
|
|
38
|
+
for (const codePoint of text) {
|
|
39
|
+
if (codePoint === "\u200d") {
|
|
40
|
+
joined = true;
|
|
41
|
+
} else if (!/\p{Mark}|\p{Format}|\p{Emoji_Modifier}/u.test(codePoint)) {
|
|
42
|
+
if (joined) joined = false;
|
|
43
|
+
else width += codePointWidth(codePoint) * fontSize;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const family = typeof fontFamily === "string" ? fontFamily.toLowerCase() : "";
|
|
47
|
+
const familyFactor = /mono/u.test(family)
|
|
48
|
+
? 1.08
|
|
49
|
+
: /serif/u.test(family) && !/sans-serif/u.test(family) ? 1.03 : 1;
|
|
50
|
+
const weight = Number(fontWeight);
|
|
51
|
+
width *= familyFactor * ((
|
|
52
|
+
Number.isFinite(weight)
|
|
53
|
+
? weight >= 600
|
|
54
|
+
: /bold|black|heavy/iu.test(fontWeight ?? "")
|
|
55
|
+
) ? 1.04 : 1);
|
|
56
|
+
if (!Number.isFinite(width)) {
|
|
57
|
+
throw new RangeError("Text measurement exceeds the finite numeric range.");
|
|
58
|
+
}
|
|
59
|
+
return width;
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
62
|
+
export function requireFiniteBounds(bounds, label = "Graphic") {
|
|
63
|
+
if (bounds !== undefined && !Object.values(bounds).every(Number.isFinite)) {
|
|
64
|
+
throw new RangeError(`${label} bounds exceed the finite numeric range.`);
|
|
65
|
+
}
|
|
66
|
+
return bounds;
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
export function resolveTextBounds({
|
|
@@ -40,6 +71,8 @@ export function resolveTextBounds({
|
|
|
40
71
|
y,
|
|
41
72
|
text,
|
|
42
73
|
fontSize,
|
|
74
|
+
fontFamily,
|
|
75
|
+
fontWeight,
|
|
43
76
|
textAlign = "left",
|
|
44
77
|
textBaseline = "alphabetic",
|
|
45
78
|
rotation = 0
|
|
@@ -47,24 +80,28 @@ export function resolveTextBounds({
|
|
|
47
80
|
if (![x, y, rotation].every(Number.isFinite)) {
|
|
48
81
|
throw new TypeError("Text bounds require finite x, y, and rotation values.");
|
|
49
82
|
}
|
|
50
|
-
const width = measureTextWidth(text, { fontSize });
|
|
51
|
-
const [left, right] =
|
|
52
|
-
|
|
83
|
+
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight });
|
|
84
|
+
const [left, right] = textAlign === "center"
|
|
85
|
+
? [-width / 2, width / 2]
|
|
86
|
+
: ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
|
|
87
|
+
const [top, bottom] = textBaseline === "middle"
|
|
88
|
+
? [-fontSize / 2, fontSize / 2]
|
|
89
|
+
: ["top", "hanging"].includes(textBaseline)
|
|
90
|
+
? [0, fontSize]
|
|
91
|
+
: ["bottom", "ideographic"].includes(textBaseline)
|
|
92
|
+
? [-fontSize, 0]
|
|
93
|
+
: [-fontSize * 0.8, fontSize * 0.2];
|
|
53
94
|
const cosine = Math.cos(rotation);
|
|
54
95
|
const sine = Math.sin(rotation);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
right: Math.max(...corners.map(point => point.x)),
|
|
67
|
-
top: Math.min(...corners.map(point => point.y)),
|
|
68
|
-
bottom: Math.max(...corners.map(point => point.y))
|
|
69
|
-
});
|
|
96
|
+
const bounds = {
|
|
97
|
+
left: x + Math.min(left * cosine, right * cosine) -
|
|
98
|
+
Math.max(top * sine, bottom * sine),
|
|
99
|
+
right: x + Math.max(left * cosine, right * cosine) -
|
|
100
|
+
Math.min(top * sine, bottom * sine),
|
|
101
|
+
top: y + Math.min(left * sine, right * sine) +
|
|
102
|
+
Math.min(top * cosine, bottom * cosine),
|
|
103
|
+
bottom: y + Math.max(left * sine, right * sine) +
|
|
104
|
+
Math.max(top * cosine, bottom * cosine)
|
|
105
|
+
};
|
|
106
|
+
return Object.freeze(requireFiniteBounds(bounds, "Text"));
|
|
70
107
|
}
|
package/src/core/validation.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { isPlainObject } from "./immutable.js";
|
|
2
2
|
|
|
3
|
+
export const MAX_GENERATED_ITEMS = 10_000;
|
|
4
|
+
export const MAX_WORK_ITEMS = 10_000_000;
|
|
5
|
+
|
|
6
|
+
export function validateGeneratedItemLimit(
|
|
7
|
+
value,
|
|
8
|
+
label,
|
|
9
|
+
maximum = MAX_GENERATED_ITEMS
|
|
10
|
+
) {
|
|
11
|
+
if (value > maximum) {
|
|
12
|
+
throw new RangeError(`${label} must not exceed ${maximum}.`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateWorkLimit(value, label) {
|
|
18
|
+
return validateGeneratedItemLimit(value, label, MAX_WORK_ITEMS);
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export function validateKeys(value, supported, label) {
|
|
4
22
|
for (const key of Object.keys(value)) {
|
|
5
23
|
if (!supported.includes(key)) {
|
package/src/extension.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export { action } from "./core/action.js";
|
|
2
|
-
|
|
2
|
+
import { ChartProgram } from "./ChartProgram.js";
|
|
3
|
+
import { registerProgramExtension } from "./core/extensionRegistry.js";
|
|
4
|
+
|
|
5
|
+
export { ChartProgram };
|
|
6
|
+
|
|
7
|
+
export function registerExtension(definition) {
|
|
8
|
+
registerProgramExtension(ChartProgram, definition);
|
|
9
|
+
}
|
package/src/grammar/aggregate.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
interpolateNumber,
|
|
4
|
+
numericExtent,
|
|
5
|
+
requireFiniteResult,
|
|
6
|
+
stableFiniteDeviation,
|
|
7
|
+
stableFiniteMean,
|
|
8
|
+
stableFiniteSum
|
|
9
|
+
} from "./numeric.js";
|
|
2
10
|
|
|
3
11
|
export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
|
|
4
12
|
"count", "sum", "mean", "median", "min", "max",
|
|
@@ -8,8 +16,8 @@ export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
|
|
|
8
16
|
]);
|
|
9
17
|
|
|
10
18
|
const SCALAR_OPERATIONS = new Set(SCALAR_AGGREGATE_OPERATIONS);
|
|
11
|
-
const NOMINAL_OPERATIONS =
|
|
12
|
-
const PARAMETERIZED_OPERATIONS =
|
|
19
|
+
const NOMINAL_OPERATIONS = ["count", "distinct", "valid", "missing"];
|
|
20
|
+
const PARAMETERIZED_OPERATIONS = ["quantile", "first", "last"];
|
|
13
21
|
|
|
14
22
|
function nonEmptyString(value, label) {
|
|
15
23
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -22,7 +30,7 @@ export function isScalarAggregate(value) {
|
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export function isParameterizedAggregate(value) {
|
|
25
|
-
return isPlainObject(value) && PARAMETERIZED_OPERATIONS.
|
|
33
|
+
return isPlainObject(value) && PARAMETERIZED_OPERATIONS.includes(value.op);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export function isAggregate(value) {
|
|
@@ -95,7 +103,7 @@ export function validateScalarAggregateFieldType(operation, fieldType) {
|
|
|
95
103
|
throw new Error("Scalar aggregate calculation requires a scalar operation.");
|
|
96
104
|
}
|
|
97
105
|
if (fieldType === "quantitative") return fieldType;
|
|
98
|
-
if (fieldType === "nominal" && NOMINAL_OPERATIONS.
|
|
106
|
+
if (fieldType === "nominal" && NOMINAL_OPERATIONS.includes(operation)) {
|
|
99
107
|
return fieldType;
|
|
100
108
|
}
|
|
101
109
|
throw new Error(
|
|
@@ -139,8 +147,11 @@ function quantile(values, probability) {
|
|
|
139
147
|
const position = (ordered.length - 1) * probability;
|
|
140
148
|
const lower = Math.floor(position);
|
|
141
149
|
const upper = Math.ceil(position);
|
|
142
|
-
return
|
|
143
|
-
|
|
150
|
+
return interpolateNumber(
|
|
151
|
+
ordered[lower],
|
|
152
|
+
ordered[upper],
|
|
153
|
+
position - lower
|
|
154
|
+
);
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
function comparableOrderKey(value) {
|
|
@@ -179,15 +190,6 @@ function aggregateOrderedRows(rows, field, aggregate) {
|
|
|
179
190
|
: candidates.at(-1).value;
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
function moments(values) {
|
|
183
|
-
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
184
|
-
const squared = values.reduce(
|
|
185
|
-
(sum, value) => sum + (value - mean) ** 2,
|
|
186
|
-
0
|
|
187
|
-
);
|
|
188
|
-
return { mean, squared };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
193
|
export function aggregateScalarValues(values, operation) {
|
|
192
194
|
if (!Array.isArray(values)) {
|
|
193
195
|
throw new TypeError("Aggregate values must be an array.");
|
|
@@ -206,27 +208,45 @@ export function aggregateScalarValues(values, operation) {
|
|
|
206
208
|
const finite = finiteValues(values);
|
|
207
209
|
if (finite.length === 0) return undefined;
|
|
208
210
|
if (operation === "sum") {
|
|
209
|
-
return finite
|
|
211
|
+
return stableFiniteSum(finite, "Aggregate sum");
|
|
210
212
|
}
|
|
211
|
-
if (operation === "min") return
|
|
212
|
-
if (operation === "max") return
|
|
213
|
+
if (operation === "min") return numericExtent(finite)[0];
|
|
214
|
+
if (operation === "max") return numericExtent(finite)[1];
|
|
213
215
|
if (operation === "median") return quantile(finite, 0.5);
|
|
214
216
|
if (operation === "q1") return quantile(finite, 0.25);
|
|
215
217
|
if (operation === "q3") return quantile(finite, 0.75);
|
|
216
218
|
|
|
217
|
-
const
|
|
219
|
+
const mean = stableFiniteMean(finite, "Aggregate mean");
|
|
218
220
|
if (operation === "mean") return mean;
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
+
if (["variance", "varianceP", "stdev", "stdevP"].includes(operation)) {
|
|
222
|
+
const population = operation.endsWith("P");
|
|
223
|
+
if (!population && finite.length < 2) return undefined;
|
|
224
|
+
const kind = population ? "population" : "sample";
|
|
225
|
+
const deviation = stableFiniteDeviation(finite, {
|
|
226
|
+
sample: !population,
|
|
227
|
+
label: `Aggregate ${kind} deviation`
|
|
228
|
+
});
|
|
229
|
+
if (operation.startsWith("stdev")) return deviation.deviation;
|
|
230
|
+
return deviation.squared === undefined
|
|
231
|
+
? requireFiniteResult(
|
|
232
|
+
deviation.deviation ** 2,
|
|
233
|
+
`Aggregate ${kind} variance`
|
|
234
|
+
)
|
|
235
|
+
: deviation.squared / (finite.length - (population ? 0 : 1));
|
|
236
|
+
}
|
|
221
237
|
if (finite.length < 2) return undefined;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
const stderr = stableFiniteDeviation(finite, {
|
|
239
|
+
sample: true,
|
|
240
|
+
divisor: Math.sqrt(finite.length),
|
|
241
|
+
label: "Aggregate standard error"
|
|
242
|
+
}).deviation;
|
|
227
243
|
if (operation === "stderr") return stderr;
|
|
228
|
-
if (operation === "ciLower")
|
|
229
|
-
|
|
244
|
+
if (operation === "ciLower") {
|
|
245
|
+
return requireFiniteResult(mean - 1.96 * stderr, "Aggregate lower CI");
|
|
246
|
+
}
|
|
247
|
+
if (operation === "ciUpper") {
|
|
248
|
+
return requireFiniteResult(mean + 1.96 * stderr, "Aggregate upper CI");
|
|
249
|
+
}
|
|
230
250
|
throw new Error(`Unsupported scalar aggregate "${operation}".`);
|
|
231
251
|
}
|
|
232
252
|
|
package/src/grammar/arcs.js
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
readNominalField,
|
|
6
6
|
readQuantitativeField
|
|
7
7
|
} from "./scales/index.js";
|
|
8
|
+
import {
|
|
9
|
+
interpolateNumber,
|
|
10
|
+
normalizedFiniteSum,
|
|
11
|
+
requireFiniteResult,
|
|
12
|
+
stableFiniteSum
|
|
13
|
+
} from "./numeric.js";
|
|
8
14
|
|
|
9
15
|
function requireArcLayer(layer) {
|
|
10
16
|
if (layer?.mark?.type !== "arc") {
|
|
@@ -22,6 +28,9 @@ function requireBandScale(scale, label) {
|
|
|
22
28
|
scale?.type !== "band" ||
|
|
23
29
|
!Array.isArray(scale.domain) ||
|
|
24
30
|
!Array.isArray(scale.range) ||
|
|
31
|
+
scale.range.length !== 2 ||
|
|
32
|
+
!scale.range.every(Number.isFinite) ||
|
|
33
|
+
scale.range[0] === scale.range[1] ||
|
|
25
34
|
!Number.isFinite(scale.bandwidth) ||
|
|
26
35
|
scale.bandwidth <= 0
|
|
27
36
|
) {
|
|
@@ -73,31 +82,69 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
73
82
|
throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
|
|
74
83
|
}
|
|
75
84
|
group.push({
|
|
76
|
-
row: rows[index],
|
|
77
85
|
color: colors[index],
|
|
78
86
|
sourceIndex: index,
|
|
79
87
|
weight: weights[index]
|
|
80
88
|
});
|
|
81
89
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
|
|
91
|
+
throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
|
|
92
|
+
}
|
|
93
|
+
const innerRadius = requireFiniteResult(
|
|
94
|
+
frame.availableRadius * innerRadiusRatio,
|
|
95
|
+
`Arc mark "${layer.id}" inner radius`
|
|
85
96
|
);
|
|
86
|
-
|
|
87
|
-
const start = thetaScale.range[0];
|
|
88
|
-
const span = thetaScale.range[1] - thetaScale.range[0];
|
|
89
|
-
const innerRadius = frame.availableRadius * innerRadiusRatio;
|
|
90
|
-
let cursor = start;
|
|
97
|
+
let cursor = thetaScale.range[0];
|
|
91
98
|
const sectors = [];
|
|
92
99
|
const positiveValues = thetaScale.domain.filter(value =>
|
|
93
100
|
groups.get(value).some(item => item.weight > 0)
|
|
94
101
|
);
|
|
102
|
+
const aggregateValues = positiveValues.map(value => stableFiniteSum(
|
|
103
|
+
groups.get(value).map(item => item.weight),
|
|
104
|
+
`Arc theta group "${String(value)}" aggregate`
|
|
105
|
+
));
|
|
106
|
+
const total = aggregateValues.reduce((sum, value) => sum + value, 0);
|
|
107
|
+
const normalizedTotal = Number.isFinite(total)
|
|
108
|
+
? undefined
|
|
109
|
+
: normalizedFiniteSum(
|
|
110
|
+
aggregateValues,
|
|
111
|
+
`Arc mark "${layer.id}" theta aggregates`
|
|
112
|
+
);
|
|
113
|
+
if (total === 0 || normalizedTotal?.total <= 0) {
|
|
114
|
+
throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
|
|
115
|
+
}
|
|
116
|
+
const span = thetaScale.range[1] - thetaScale.range[0];
|
|
117
|
+
let cumulative = 0;
|
|
118
|
+
let cumulativeCorrection = 0;
|
|
95
119
|
for (const [positiveIndex, value] of positiveValues.entries()) {
|
|
96
120
|
const group = groups.get(value);
|
|
97
|
-
const aggregateValue =
|
|
121
|
+
const aggregateValue = aggregateValues[positiveIndex];
|
|
122
|
+
const normalizedValue = normalizedTotal === undefined
|
|
123
|
+
? aggregateValue
|
|
124
|
+
: aggregateValue / normalizedTotal.scale;
|
|
125
|
+
const next = cumulative + normalizedValue;
|
|
126
|
+
if (normalizedTotal !== undefined) {
|
|
127
|
+
cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
|
|
128
|
+
? cumulative - next + normalizedValue
|
|
129
|
+
: normalizedValue - next + cumulative;
|
|
130
|
+
}
|
|
131
|
+
cumulative = next;
|
|
132
|
+
const directEnd = cursor + aggregateValue / total * span;
|
|
98
133
|
const endTheta = positiveIndex === positiveValues.length - 1
|
|
99
134
|
? thetaScale.range[1]
|
|
100
|
-
:
|
|
135
|
+
: normalizedTotal === undefined && Number.isFinite(directEnd)
|
|
136
|
+
? directEnd
|
|
137
|
+
: interpolateNumber(
|
|
138
|
+
thetaScale.range[0],
|
|
139
|
+
thetaScale.range[1],
|
|
140
|
+
(cumulative + cumulativeCorrection) /
|
|
141
|
+
(normalizedTotal?.total ?? total)
|
|
142
|
+
);
|
|
143
|
+
if (!Number.isFinite(endTheta) || endTheta === cursor) {
|
|
144
|
+
throw new RangeError(
|
|
145
|
+
`Arc theta group "${String(value)}" cannot resolve a distinct finite sector.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
101
148
|
const distinctColors = [...new Set(group.map(item => item.color))];
|
|
102
149
|
if (distinctColors.length > 1) {
|
|
103
150
|
throw new Error(
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
import { stableOrderPathValues } from "./pathOrder.js";
|
|
12
12
|
import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
|
|
13
13
|
|
|
14
|
+
function indexSegments(segments) {
|
|
15
|
+
const indexed = [];
|
|
16
|
+
for (const segment of segments) indexed[segment.index] = segment;
|
|
17
|
+
return indexed;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export function deriveAreaSeries(rows, layer) {
|
|
15
21
|
if (layer?.mark?.type !== "area") {
|
|
16
22
|
throw new Error("Area series derivation requires a semantic area mark.");
|
|
@@ -187,15 +193,11 @@ export function deriveCenteredAreaSeries(rows, layer) {
|
|
|
187
193
|
const valuesBySeries = groupOrder.map(() => []);
|
|
188
194
|
for (const position of orderedPositions) {
|
|
189
195
|
const partition = groupOrder.map(key => byGroup.get(key).get(position));
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
segment
|
|
194
|
-
])
|
|
195
|
-
);
|
|
196
|
-
let endpoint = -partition.reduce((sum, value) => sum + value, 0) / 2;
|
|
196
|
+
const resolvedSegments = layoutSeriesPartition(partition, "center");
|
|
197
|
+
const segments = indexSegments(resolvedSegments);
|
|
198
|
+
let endpoint = resolvedSegments[0]?.start ?? 0;
|
|
197
199
|
for (let index = 0; index < groupOrder.length; index += 1) {
|
|
198
|
-
const segment = segments
|
|
200
|
+
const segment = segments[index];
|
|
199
201
|
valuesBySeries[index].push({
|
|
200
202
|
x: position,
|
|
201
203
|
y: partition[index],
|
|
@@ -331,17 +333,13 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
|
|
|
331
333
|
}
|
|
332
334
|
return series.values[sample].y;
|
|
333
335
|
});
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
segment.index,
|
|
337
|
-
segment
|
|
338
|
-
])
|
|
339
|
-
);
|
|
336
|
+
const resolvedSegments = layoutSeriesPartition(densities, layout);
|
|
337
|
+
const segments = indexSegments(resolvedSegments);
|
|
340
338
|
let zeroThicknessEndpoint = layout === "center"
|
|
341
|
-
?
|
|
339
|
+
? resolvedSegments[0]?.start ?? 0
|
|
342
340
|
: 0;
|
|
343
341
|
for (let index = 0; index < derived.series.length; index += 1) {
|
|
344
|
-
const segment = segments
|
|
342
|
+
const segment = segments[index];
|
|
345
343
|
valuesBySeries[index].push({
|
|
346
344
|
x,
|
|
347
345
|
lower: segment?.start ?? zeroThicknessEndpoint,
|
package/src/grammar/bin2d.js
CHANGED
|
@@ -2,25 +2,22 @@ import {
|
|
|
2
2
|
cloneAndFreeze,
|
|
3
3
|
isPlainObject
|
|
4
4
|
} from "../core/immutable.js";
|
|
5
|
+
import { validateNonEmptyString as requireField } from "../core/validation.js";
|
|
6
|
+
import { interpolateNumber, numericExtent } from "./numeric.js";
|
|
7
|
+
|
|
8
|
+
const MAX_AXIS_BINS = 10_000;
|
|
9
|
+
const MAX_CELLS = 1_000_000;
|
|
5
10
|
|
|
6
11
|
const TRANSFORM_KEYS = Object.freeze([
|
|
7
12
|
"type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
|
|
8
13
|
"resolved"
|
|
9
14
|
]);
|
|
10
|
-
const
|
|
11
|
-
const EXTENT_KEYS = Object.freeze(["x", "y"]);
|
|
15
|
+
const AXIS_KEYS = Object.freeze(["x", "y"]);
|
|
12
16
|
const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
|
|
13
17
|
const RESOLVED_KEYS = Object.freeze([
|
|
14
18
|
"extent", "edges", "eligibleCount", "occupiedCount"
|
|
15
19
|
]);
|
|
16
20
|
|
|
17
|
-
function requireField(value, label) {
|
|
18
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
function rejectUnknownKeys(value, supported, label) {
|
|
25
22
|
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
26
23
|
if (unknown !== undefined) {
|
|
@@ -43,7 +40,7 @@ function normalizeBins(value) {
|
|
|
43
40
|
"2D bin bins must be a positive integer or an object with x and y."
|
|
44
41
|
);
|
|
45
42
|
}
|
|
46
|
-
rejectUnknownKeys(value,
|
|
43
|
+
rejectUnknownKeys(value, AXIS_KEYS, "2D bin bins");
|
|
47
44
|
return { x: value.x, y: value.y };
|
|
48
45
|
}
|
|
49
46
|
|
|
@@ -51,9 +48,15 @@ function validateBins(value) {
|
|
|
51
48
|
if (!isPlainObject(value)) {
|
|
52
49
|
throw new TypeError("2D bin bins must be an object.");
|
|
53
50
|
}
|
|
54
|
-
rejectUnknownKeys(value,
|
|
51
|
+
rejectUnknownKeys(value, AXIS_KEYS, "2D bin bins");
|
|
55
52
|
requirePositiveInteger(value.x, "2D bin x bins");
|
|
56
53
|
requirePositiveInteger(value.y, "2D bin y bins");
|
|
54
|
+
if (value.x > MAX_AXIS_BINS || value.y > MAX_AXIS_BINS) {
|
|
55
|
+
throw new RangeError(`2D bin axes support at most ${MAX_AXIS_BINS} bins.`);
|
|
56
|
+
}
|
|
57
|
+
if (value.x * value.y > MAX_CELLS) {
|
|
58
|
+
throw new RangeError(`2D bin grids support at most ${MAX_CELLS} cells.`);
|
|
59
|
+
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function normalizeRequestedExtent(value) {
|
|
@@ -61,7 +64,7 @@ function normalizeRequestedExtent(value) {
|
|
|
61
64
|
if (!isPlainObject(value)) {
|
|
62
65
|
throw new TypeError("2D bin extent must be an object.");
|
|
63
66
|
}
|
|
64
|
-
rejectUnknownKeys(value,
|
|
67
|
+
rejectUnknownKeys(value, AXIS_KEYS, "2D bin extent");
|
|
65
68
|
return {
|
|
66
69
|
x: value.x ?? "auto",
|
|
67
70
|
y: value.y ?? "auto"
|
|
@@ -84,7 +87,7 @@ function validateRequestedExtent(value) {
|
|
|
84
87
|
if (!isPlainObject(value)) {
|
|
85
88
|
throw new TypeError("2D bin extent must be an object.");
|
|
86
89
|
}
|
|
87
|
-
rejectUnknownKeys(value,
|
|
90
|
+
rejectUnknownKeys(value, AXIS_KEYS, "2D bin extent");
|
|
88
91
|
validateExtentValue(value.x, "2D bin x extent");
|
|
89
92
|
validateExtentValue(value.y, "2D bin y extent");
|
|
90
93
|
}
|
|
@@ -152,9 +155,9 @@ function validateResolved(value, bins) {
|
|
|
152
155
|
if (!isPlainObject(value.extent) || !isPlainObject(value.edges)) {
|
|
153
156
|
throw new TypeError("Resolved 2D bin extent and edges must be objects.");
|
|
154
157
|
}
|
|
155
|
-
rejectUnknownKeys(value.extent,
|
|
156
|
-
rejectUnknownKeys(value.edges,
|
|
157
|
-
for (const axis of
|
|
158
|
+
rejectUnknownKeys(value.extent, AXIS_KEYS, "resolved 2D bin extent");
|
|
159
|
+
rejectUnknownKeys(value.edges, AXIS_KEYS, "resolved 2D bin edges");
|
|
160
|
+
for (const axis of AXIS_KEYS) {
|
|
158
161
|
validateExtentValue(
|
|
159
162
|
value.extent[axis],
|
|
160
163
|
`Resolved 2D bin ${axis} extent`,
|
|
@@ -223,8 +226,7 @@ export function normalizeBin2DTransform({
|
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
function resolveExtent(values, requested, axis) {
|
|
226
|
-
const minimum =
|
|
227
|
-
const maximum = Math.max(...values);
|
|
229
|
+
const [minimum, maximum] = numericExtent(values);
|
|
228
230
|
const extent = requested === "auto" ? [minimum, maximum] : requested;
|
|
229
231
|
if (extent[0] > minimum || extent[1] < maximum) {
|
|
230
232
|
throw new RangeError(
|
|
@@ -237,12 +239,24 @@ function resolveExtent(values, requested, axis) {
|
|
|
237
239
|
return [...extent];
|
|
238
240
|
}
|
|
239
241
|
|
|
240
|
-
function createEdges(extent, count) {
|
|
242
|
+
function createEdges(extent, count, axis) {
|
|
241
243
|
const [minimum, maximum] = extent;
|
|
242
244
|
const step = (maximum - minimum) / count;
|
|
243
|
-
|
|
244
|
-
index === count
|
|
245
|
+
const edges = Array.from({ length: count + 1 }, (_, index) =>
|
|
246
|
+
index === count
|
|
247
|
+
? maximum
|
|
248
|
+
: Number.isFinite(step)
|
|
249
|
+
? minimum + step * index
|
|
250
|
+
: interpolateNumber(minimum, maximum, index / count)
|
|
245
251
|
);
|
|
252
|
+
if (edges.some((edge, index) =>
|
|
253
|
+
!Number.isFinite(edge) || index > 0 && edge <= edges[index - 1]
|
|
254
|
+
)) {
|
|
255
|
+
throw new RangeError(
|
|
256
|
+
`2D bin ${axis} extent cannot represent ${count} distinct bins.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return edges;
|
|
246
260
|
}
|
|
247
261
|
|
|
248
262
|
function findBinIndex(value, edges) {
|
|
@@ -287,8 +301,8 @@ export function deriveBin2DRows(rows, transform) {
|
|
|
287
301
|
)
|
|
288
302
|
};
|
|
289
303
|
const edges = {
|
|
290
|
-
x: createEdges(extent.x, transform.bins.x),
|
|
291
|
-
y: createEdges(extent.y, transform.bins.y)
|
|
304
|
+
x: createEdges(extent.x, transform.bins.x, "x"),
|
|
305
|
+
y: createEdges(extent.y, transform.bins.y, "y")
|
|
292
306
|
};
|
|
293
307
|
const buckets = Array.from(
|
|
294
308
|
{ length: transform.bins.x * transform.bins.y },
|
package/src/grammar/boxPlot.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { interpolateNumber, stableDecimal } from "./numeric.js";
|
|
2
3
|
|
|
3
4
|
export const BOX_FIELDS = Object.freeze({
|
|
4
5
|
q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
|
|
@@ -7,10 +8,10 @@ export const BOX_FIELDS = Object.freeze({
|
|
|
7
8
|
count: "__boxPlot_count"
|
|
8
9
|
});
|
|
9
10
|
|
|
10
|
-
const TRANSFORM_KEYS =
|
|
11
|
+
const TRANSFORM_KEYS = [
|
|
11
12
|
"type", "category", "field", "method", "whisker", "factor", "as"
|
|
12
|
-
]
|
|
13
|
-
const OUTPUT_KEYS = Object.
|
|
13
|
+
];
|
|
14
|
+
const OUTPUT_KEYS = Object.keys(BOX_FIELDS);
|
|
14
15
|
|
|
15
16
|
function field(value, label) {
|
|
16
17
|
if (typeof value !== "string" || value.length === 0) throw new TypeError(`${label} must be a non-empty string.`);
|
|
@@ -82,11 +83,7 @@ function quantile(sorted, p) {
|
|
|
82
83
|
const position = (sorted.length - 1) * p;
|
|
83
84
|
const lower = Math.floor(position);
|
|
84
85
|
const upper = Math.ceil(position);
|
|
85
|
-
return sorted[lower]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function stableNumber(value) {
|
|
89
|
-
return Number(value.toFixed(12));
|
|
86
|
+
return interpolateNumber(sorted[lower], sorted[upper], position - lower);
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
export function deriveBoxData(rows, transform) {
|
|
@@ -113,15 +110,19 @@ export function deriveBoxData(rows, transform) {
|
|
|
113
110
|
const outlierSet = new Set();
|
|
114
111
|
for (const group of groups) {
|
|
115
112
|
const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
|
|
116
|
-
const q1 =
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
const [q1, median, q3] = [0.25, 0.5, 0.75].map(probability =>
|
|
114
|
+
stableDecimal(quantile(sorted, probability))
|
|
115
|
+
);
|
|
116
|
+
const spread = q3 - q1;
|
|
119
117
|
const lowerFence = whisker === "minmax"
|
|
120
118
|
? sorted[0]
|
|
121
|
-
:
|
|
119
|
+
: stableDecimal(q1 - factor * spread);
|
|
122
120
|
const upperFence = whisker === "minmax"
|
|
123
121
|
? sorted.at(-1)
|
|
124
|
-
:
|
|
122
|
+
: stableDecimal(q3 + factor * spread);
|
|
123
|
+
if (![lowerFence, upperFence].every(Number.isFinite)) {
|
|
124
|
+
throw new RangeError("Box Tukey fences are outside the finite numeric range.");
|
|
125
|
+
}
|
|
125
126
|
const inliers = sorted.filter(value => value >= lowerFence && value <= upperFence);
|
|
126
127
|
const lowerWhisker = inliers[0];
|
|
127
128
|
const upperWhisker = inliers.at(-1);
|