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
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateGeneratedItemLimit,
|
|
5
|
+
validateWorkLimit
|
|
6
|
+
} from "../../core/validation.js";
|
|
3
7
|
import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
|
|
4
8
|
import { planFacetDependencies } from "./dependencies.js";
|
|
5
9
|
import { readNominalField } from "../scales/index.js";
|
|
@@ -12,6 +16,7 @@ const SUPPORTED_BAR_GRAINS = new Set([
|
|
|
12
16
|
BAR_GRAINS.aggregate,
|
|
13
17
|
BAR_GRAINS.ranged
|
|
14
18
|
]);
|
|
19
|
+
const MAX_FACET_CHILDREN = 100;
|
|
15
20
|
|
|
16
21
|
function requireFacetField(field) {
|
|
17
22
|
if (typeof field !== "string" || field.length === 0) {
|
|
@@ -109,6 +114,15 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
109
114
|
throw new Error(`facet field "${field}" has no values.`);
|
|
110
115
|
}
|
|
111
116
|
const values = resolveValues(observed, options.values);
|
|
117
|
+
validateGeneratedItemLimit(
|
|
118
|
+
values.length,
|
|
119
|
+
"Facet child count",
|
|
120
|
+
MAX_FACET_CHILDREN
|
|
121
|
+
);
|
|
122
|
+
validateWorkLimit(
|
|
123
|
+
dataset.values.length * values.length,
|
|
124
|
+
"Facet partition work"
|
|
125
|
+
);
|
|
112
126
|
return cloneAndFreeze({
|
|
113
127
|
id,
|
|
114
128
|
data,
|
|
@@ -131,10 +131,13 @@ function continuousUnion(domains, id) {
|
|
|
131
131
|
)) {
|
|
132
132
|
throw new TypeError(`Facet shared scale "${id}" requires finite pair domains.`);
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
let minimum = Infinity;
|
|
135
|
+
let maximum = -Infinity;
|
|
136
|
+
for (const domain of domains) {
|
|
137
|
+
minimum = Math.min(minimum, domain[0], domain[1]);
|
|
138
|
+
maximum = Math.max(maximum, domain[0], domain[1]);
|
|
139
|
+
}
|
|
140
|
+
return [minimum, maximum];
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
function stableUnion(domains) {
|
|
@@ -2,8 +2,10 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
|
2
2
|
import {
|
|
3
3
|
deriveKernelDensity,
|
|
4
4
|
validateDensityKernel,
|
|
5
|
-
validateDensityNormalization
|
|
5
|
+
validateDensityNormalization,
|
|
6
|
+
validateDensitySteps
|
|
6
7
|
} from "./density.js";
|
|
8
|
+
import { finiteMidpoint, stableFiniteMean } from "./numeric.js";
|
|
7
9
|
|
|
8
10
|
export const GRADIENT_PROFILE_FIELDS = Object.freeze({
|
|
9
11
|
category: "__gradientPlot_category",
|
|
@@ -15,11 +17,11 @@ export const GRADIENT_PROFILE_FIELDS = Object.freeze({
|
|
|
15
17
|
count: "__gradientPlot_count"
|
|
16
18
|
});
|
|
17
19
|
|
|
18
|
-
const TRANSFORM_KEYS =
|
|
20
|
+
const TRANSFORM_KEYS = [
|
|
19
21
|
"type", "category", "field", "bandwidth", "extent", "steps", "kernel",
|
|
20
22
|
"normalization", "center", "as", "resolve", "resolved"
|
|
21
|
-
]
|
|
22
|
-
const OUTPUT_KEYS = Object.
|
|
23
|
+
];
|
|
24
|
+
const OUTPUT_KEYS = Object.keys(GRADIENT_PROFILE_FIELDS);
|
|
23
25
|
|
|
24
26
|
function requireField(value, label) {
|
|
25
27
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -92,9 +94,7 @@ export function validateGradientProfileTransform(value) {
|
|
|
92
94
|
if (value.extent !== "auto") {
|
|
93
95
|
validatePair(value.extent, "Gradient profile extent");
|
|
94
96
|
}
|
|
95
|
-
|
|
96
|
-
throw new RangeError("Gradient profile steps must be an integer of at least 2.");
|
|
97
|
-
}
|
|
97
|
+
validateDensitySteps(value.steps, "Gradient profile steps");
|
|
98
98
|
validateDensityKernel(value.kernel);
|
|
99
99
|
validateDensityNormalization(value.normalization);
|
|
100
100
|
if (!["mean", "median"].includes(value.center)) {
|
|
@@ -148,14 +148,10 @@ export function requestedGradientProfileTransform(transform) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function centerOf(values, type) {
|
|
151
|
-
if (type === "mean")
|
|
152
|
-
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
153
|
-
}
|
|
151
|
+
if (type === "mean") return stableFiniteMean(values);
|
|
154
152
|
const sorted = [...values].sort((left, right) => left - right);
|
|
155
153
|
const middle = (sorted.length - 1) / 2;
|
|
156
|
-
|
|
157
|
-
const upper = Math.ceil(middle);
|
|
158
|
-
return (sorted[lower] + sorted[upper]) / 2;
|
|
154
|
+
return finiteMidpoint(sorted[Math.floor(middle)], sorted[Math.ceil(middle)]);
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
export function deriveGradientProfiles(rows, transform) {
|
|
@@ -178,8 +174,9 @@ export function deriveGradientProfiles(rows, transform) {
|
|
|
178
174
|
row[transform.category] !== undefined && row[transform.category] !== null &&
|
|
179
175
|
row[transform.category] !== "" && Number.isFinite(row[transform.field])
|
|
180
176
|
);
|
|
181
|
-
const maximumIntensity =
|
|
182
|
-
|
|
177
|
+
const maximumIntensity = density.values.reduce(
|
|
178
|
+
(maximum, row) => Math.max(maximum, row.__gradientProfile_intensity),
|
|
179
|
+
-Infinity
|
|
183
180
|
);
|
|
184
181
|
if (!Number.isFinite(maximumIntensity) || maximumIntensity <= 0) {
|
|
185
182
|
throw new Error("Gradient profile requires a positive resolved density range.");
|
package/src/grammar/histogram.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { MAX_GENERATED_ITEMS } from "../core/validation.js";
|
|
3
|
+
import {
|
|
4
|
+
alignNumericStep,
|
|
5
|
+
cleanNumericValue,
|
|
6
|
+
interpolateNumber,
|
|
7
|
+
NICE_FACTORS,
|
|
8
|
+
niceNumericStep,
|
|
9
|
+
numericExtent,
|
|
10
|
+
normalizeNumericRange
|
|
11
|
+
} from "./numeric.js";
|
|
4
12
|
|
|
5
13
|
function validateValues(values) {
|
|
6
14
|
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
@@ -23,14 +31,12 @@ export function validateHistogramBinStep(step) {
|
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export function validateHistogramBinBoundaries(boundaries) {
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
) {
|
|
34
|
+
if (Array.isArray(boundaries) && boundaries.length > MAX_GENERATED_ITEMS + 1) {
|
|
35
|
+
throw new RangeError(
|
|
36
|
+
`Histogram bin boundaries must generate at most ${MAX_GENERATED_ITEMS} bins.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (!validBoundaries(boundaries)) {
|
|
34
40
|
throw new TypeError(
|
|
35
41
|
"Histogram bin boundaries must contain at least two strictly increasing finite numbers."
|
|
36
42
|
);
|
|
@@ -38,6 +44,14 @@ export function validateHistogramBinBoundaries(boundaries) {
|
|
|
38
44
|
return boundaries;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
function validBoundaries(boundaries) {
|
|
48
|
+
return Array.isArray(boundaries) && boundaries.length >= 2 &&
|
|
49
|
+
boundaries.every(Number.isFinite) &&
|
|
50
|
+
!boundaries.some(
|
|
51
|
+
(value, index) => index > 0 && value <= boundaries[index - 1]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
export function normalizeHistogramBin(bin = {}) {
|
|
42
56
|
if (!isPlainObject(bin)) {
|
|
43
57
|
throw new TypeError("Histogram bin must be a plain object.");
|
|
@@ -78,24 +92,6 @@ function validateDomain(domain) {
|
|
|
78
92
|
return domain;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
function firstNiceStep(rough) {
|
|
82
|
-
const power = 10 ** Math.floor(Math.log10(rough));
|
|
83
|
-
const fraction = rough / power;
|
|
84
|
-
const factor = NICE_FACTORS.find(candidate => candidate >= fraction);
|
|
85
|
-
return (factor ?? 10) * power;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function nextNiceStep(step) {
|
|
89
|
-
const power = 10 ** Math.floor(Math.log10(step));
|
|
90
|
-
const fraction = step / power;
|
|
91
|
-
const factor = NICE_FACTORS.find(candidate => candidate > fraction + 1e-12);
|
|
92
|
-
return (factor ?? 10) * power;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function normalizeBoundary(value) {
|
|
96
|
-
return Number(value.toPrecision(15));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
95
|
function includesExtent(domain, values) {
|
|
100
96
|
return values.every(value => value >= domain[0] && value <= domain[1]);
|
|
101
97
|
}
|
|
@@ -122,11 +118,16 @@ function exactStepBoundaries(domain, step) {
|
|
|
122
118
|
);
|
|
123
119
|
}
|
|
124
120
|
const count = Math.round((domain[1] - domain[0]) / step);
|
|
121
|
+
if (!Number.isSafeInteger(count) || count > MAX_GENERATED_ITEMS) {
|
|
122
|
+
throw new RangeError(
|
|
123
|
+
`Histogram bin step must generate at most ${MAX_GENERATED_ITEMS} bins.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
125
126
|
return Array.from(
|
|
126
127
|
{ length: count + 1 },
|
|
127
128
|
(_, index) => index === count
|
|
128
129
|
? domain[1]
|
|
129
|
-
:
|
|
130
|
+
: cleanNumericValue(domain[0] + index * step, step)
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -144,17 +145,16 @@ function stepBins(values, step, domain, zero) {
|
|
|
144
145
|
throw new Error("Cannot infer histogram bins from no values.");
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
let minimum =
|
|
148
|
-
let maximum = Math.max(...values);
|
|
148
|
+
let [minimum, maximum] = numericExtent(values);
|
|
149
149
|
if (zero) {
|
|
150
150
|
minimum = Math.min(0, minimum);
|
|
151
151
|
maximum = Math.max(0, maximum);
|
|
152
152
|
}
|
|
153
|
-
let start =
|
|
154
|
-
let stop =
|
|
153
|
+
let start = cleanNumericValue(Math.floor(minimum / step) * step, step);
|
|
154
|
+
let stop = cleanNumericValue(Math.ceil(maximum / step) * step, step);
|
|
155
155
|
if (start === stop) {
|
|
156
|
-
if (stop <= 0) start =
|
|
157
|
-
else stop =
|
|
156
|
+
if (stop <= 0) start = cleanNumericValue(start - step, step);
|
|
157
|
+
else stop = cleanNumericValue(stop + step, step);
|
|
158
158
|
}
|
|
159
159
|
const resolvedDomain = [start, stop];
|
|
160
160
|
return {
|
|
@@ -183,41 +183,120 @@ function boundaryBins(values, boundaries, domain) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
function equalBins(domain, maxBins) {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
186
|
+
const normalized = normalizeNumericRange(domain[0], domain[1]);
|
|
187
|
+
const candidateStep = normalized.span / maxBins * normalized.scale;
|
|
188
|
+
const boundaries = [];
|
|
189
|
+
for (let index = 0; index <= maxBins; index += 1) {
|
|
190
|
+
const value = index === maxBins
|
|
191
|
+
? domain[1]
|
|
192
|
+
: cleanNumericValue(
|
|
193
|
+
interpolateNumber(domain[0], domain[1], index / maxBins),
|
|
194
|
+
candidateStep
|
|
195
|
+
);
|
|
196
|
+
if (boundaries.length === 0 || value > boundaries.at(-1)) {
|
|
197
|
+
boundaries.push(value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const step = Number.isFinite(candidateStep) && candidateStep > 0
|
|
201
|
+
? candidateStep
|
|
202
|
+
: firstFiniteBoundaryDifference(boundaries) ?? Number.MAX_VALUE;
|
|
203
|
+
return {
|
|
204
|
+
domain: [boundaries[0], boundaries.at(-1)],
|
|
205
|
+
step,
|
|
206
|
+
boundaries
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function firstFiniteBoundaryDifference(boundaries) {
|
|
211
|
+
for (let index = 1; index < boundaries.length; index += 1) {
|
|
212
|
+
const difference = boundaries[index] - boundaries[index - 1];
|
|
213
|
+
if (Number.isFinite(difference) && difference > 0) return difference;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function constantBins(value) {
|
|
219
|
+
const delta = Math.max(0.5, Math.abs(value) * Number.EPSILON);
|
|
220
|
+
let lower = value - delta;
|
|
221
|
+
let upper = value + delta;
|
|
222
|
+
if (!Number.isFinite(lower)) lower = value;
|
|
223
|
+
if (!Number.isFinite(upper)) upper = value;
|
|
224
|
+
if (lower === value) upper = value + delta;
|
|
225
|
+
if (upper === value) lower = value - delta;
|
|
226
|
+
const step = upper - lower;
|
|
227
|
+
return {
|
|
228
|
+
domain: [lower, upper],
|
|
229
|
+
step,
|
|
230
|
+
boundaries: [lower, upper]
|
|
231
|
+
};
|
|
195
232
|
}
|
|
196
233
|
|
|
197
234
|
function niceBins(extent, maxBins) {
|
|
198
|
-
const
|
|
199
|
-
let
|
|
235
|
+
const normalized = normalizeNumericRange(extent[0], extent[1]);
|
|
236
|
+
let normalizedStep = niceNumericStep(
|
|
237
|
+
normalized.span,
|
|
238
|
+
maxBins,
|
|
239
|
+
NICE_FACTORS
|
|
240
|
+
);
|
|
200
241
|
|
|
201
|
-
|
|
202
|
-
const start =
|
|
203
|
-
|
|
204
|
-
|
|
242
|
+
for (let attempt = 0; attempt < 64; attempt += 1) {
|
|
243
|
+
const start = alignNumericStep(
|
|
244
|
+
normalized.start,
|
|
245
|
+
normalizedStep,
|
|
246
|
+
"floor"
|
|
247
|
+
);
|
|
248
|
+
const stop = alignNumericStep(
|
|
249
|
+
normalized.end,
|
|
250
|
+
normalizedStep,
|
|
251
|
+
"ceil"
|
|
252
|
+
);
|
|
253
|
+
const count = Math.round((stop - start) / normalizedStep);
|
|
205
254
|
|
|
206
255
|
if (count > 0 && count <= maxBins) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
256
|
+
const resolution = normalizedStep * normalized.scale;
|
|
257
|
+
const boundaries = [];
|
|
258
|
+
for (let index = 0; index <= count; index += 1) {
|
|
259
|
+
let value = (index === count
|
|
260
|
+
? stop
|
|
261
|
+
: start + index * normalizedStep) * normalized.scale;
|
|
262
|
+
if (!Number.isFinite(value)) {
|
|
263
|
+
value = index === 0
|
|
264
|
+
? extent[0]
|
|
265
|
+
: index === count ? extent[1] : value;
|
|
266
|
+
}
|
|
267
|
+
value = cleanNumericValue(value, resolution);
|
|
268
|
+
if (index === 0 && value > extent[0]) value = extent[0];
|
|
269
|
+
if (index === count && value < extent[1]) value = extent[1];
|
|
270
|
+
if (
|
|
271
|
+
Number.isFinite(value) &&
|
|
272
|
+
(boundaries.length === 0 || value > boundaries.at(-1))
|
|
273
|
+
) {
|
|
274
|
+
boundaries.push(value);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
boundaries.length >= 2 &&
|
|
279
|
+
boundaries[0] <= extent[0] &&
|
|
280
|
+
boundaries.at(-1) >= extent[1]
|
|
281
|
+
) {
|
|
282
|
+
const step = Number.isFinite(resolution) && resolution > 0
|
|
283
|
+
? resolution
|
|
284
|
+
: firstFiniteBoundaryDifference(boundaries) ?? Number.MAX_VALUE;
|
|
285
|
+
return {
|
|
286
|
+
domain: [boundaries[0], boundaries.at(-1)],
|
|
287
|
+
step,
|
|
288
|
+
boundaries
|
|
289
|
+
};
|
|
290
|
+
}
|
|
218
291
|
}
|
|
219
|
-
|
|
292
|
+
normalizedStep = niceNumericStep(
|
|
293
|
+
normalizedStep * (1 + 1e-12),
|
|
294
|
+
1,
|
|
295
|
+
NICE_FACTORS
|
|
296
|
+
);
|
|
220
297
|
}
|
|
298
|
+
|
|
299
|
+
return equalBins(extent, maxBins);
|
|
221
300
|
}
|
|
222
301
|
|
|
223
302
|
export function resolveHistogramBins({
|
|
@@ -254,7 +333,7 @@ export function resolveHistogramBins({
|
|
|
254
333
|
);
|
|
255
334
|
}
|
|
256
335
|
|
|
257
|
-
maxBins = normalizedBin.maxBins;
|
|
336
|
+
maxBins = Math.min(normalizedBin.maxBins, MAX_GENERATED_ITEMS);
|
|
258
337
|
|
|
259
338
|
if (domain !== "auto") {
|
|
260
339
|
return cloneAndFreeze(equalBins(validateDomain(domain), maxBins));
|
|
@@ -263,24 +342,14 @@ export function resolveHistogramBins({
|
|
|
263
342
|
throw new Error("Cannot infer histogram bins from no values.");
|
|
264
343
|
}
|
|
265
344
|
|
|
266
|
-
let minimum = values
|
|
267
|
-
let maximum = values[0];
|
|
268
|
-
|
|
269
|
-
for (const value of values.slice(1)) {
|
|
270
|
-
minimum = Math.min(minimum, value);
|
|
271
|
-
maximum = Math.max(maximum, value);
|
|
272
|
-
}
|
|
345
|
+
let [minimum, maximum] = numericExtent(values);
|
|
273
346
|
|
|
274
347
|
if (zero) {
|
|
275
348
|
minimum = Math.min(0, minimum);
|
|
276
349
|
maximum = Math.max(0, maximum);
|
|
277
350
|
}
|
|
278
351
|
if (minimum === maximum) {
|
|
279
|
-
return cloneAndFreeze(
|
|
280
|
-
domain: [minimum - 0.5, maximum + 0.5],
|
|
281
|
-
step: 1,
|
|
282
|
-
boundaries: [minimum - 0.5, maximum + 0.5]
|
|
283
|
-
});
|
|
352
|
+
return cloneAndFreeze(constantBins(minimum));
|
|
284
353
|
}
|
|
285
354
|
|
|
286
355
|
const resolved = nice
|
|
@@ -292,14 +361,7 @@ export function resolveHistogramBins({
|
|
|
292
361
|
export function countHistogramBins(values, boundaries) {
|
|
293
362
|
validateValues(values);
|
|
294
363
|
|
|
295
|
-
if (
|
|
296
|
-
!Array.isArray(boundaries) ||
|
|
297
|
-
boundaries.length < 2 ||
|
|
298
|
-
!boundaries.every(Number.isFinite) ||
|
|
299
|
-
boundaries.some(
|
|
300
|
-
(value, index) => index > 0 && value <= boundaries[index - 1]
|
|
301
|
-
)
|
|
302
|
-
) {
|
|
364
|
+
if (!validBoundaries(boundaries)) {
|
|
303
365
|
throw new TypeError(
|
|
304
366
|
"Histogram boundaries must be ascending finite numbers."
|
|
305
367
|
);
|
package/src/grammar/horizon.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateGeneratedItemLimit,
|
|
4
|
+
validateNonEmptyString as requireField
|
|
5
|
+
} from "../core/validation.js";
|
|
2
6
|
import { normalizePalette } from "./palettes.js";
|
|
7
|
+
import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
8
|
+
import { requireFiniteResult } from "./numeric.js";
|
|
3
9
|
import {
|
|
4
10
|
isNominalValue,
|
|
5
11
|
normalizeTemporalValue
|
|
@@ -14,13 +20,6 @@ const OUTPUT_PROPERTIES = Object.freeze([
|
|
|
14
20
|
"x", "lower", "upper", "group", "color", "sign", "band", "segment"
|
|
15
21
|
]);
|
|
16
22
|
|
|
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
23
|
function validateEncoding(value, role) {
|
|
25
24
|
if (!isPlainObject(value)) {
|
|
26
25
|
throw new TypeError(`Horizon ${role} encoding must be a plain object.`);
|
|
@@ -32,13 +31,14 @@ function validateEncoding(value, role) {
|
|
|
32
31
|
throw new Error(`Unknown Horizon ${role} encoding property "${unknown}".`);
|
|
33
32
|
}
|
|
34
33
|
const field = requireField(value.field, `Horizon ${role} field`);
|
|
35
|
-
const fieldType = value.fieldType ??
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const fieldType = value.fieldType ?? "quantitative";
|
|
35
|
+
const supportsTemporal = role === "x";
|
|
36
|
+
if (
|
|
37
|
+
fieldType !== "quantitative" &&
|
|
38
|
+
(!supportsTemporal || fieldType !== "temporal")
|
|
39
|
+
) {
|
|
40
40
|
throw new Error(
|
|
41
|
-
`Horizon ${role} fieldType must be ${
|
|
41
|
+
`Horizon ${role} fieldType must be quantitative${supportsTemporal ? " or temporal" : ""}.`
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
return { field, fieldType };
|
|
@@ -140,6 +140,7 @@ export function validateHorizonTransform(transform) {
|
|
|
140
140
|
if (!Number.isInteger(transform.bands) || transform.bands <= 0) {
|
|
141
141
|
throw new RangeError("Horizon bands must be a positive integer.");
|
|
142
142
|
}
|
|
143
|
+
validateGeneratedItemLimit(transform.bands, "Horizon bands");
|
|
143
144
|
if (!Number.isFinite(transform.baseline)) {
|
|
144
145
|
throw new TypeError("Horizon baseline must be finite.");
|
|
145
146
|
}
|
|
@@ -254,7 +255,14 @@ function splitSegments(rows, baseline) {
|
|
|
254
255
|
flush();
|
|
255
256
|
continue;
|
|
256
257
|
}
|
|
257
|
-
current.push({
|
|
258
|
+
current.push({
|
|
259
|
+
...row,
|
|
260
|
+
signed: requireFiniteResult(
|
|
261
|
+
row.y - baseline,
|
|
262
|
+
"Horizon signed deviation"
|
|
263
|
+
),
|
|
264
|
+
interpolated: false
|
|
265
|
+
});
|
|
258
266
|
}
|
|
259
267
|
flush();
|
|
260
268
|
return segments;
|
|
@@ -267,10 +275,10 @@ function insertCrossings(points, baseline) {
|
|
|
267
275
|
const left = points[index - 1];
|
|
268
276
|
const right = points[index];
|
|
269
277
|
if (left.signed * right.signed < 0) {
|
|
270
|
-
const fraction = (baseline
|
|
278
|
+
const fraction = inverseLerp(baseline, left.y, right.y);
|
|
271
279
|
output.push({
|
|
272
280
|
sourceRowIndex: undefined,
|
|
273
|
-
x: left.x
|
|
281
|
+
x: interpolateNumber(left.x, right.x, fraction),
|
|
274
282
|
y: baseline,
|
|
275
283
|
missing: false,
|
|
276
284
|
signed: 0,
|
|
@@ -323,23 +331,51 @@ function runsForSign(points, desired) {
|
|
|
323
331
|
}
|
|
324
332
|
|
|
325
333
|
function maximumMagnitude(group) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
334
|
+
let maximum = 0;
|
|
335
|
+
for (const segment of group.segments ?? [group]) {
|
|
336
|
+
for (const point of segment) {
|
|
337
|
+
maximum = Math.max(maximum, Math.abs(point.signed));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return maximum;
|
|
332
341
|
}
|
|
333
342
|
|
|
334
343
|
function resolvedExtents(groups, transform) {
|
|
335
344
|
if (transform.extent !== "auto") return groups.map(() => transform.extent);
|
|
336
345
|
if (transform.resolve === "shared") {
|
|
337
|
-
|
|
346
|
+
let shared = 0;
|
|
347
|
+
for (const group of groups) shared = Math.max(shared, maximumMagnitude(group));
|
|
338
348
|
return groups.map(() => shared);
|
|
339
349
|
}
|
|
340
350
|
return groups.map(maximumMagnitude);
|
|
341
351
|
}
|
|
342
352
|
|
|
353
|
+
function validateOutputBudget(groups, extents, transform) {
|
|
354
|
+
let count = 0;
|
|
355
|
+
for (const [groupIndex, group] of groups.entries()) {
|
|
356
|
+
const extent = extents[groupIndex];
|
|
357
|
+
if (extent === 0) continue;
|
|
358
|
+
const bandHeight = extent / transform.bands;
|
|
359
|
+
if (!(bandHeight > 0) || !Number.isFinite(bandHeight)) {
|
|
360
|
+
throw new RangeError(
|
|
361
|
+
"Horizon band height is outside the positive finite numeric range."
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
for (const sign of ["negative", "positive"]) {
|
|
365
|
+
for (const segment of group.segments) {
|
|
366
|
+
for (const run of runsForSign(segment, sign)) {
|
|
367
|
+
const magnitude = maximumMagnitude(run);
|
|
368
|
+
count += Math.min(
|
|
369
|
+
transform.bands,
|
|
370
|
+
Math.ceil(Math.min(magnitude, extent) / bandHeight)
|
|
371
|
+
) * run.length;
|
|
372
|
+
validateGeneratedItemLimit(count, "Horizon generated row count");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
343
379
|
function seriesForGroup(group, extent, transform) {
|
|
344
380
|
if (extent === 0) return [];
|
|
345
381
|
const bandHeight = extent / transform.bands;
|
|
@@ -437,6 +473,7 @@ export function deriveHorizon(rows, requested) {
|
|
|
437
473
|
throw new Error("Horizon requires at least one finite y value.");
|
|
438
474
|
}
|
|
439
475
|
const extents = resolvedExtents(grouped, transform);
|
|
476
|
+
validateOutputBudget(grouped, extents, transform);
|
|
440
477
|
const groups = grouped.map((group, index) => ({
|
|
441
478
|
...(transform.groupBy === undefined ? {} : { group: group.group }),
|
|
442
479
|
extent: extents[index],
|
package/src/grammar/interval.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
3
|
+
import { requireFiniteResult, stableDecimal } from "./numeric.js";
|
|
3
4
|
import { studentTCriticalValue } from "./statistics/studentT.js";
|
|
4
5
|
|
|
5
|
-
const CENTER_VALUES =
|
|
6
|
-
const EXTENT_VALUES =
|
|
7
|
-
const TRANSFORM_KEYS =
|
|
6
|
+
const CENTER_VALUES = ["mean", "median"];
|
|
7
|
+
const EXTENT_VALUES = ["stderr", "stdev", "ci", "iqr"];
|
|
8
|
+
const TRANSFORM_KEYS = [
|
|
8
9
|
"type",
|
|
9
10
|
"field",
|
|
10
11
|
"groupBy",
|
|
@@ -12,8 +13,8 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
12
13
|
"extent",
|
|
13
14
|
"level",
|
|
14
15
|
"as"
|
|
15
|
-
]
|
|
16
|
-
const OUTPUT_KEYS =
|
|
16
|
+
];
|
|
17
|
+
const OUTPUT_KEYS = ["center", "lower", "upper"];
|
|
17
18
|
|
|
18
19
|
function nonEmptyString(value, label) {
|
|
19
20
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -154,10 +155,6 @@ function isGroupValue(value) {
|
|
|
154
155
|
(typeof value === "number" && Number.isFinite(value));
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
function stableNumber(value) {
|
|
158
|
-
return Number(value.toFixed(12));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
158
|
function deriveGroup(values, transform) {
|
|
162
159
|
if (transform.center === "median") {
|
|
163
160
|
if (values.length === 0) return undefined;
|
|
@@ -175,7 +172,11 @@ function deriveGroup(values, transform) {
|
|
|
175
172
|
? aggregateScalarValues(values, "stderr")
|
|
176
173
|
: studentTCritical(values.length - 1, transform.level) *
|
|
177
174
|
aggregateScalarValues(values, "stderr");
|
|
178
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
center,
|
|
177
|
+
lower: requireFiniteResult(center - spread, "Interval lower endpoint"),
|
|
178
|
+
upper: requireFiniteResult(center + spread, "Interval upper endpoint")
|
|
179
|
+
};
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
export function deriveInterval(rows, transform) {
|
|
@@ -218,9 +219,9 @@ export function deriveInterval(rows, transform) {
|
|
|
218
219
|
if (result === undefined) continue;
|
|
219
220
|
output.push({
|
|
220
221
|
...group.fields,
|
|
221
|
-
[transform.as.center]:
|
|
222
|
-
[transform.as.lower]:
|
|
223
|
-
[transform.as.upper]:
|
|
222
|
+
[transform.as.center]: stableDecimal(result.center),
|
|
223
|
+
[transform.as.lower]: stableDecimal(result.lower),
|
|
224
|
+
[transform.as.upper]: stableDecimal(result.upper)
|
|
224
225
|
});
|
|
225
226
|
}
|
|
226
227
|
return cloneAndFreeze(output);
|