ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const FILTER_COMPARISON_OPERATORS = Object.freeze([
|
|
4
|
+
"eq", "neq", "lt", "lte", "gt", "gte"
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
function isFiniteOrString(value) {
|
|
8
|
+
return typeof value === "string" || Number.isFinite(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validatePredicate(predicate) {
|
|
12
|
+
if (!isPlainObject(predicate)) {
|
|
13
|
+
throw new TypeError("Filter predicate must be a plain object.");
|
|
14
|
+
}
|
|
15
|
+
const unknown = Object.keys(predicate).find(
|
|
16
|
+
key => !["op", "value"].includes(key)
|
|
17
|
+
);
|
|
18
|
+
if (unknown !== undefined) {
|
|
19
|
+
throw new Error(`Unknown filter predicate property "${unknown}".`);
|
|
20
|
+
}
|
|
21
|
+
if (!FILTER_COMPARISON_OPERATORS.includes(predicate.op)) {
|
|
22
|
+
throw new Error(`Unsupported filter comparison operator "${predicate.op}".`);
|
|
23
|
+
}
|
|
24
|
+
if (!Object.hasOwn(predicate, "value")) {
|
|
25
|
+
throw new TypeError("Filter predicate requires a value.");
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
!["eq", "neq"].includes(predicate.op) &&
|
|
29
|
+
!isFiniteOrString(predicate.value)
|
|
30
|
+
) {
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
"Ordered filter comparison values must be finite numbers or strings."
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateRange(range) {
|
|
38
|
+
if (!isPlainObject(range)) {
|
|
39
|
+
throw new TypeError("Filter range must be a plain object.");
|
|
40
|
+
}
|
|
41
|
+
const unknown = Object.keys(range).find(
|
|
42
|
+
key => !["min", "max", "inclusive"].includes(key)
|
|
43
|
+
);
|
|
44
|
+
if (unknown !== undefined) {
|
|
45
|
+
throw new Error(`Unknown filter range property "${unknown}".`);
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
!isFiniteOrString(range.min) ||
|
|
49
|
+
!isFiniteOrString(range.max) ||
|
|
50
|
+
typeof range.min !== typeof range.max
|
|
51
|
+
) {
|
|
52
|
+
throw new TypeError(
|
|
53
|
+
"Filter range endpoints must be finite numbers or strings of one type."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (range.min > range.max) {
|
|
57
|
+
throw new RangeError("Filter range min must not exceed max.");
|
|
58
|
+
}
|
|
59
|
+
if (range.inclusive !== undefined && typeof range.inclusive !== "boolean") {
|
|
60
|
+
throw new TypeError("Filter range inclusive must be a boolean.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateFilterTransform(transform) {
|
|
65
|
+
if (!isPlainObject(transform)) {
|
|
66
|
+
throw new TypeError("Filter transform must be a plain object.");
|
|
67
|
+
}
|
|
68
|
+
const supported = ["type", "field", "oneOf", "predicate", "range"];
|
|
69
|
+
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
70
|
+
if (unknown !== undefined) {
|
|
71
|
+
throw new Error(`Unknown filter transform property "${unknown}".`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof transform.field !== "string" || transform.field.length === 0) {
|
|
74
|
+
throw new TypeError("Filter field must be a non-empty field string.");
|
|
75
|
+
}
|
|
76
|
+
const modes = ["oneOf", "predicate", "range"].filter(
|
|
77
|
+
key => Object.hasOwn(transform, key)
|
|
78
|
+
);
|
|
79
|
+
if (modes.length !== 1) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"Filter transform requires exactly one of oneOf, predicate, or range."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (modes[0] === "predicate") {
|
|
85
|
+
validatePredicate(transform.predicate);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (modes[0] === "range") {
|
|
89
|
+
validateRange(transform.range);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
!Array.isArray(transform.oneOf) ||
|
|
94
|
+
transform.oneOf.length === 0 ||
|
|
95
|
+
transform.oneOf.some(value =>
|
|
96
|
+
value !== null &&
|
|
97
|
+
typeof value !== "string" &&
|
|
98
|
+
typeof value !== "boolean" &&
|
|
99
|
+
!(typeof value === "number" && Number.isFinite(value))
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
throw new TypeError("Filter oneOf must be a non-empty array of scalar values.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function normalizeFilterTransform({
|
|
107
|
+
field,
|
|
108
|
+
oneOf,
|
|
109
|
+
predicate,
|
|
110
|
+
range
|
|
111
|
+
}) {
|
|
112
|
+
const modes = { oneOf, predicate, range };
|
|
113
|
+
const selected = Object.keys(modes).filter(key => modes[key] !== undefined);
|
|
114
|
+
const transform = {
|
|
115
|
+
type: "filter",
|
|
116
|
+
field,
|
|
117
|
+
...(selected[0] === "oneOf" ? { oneOf } : {}),
|
|
118
|
+
...(selected[0] === "predicate" ? { predicate } : {}),
|
|
119
|
+
...(selected[0] === "range"
|
|
120
|
+
? { range: { ...range, inclusive: range?.inclusive ?? true } }
|
|
121
|
+
: {})
|
|
122
|
+
};
|
|
123
|
+
if (selected.length !== 1) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"filterData requires exactly one of oneOf, predicate, or range."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
validateFilterTransform(transform);
|
|
129
|
+
return cloneAndFreeze(transform);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function comparable(value, operand) {
|
|
133
|
+
return (
|
|
134
|
+
Number.isFinite(value) && Number.isFinite(operand)
|
|
135
|
+
) || (
|
|
136
|
+
typeof value === "string" && typeof operand === "string"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function matches(value, transform) {
|
|
141
|
+
if (transform.oneOf !== undefined) {
|
|
142
|
+
return transform.oneOf.includes(value);
|
|
143
|
+
}
|
|
144
|
+
if (transform.predicate !== undefined) {
|
|
145
|
+
const { op, value: operand } = transform.predicate;
|
|
146
|
+
if (op === "eq") return value === operand;
|
|
147
|
+
if (op === "neq") return value !== operand;
|
|
148
|
+
if (!comparable(value, operand)) return false;
|
|
149
|
+
if (op === "lt") return value < operand;
|
|
150
|
+
if (op === "lte") return value <= operand;
|
|
151
|
+
if (op === "gt") return value > operand;
|
|
152
|
+
return value >= operand;
|
|
153
|
+
}
|
|
154
|
+
const { min, max, inclusive = true } = transform.range;
|
|
155
|
+
if (!comparable(value, min) || !comparable(value, max)) return false;
|
|
156
|
+
return inclusive
|
|
157
|
+
? value >= min && value <= max
|
|
158
|
+
: value > min && value < max;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function deriveFilteredRows(values, transform) {
|
|
162
|
+
if (!Array.isArray(values)) {
|
|
163
|
+
throw new TypeError("Filter source values must be an array.");
|
|
164
|
+
}
|
|
165
|
+
validateFilterTransform(transform);
|
|
166
|
+
return values.filter(row =>
|
|
167
|
+
isPlainObject(row) && matches(row[transform.field], transform)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const NICE_FACTORS = Object.freeze([1, 2, 3, 5]);
|
|
4
|
+
|
|
5
|
+
function validateValues(values) {
|
|
6
|
+
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
7
|
+
throw new TypeError("Histogram values must be finite numbers.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validateMaxBins(maxBins) {
|
|
12
|
+
if (!Number.isInteger(maxBins) || maxBins <= 0) {
|
|
13
|
+
throw new TypeError("Histogram maxBins must be a positive integer.");
|
|
14
|
+
}
|
|
15
|
+
return maxBins;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateHistogramBinStep(step) {
|
|
19
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
20
|
+
throw new TypeError("Histogram bin step must be a positive finite number.");
|
|
21
|
+
}
|
|
22
|
+
return step;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateHistogramBinBoundaries(boundaries) {
|
|
26
|
+
if (
|
|
27
|
+
!Array.isArray(boundaries) ||
|
|
28
|
+
boundaries.length < 2 ||
|
|
29
|
+
!boundaries.every(Number.isFinite) ||
|
|
30
|
+
boundaries.some(
|
|
31
|
+
(value, index) => index > 0 && value <= boundaries[index - 1]
|
|
32
|
+
)
|
|
33
|
+
) {
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
"Histogram bin boundaries must contain at least two strictly increasing finite numbers."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return boundaries;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeHistogramBin(bin = {}) {
|
|
42
|
+
if (!isPlainObject(bin)) {
|
|
43
|
+
throw new TypeError("Histogram bin must be a plain object.");
|
|
44
|
+
}
|
|
45
|
+
const supported = ["maxBins", "step", "boundaries"];
|
|
46
|
+
const unknown = Object.keys(bin).find(key => !supported.includes(key));
|
|
47
|
+
if (unknown !== undefined) {
|
|
48
|
+
throw new Error(`Unknown bin option "${unknown}".`);
|
|
49
|
+
}
|
|
50
|
+
const selected = supported.filter(key => bin[key] !== undefined);
|
|
51
|
+
if (selected.length > 1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Histogram bin accepts only one of maxBins, step, or boundaries."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (bin.step !== undefined) {
|
|
57
|
+
return cloneAndFreeze({ step: validateHistogramBinStep(bin.step) });
|
|
58
|
+
}
|
|
59
|
+
if (bin.boundaries !== undefined) {
|
|
60
|
+
return cloneAndFreeze({
|
|
61
|
+
boundaries: validateHistogramBinBoundaries(bin.boundaries)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return cloneAndFreeze({ maxBins: validateMaxBins(bin.maxBins ?? 10) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateDomain(domain) {
|
|
68
|
+
if (
|
|
69
|
+
!Array.isArray(domain) ||
|
|
70
|
+
domain.length !== 2 ||
|
|
71
|
+
!domain.every(Number.isFinite) ||
|
|
72
|
+
domain[0] >= domain[1]
|
|
73
|
+
) {
|
|
74
|
+
throw new TypeError(
|
|
75
|
+
"Histogram domain must be two ascending finite numbers."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return domain;
|
|
79
|
+
}
|
|
80
|
+
|
|
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
|
+
function includesExtent(domain, values) {
|
|
100
|
+
return values.every(value => value >= domain[0] && value <= domain[1]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function requireContainedExtent(domain, values, label) {
|
|
104
|
+
if (!includesExtent(domain, values)) {
|
|
105
|
+
throw new RangeError(`${label} must contain the histogram data extent.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isStepMultiple(value, step) {
|
|
110
|
+
const quotient = value / step;
|
|
111
|
+
return Math.abs(quotient - Math.round(quotient)) <= 1e-10;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function exactStepBoundaries(domain, step) {
|
|
115
|
+
if (
|
|
116
|
+
!isStepMultiple(domain[0], step) ||
|
|
117
|
+
!isStepMultiple(domain[1], step) ||
|
|
118
|
+
!isStepMultiple(domain[1] - domain[0], step)
|
|
119
|
+
) {
|
|
120
|
+
throw new RangeError(
|
|
121
|
+
"Histogram domain endpoints must align with the zero-anchored bin step."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const count = Math.round((domain[1] - domain[0]) / step);
|
|
125
|
+
return Array.from(
|
|
126
|
+
{ length: count + 1 },
|
|
127
|
+
(_, index) => index === count
|
|
128
|
+
? domain[1]
|
|
129
|
+
: normalizeBoundary(domain[0] + index * step)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stepBins(values, step, domain, zero) {
|
|
134
|
+
if (domain !== "auto") {
|
|
135
|
+
const explicit = validateDomain(domain);
|
|
136
|
+
requireContainedExtent(explicit, values, "Histogram domain");
|
|
137
|
+
return {
|
|
138
|
+
domain: explicit,
|
|
139
|
+
step,
|
|
140
|
+
boundaries: exactStepBoundaries(explicit, step)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (values.length === 0) {
|
|
144
|
+
throw new Error("Cannot infer histogram bins from no values.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let minimum = Math.min(...values);
|
|
148
|
+
let maximum = Math.max(...values);
|
|
149
|
+
if (zero) {
|
|
150
|
+
minimum = Math.min(0, minimum);
|
|
151
|
+
maximum = Math.max(0, maximum);
|
|
152
|
+
}
|
|
153
|
+
let start = normalizeBoundary(Math.floor(minimum / step) * step);
|
|
154
|
+
let stop = normalizeBoundary(Math.ceil(maximum / step) * step);
|
|
155
|
+
if (start === stop) {
|
|
156
|
+
if (stop <= 0) start = normalizeBoundary(start - step);
|
|
157
|
+
else stop = normalizeBoundary(stop + step);
|
|
158
|
+
}
|
|
159
|
+
const resolvedDomain = [start, stop];
|
|
160
|
+
return {
|
|
161
|
+
domain: resolvedDomain,
|
|
162
|
+
step,
|
|
163
|
+
boundaries: exactStepBoundaries(resolvedDomain, step)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function boundaryBins(values, boundaries, domain) {
|
|
168
|
+
const resolvedBoundaries = [...boundaries];
|
|
169
|
+
const boundaryDomain = [resolvedBoundaries[0], resolvedBoundaries.at(-1)];
|
|
170
|
+
requireContainedExtent(boundaryDomain, values, "Histogram bin boundaries");
|
|
171
|
+
if (domain !== "auto") {
|
|
172
|
+
const explicit = validateDomain(domain);
|
|
173
|
+
if (
|
|
174
|
+
explicit[0] !== boundaryDomain[0] ||
|
|
175
|
+
explicit[1] !== boundaryDomain[1]
|
|
176
|
+
) {
|
|
177
|
+
throw new RangeError(
|
|
178
|
+
"Histogram domain must match the explicit bin boundary endpoints."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { domain: boundaryDomain, boundaries: resolvedBoundaries };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function equalBins(domain, maxBins) {
|
|
186
|
+
const step = (domain[1] - domain[0]) / maxBins;
|
|
187
|
+
const boundaries = Array.from(
|
|
188
|
+
{ length: maxBins + 1 },
|
|
189
|
+
(_, index) =>
|
|
190
|
+
index === maxBins
|
|
191
|
+
? domain[1]
|
|
192
|
+
: normalizeBoundary(domain[0] + index * step)
|
|
193
|
+
);
|
|
194
|
+
return { domain, step, boundaries };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function niceBins(extent, maxBins) {
|
|
198
|
+
const span = extent[1] - extent[0];
|
|
199
|
+
let step = firstNiceStep(span / maxBins);
|
|
200
|
+
|
|
201
|
+
while (true) {
|
|
202
|
+
const start = normalizeBoundary(Math.floor(extent[0] / step) * step);
|
|
203
|
+
const stop = normalizeBoundary(Math.ceil(extent[1] / step) * step);
|
|
204
|
+
const count = Math.round((stop - start) / step);
|
|
205
|
+
|
|
206
|
+
if (count > 0 && count <= maxBins) {
|
|
207
|
+
return {
|
|
208
|
+
domain: [start, stop],
|
|
209
|
+
step,
|
|
210
|
+
boundaries: Array.from(
|
|
211
|
+
{ length: count + 1 },
|
|
212
|
+
(_, index) =>
|
|
213
|
+
index === count
|
|
214
|
+
? stop
|
|
215
|
+
: normalizeBoundary(start + index * step)
|
|
216
|
+
)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
step = nextNiceStep(step);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function resolveHistogramBins({
|
|
224
|
+
values,
|
|
225
|
+
bin,
|
|
226
|
+
maxBins,
|
|
227
|
+
domain = "auto",
|
|
228
|
+
nice = true,
|
|
229
|
+
zero = false
|
|
230
|
+
}) {
|
|
231
|
+
validateValues(values);
|
|
232
|
+
if (bin !== undefined && maxBins !== undefined) {
|
|
233
|
+
throw new Error("Histogram bins require either bin or maxBins, not both.");
|
|
234
|
+
}
|
|
235
|
+
const normalizedBin = normalizeHistogramBin(
|
|
236
|
+
bin ?? (maxBins === undefined ? {} : { maxBins })
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (typeof nice !== "boolean") {
|
|
240
|
+
throw new TypeError("Histogram nice must be a boolean.");
|
|
241
|
+
}
|
|
242
|
+
if (typeof zero !== "boolean") {
|
|
243
|
+
throw new TypeError("Histogram zero must be a boolean.");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (normalizedBin.step !== undefined) {
|
|
247
|
+
return cloneAndFreeze(
|
|
248
|
+
stepBins(values, normalizedBin.step, domain, zero)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (normalizedBin.boundaries !== undefined) {
|
|
252
|
+
return cloneAndFreeze(
|
|
253
|
+
boundaryBins(values, normalizedBin.boundaries, domain)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
maxBins = normalizedBin.maxBins;
|
|
258
|
+
|
|
259
|
+
if (domain !== "auto") {
|
|
260
|
+
return cloneAndFreeze(equalBins(validateDomain(domain), maxBins));
|
|
261
|
+
}
|
|
262
|
+
if (values.length === 0) {
|
|
263
|
+
throw new Error("Cannot infer histogram bins from no values.");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let minimum = values[0];
|
|
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
|
+
}
|
|
273
|
+
|
|
274
|
+
if (zero) {
|
|
275
|
+
minimum = Math.min(0, minimum);
|
|
276
|
+
maximum = Math.max(0, maximum);
|
|
277
|
+
}
|
|
278
|
+
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
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const resolved = nice
|
|
287
|
+
? niceBins([minimum, maximum], maxBins)
|
|
288
|
+
: equalBins([minimum, maximum], maxBins);
|
|
289
|
+
return cloneAndFreeze(resolved);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function countHistogramBins(values, boundaries) {
|
|
293
|
+
validateValues(values);
|
|
294
|
+
|
|
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
|
+
) {
|
|
303
|
+
throw new TypeError(
|
|
304
|
+
"Histogram boundaries must be ascending finite numbers."
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const counts = Array(boundaries.length - 1).fill(0);
|
|
309
|
+
|
|
310
|
+
for (const value of values) {
|
|
311
|
+
const index = findHistogramBinIndex(value, boundaries);
|
|
312
|
+
if (index !== -1) counts[index] += 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return cloneAndFreeze(counts);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function findHistogramBinIndex(value, boundaries) {
|
|
319
|
+
if (!Number.isFinite(value)) {
|
|
320
|
+
throw new TypeError("Histogram value must be finite.");
|
|
321
|
+
}
|
|
322
|
+
if (value < boundaries[0] || value > boundaries.at(-1)) return -1;
|
|
323
|
+
|
|
324
|
+
let low = 0;
|
|
325
|
+
let high = boundaries.length - 1;
|
|
326
|
+
while (low + 1 < high) {
|
|
327
|
+
const middle = Math.floor((low + high) / 2);
|
|
328
|
+
if (value < boundaries[middle]) high = middle;
|
|
329
|
+
else low = middle;
|
|
330
|
+
}
|
|
331
|
+
return Math.min(low, boundaries.length - 2);
|
|
332
|
+
}
|