ggaction 0.0.10 → 0.0.12
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 +97 -0
- package/README.md +5 -3
- 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/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 +18 -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 +41 -45
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/apply.js +30 -0
- package/src/actions/encodings/position/policies/arc.js +23 -2
- 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 +36 -7
- 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 +212 -30
- package/src/actions/errorBars/edit.js +91 -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 +38 -16
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +79 -54
- 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/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +170 -32
- 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/positionCompatibility.js +1 -1
- 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/rules.js +2 -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 +139 -60
- 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 +55 -30
- 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 +9 -2
- package/src/materialization/facetGuides/placement.js +49 -19
- package/src/materialization/facetGuides/preparation.js +35 -6
- package/src/materialization/facets.js +42 -28
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +15 -4
- package/src/materialization/marks/index.js +29 -6
- package/src/materialization/rowEncoding.js +57 -1
- package/src/materialization/scales/policies/bar.js +61 -102
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +6 -9
- 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/index.d.ts +3 -0
- package/types/program.d.ts +523 -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/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,23 +5,50 @@ 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") {
|
|
11
17
|
throw new Error("Arc derivation requires a semantic arc mark.");
|
|
12
18
|
}
|
|
13
19
|
const theta = layer.encoding?.theta;
|
|
14
|
-
if (
|
|
15
|
-
|
|
20
|
+
if (
|
|
21
|
+
theta === undefined ||
|
|
22
|
+
!["quantitative", "nominal", "ordinal"].includes(theta.fieldType)
|
|
23
|
+
) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Arc mark "${layer.id}" requires quantitative or categorical theta.`
|
|
26
|
+
);
|
|
16
27
|
}
|
|
17
28
|
return theta;
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
function requireContinuousThetaScale(scale, label) {
|
|
32
|
+
if (
|
|
33
|
+
scale?.type !== "linear" ||
|
|
34
|
+
!Array.isArray(scale.range) ||
|
|
35
|
+
scale.range.length !== 2 ||
|
|
36
|
+
!scale.range.every(Number.isFinite) ||
|
|
37
|
+
scale.range[0] === scale.range[1]
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`${label} requires a resolved linear scale.`);
|
|
40
|
+
}
|
|
41
|
+
return scale;
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
function requireBandScale(scale, label) {
|
|
21
45
|
if (
|
|
22
46
|
scale?.type !== "band" ||
|
|
23
47
|
!Array.isArray(scale.domain) ||
|
|
24
48
|
!Array.isArray(scale.range) ||
|
|
49
|
+
scale.range.length !== 2 ||
|
|
50
|
+
!scale.range.every(Number.isFinite) ||
|
|
51
|
+
scale.range[0] === scale.range[1] ||
|
|
25
52
|
!Number.isFinite(scale.bandwidth) ||
|
|
26
53
|
scale.bandwidth <= 0
|
|
27
54
|
) {
|
|
@@ -36,24 +63,90 @@ function colorValues(rows, encoding) {
|
|
|
36
63
|
: readNominalField(rows, encoding.field);
|
|
37
64
|
}
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
if (!Array.isArray(rows)) throw new TypeError(
|
|
66
|
+
function readNonNegativeArcThetaValues(rows, field, layerId, role) {
|
|
67
|
+
if (!Array.isArray(rows)) throw new TypeError(`Arc theta ${role}s require rows.`);
|
|
41
68
|
if (typeof field !== "string" || field.length === 0) {
|
|
42
|
-
throw new TypeError(
|
|
69
|
+
throw new TypeError(`Arc theta ${role} must be a non-empty string.`);
|
|
43
70
|
}
|
|
44
|
-
const
|
|
71
|
+
const values = rows.map((row, index) => {
|
|
45
72
|
const value = row?.[field];
|
|
46
73
|
if (!Number.isFinite(value) || value < 0) {
|
|
47
74
|
throw new TypeError(
|
|
48
|
-
`Arc theta
|
|
75
|
+
`Arc theta ${role} field "${field}" must contain non-negative finite numbers at row ${index}.`
|
|
49
76
|
);
|
|
50
77
|
}
|
|
51
78
|
return value;
|
|
52
79
|
});
|
|
53
|
-
if (
|
|
54
|
-
throw new Error(`Arc mark "${layerId}" theta
|
|
80
|
+
if (values.every(value => value === 0)) {
|
|
81
|
+
throw new Error(`Arc mark "${layerId}" theta ${role}s must have a positive total.`);
|
|
55
82
|
}
|
|
56
|
-
return cloneAndFreeze(
|
|
83
|
+
return cloneAndFreeze(values);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readArcThetaWeights(rows, field, layerId = "arc") {
|
|
87
|
+
return readNonNegativeArcThetaValues(rows, field, layerId, "weight");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readArcThetaValues(rows, field, layerId = "arc") {
|
|
91
|
+
return readNonNegativeArcThetaValues(rows, field, layerId, "value");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveProportionalRanges(values, range, labelAt) {
|
|
95
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
96
|
+
const normalizedTotal = Number.isFinite(total)
|
|
97
|
+
? undefined
|
|
98
|
+
: normalizedFiniteSum(values, "Arc theta values");
|
|
99
|
+
if (total === 0 || normalizedTotal?.total <= 0) {
|
|
100
|
+
throw new Error("Arc theta values must have a positive total.");
|
|
101
|
+
}
|
|
102
|
+
const span = range[1] - range[0];
|
|
103
|
+
let cursor = range[0];
|
|
104
|
+
let cumulative = 0;
|
|
105
|
+
let cumulativeCorrection = 0;
|
|
106
|
+
return values.map((value, index) => {
|
|
107
|
+
const normalizedValue = normalizedTotal === undefined
|
|
108
|
+
? value
|
|
109
|
+
: value / normalizedTotal.scale;
|
|
110
|
+
const next = cumulative + normalizedValue;
|
|
111
|
+
if (normalizedTotal !== undefined) {
|
|
112
|
+
cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
|
|
113
|
+
? cumulative - next + normalizedValue
|
|
114
|
+
: normalizedValue - next + cumulative;
|
|
115
|
+
}
|
|
116
|
+
cumulative = next;
|
|
117
|
+
const directEnd = cursor + value / total * span;
|
|
118
|
+
const endTheta = index === values.length - 1
|
|
119
|
+
? range[1]
|
|
120
|
+
: normalizedTotal === undefined && Number.isFinite(directEnd)
|
|
121
|
+
? directEnd
|
|
122
|
+
: interpolateNumber(
|
|
123
|
+
range[0],
|
|
124
|
+
range[1],
|
|
125
|
+
(cumulative + cumulativeCorrection) /
|
|
126
|
+
(normalizedTotal?.total ?? total)
|
|
127
|
+
);
|
|
128
|
+
if (!Number.isFinite(endTheta) || endTheta === cursor) {
|
|
129
|
+
throw new RangeError(
|
|
130
|
+
`Arc theta ${labelAt(index)} cannot resolve a distinct finite sector.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const result = { startTheta: cursor, endTheta };
|
|
134
|
+
cursor = endTheta;
|
|
135
|
+
return result;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveProportionalRadii(layer, frame, innerRadiusRatio) {
|
|
140
|
+
if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
|
|
141
|
+
throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
innerRadius: requireFiniteResult(
|
|
145
|
+
frame.availableRadius * innerRadiusRatio,
|
|
146
|
+
`Arc mark "${layer.id}" inner radius`
|
|
147
|
+
),
|
|
148
|
+
outerRadius: frame.availableRadius
|
|
149
|
+
};
|
|
57
150
|
}
|
|
58
151
|
|
|
59
152
|
function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
@@ -73,31 +166,29 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
73
166
|
throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
|
|
74
167
|
}
|
|
75
168
|
group.push({
|
|
76
|
-
row: rows[index],
|
|
77
169
|
color: colors[index],
|
|
78
170
|
sourceIndex: index,
|
|
79
171
|
weight: weights[index]
|
|
80
172
|
});
|
|
81
173
|
}
|
|
82
|
-
const
|
|
83
|
-
(sum, group) => sum + group.reduce((groupSum, item) => groupSum + item.weight, 0),
|
|
84
|
-
0
|
|
85
|
-
);
|
|
86
|
-
if (total === 0) throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
|
|
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;
|
|
174
|
+
const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
|
|
91
175
|
const sectors = [];
|
|
92
176
|
const positiveValues = thetaScale.domain.filter(value =>
|
|
93
177
|
groups.get(value).some(item => item.weight > 0)
|
|
94
178
|
);
|
|
179
|
+
const aggregateValues = positiveValues.map(value => stableFiniteSum(
|
|
180
|
+
groups.get(value).map(item => item.weight),
|
|
181
|
+
`Arc theta group "${String(value)}" aggregate`
|
|
182
|
+
));
|
|
183
|
+
const ranges = resolveProportionalRanges(
|
|
184
|
+
aggregateValues,
|
|
185
|
+
thetaScale.range,
|
|
186
|
+
index => `group "${String(positiveValues[index])}"`
|
|
187
|
+
);
|
|
95
188
|
for (const [positiveIndex, value] of positiveValues.entries()) {
|
|
96
189
|
const group = groups.get(value);
|
|
97
|
-
const aggregateValue =
|
|
98
|
-
const endTheta = positiveIndex
|
|
99
|
-
? thetaScale.range[1]
|
|
100
|
-
: cursor + aggregateValue / total * span;
|
|
190
|
+
const aggregateValue = aggregateValues[positiveIndex];
|
|
191
|
+
const { startTheta, endTheta } = ranges[positiveIndex];
|
|
101
192
|
const distinctColors = [...new Set(group.map(item => item.color))];
|
|
102
193
|
if (distinctColors.length > 1) {
|
|
103
194
|
throw new Error(
|
|
@@ -110,17 +201,46 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
110
201
|
count: group.length,
|
|
111
202
|
aggregateValue,
|
|
112
203
|
color: distinctColors[0],
|
|
113
|
-
startTheta
|
|
204
|
+
startTheta,
|
|
114
205
|
endTheta,
|
|
115
|
-
|
|
116
|
-
outerRadius: frame.availableRadius,
|
|
206
|
+
...radii,
|
|
117
207
|
sourceIndices: group.map(item => item.sourceIndex)
|
|
118
208
|
});
|
|
119
|
-
cursor = endTheta;
|
|
120
209
|
}
|
|
121
210
|
return sectors;
|
|
122
211
|
}
|
|
123
212
|
|
|
213
|
+
function quantitativeSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
214
|
+
const theta = layer.encoding.theta;
|
|
215
|
+
if (layer.encoding?.radius !== undefined) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Arc mark "${layer.id}" cannot combine quantitative theta with radius.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const values = readArcThetaValues(rows, theta.field, layer.id);
|
|
221
|
+
const colors = colorValues(rows, layer.encoding?.color);
|
|
222
|
+
const positiveIndices = values
|
|
223
|
+
.map((value, index) => value > 0 ? index : undefined)
|
|
224
|
+
.filter(index => index !== undefined);
|
|
225
|
+
const positiveValues = positiveIndices.map(index => values[index]);
|
|
226
|
+
const ranges = resolveProportionalRanges(
|
|
227
|
+
positiveValues,
|
|
228
|
+
thetaScale.range,
|
|
229
|
+
index => `row ${positiveIndices[index]}`
|
|
230
|
+
);
|
|
231
|
+
const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
|
|
232
|
+
return positiveIndices.map((sourceIndex, index) => ({
|
|
233
|
+
key: `${String(values[sourceIndex])}:${sourceIndex}`,
|
|
234
|
+
theta: values[sourceIndex],
|
|
235
|
+
count: 1,
|
|
236
|
+
aggregateValue: values[sourceIndex],
|
|
237
|
+
color: colors[sourceIndex],
|
|
238
|
+
...ranges[index],
|
|
239
|
+
...radii,
|
|
240
|
+
sourceIndices: [sourceIndex]
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
124
244
|
function radialSectors(rows, layer, thetaScale, radiusScale) {
|
|
125
245
|
const theta = layer.encoding.theta;
|
|
126
246
|
const radius = layer.encoding?.radius;
|
|
@@ -176,7 +296,6 @@ export function deriveArcSectors(rows, layer, {
|
|
|
176
296
|
} = {}) {
|
|
177
297
|
if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
|
|
178
298
|
const theta = requireArcLayer(layer);
|
|
179
|
-
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`);
|
|
180
299
|
if (
|
|
181
300
|
!Number.isFinite(innerRadiusRatio) ||
|
|
182
301
|
innerRadiusRatio < 0 ||
|
|
@@ -184,8 +303,27 @@ export function deriveArcSectors(rows, layer, {
|
|
|
184
303
|
) {
|
|
185
304
|
throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
|
|
186
305
|
}
|
|
187
|
-
const sectors =
|
|
188
|
-
?
|
|
189
|
-
|
|
306
|
+
const sectors = theta.fieldType === "quantitative"
|
|
307
|
+
? quantitativeSectors(
|
|
308
|
+
rows,
|
|
309
|
+
layer,
|
|
310
|
+
requireContinuousThetaScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
311
|
+
frame,
|
|
312
|
+
innerRadiusRatio
|
|
313
|
+
)
|
|
314
|
+
: ["count", "sum"].includes(theta.aggregate)
|
|
315
|
+
? proportionalSectors(
|
|
316
|
+
rows,
|
|
317
|
+
layer,
|
|
318
|
+
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
319
|
+
frame,
|
|
320
|
+
innerRadiusRatio
|
|
321
|
+
)
|
|
322
|
+
: radialSectors(
|
|
323
|
+
rows,
|
|
324
|
+
layer,
|
|
325
|
+
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
326
|
+
radiusScale
|
|
327
|
+
);
|
|
190
328
|
return cloneAndFreeze({ sectors });
|
|
191
329
|
}
|
|
@@ -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);
|