insomni-plot 0.1.0-alpha.0
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/LICENSE.md +674 -0
- package/README.md +81 -0
- package/dist/core.d.mts +340 -0
- package/dist/core.mjs +1047 -0
- package/dist/index.d.mts +3426 -0
- package/dist/index.mjs +12762 -0
- package/dist/interactions-DEFL_F4E.mjs +5395 -0
- package/dist/range-presets-CzECsu3V.d.mts +1523 -0
- package/package.json +34 -0
- package/src/annotations.d.ts +121 -0
- package/src/annotations.ts +438 -0
- package/src/axis.d.ts +184 -0
- package/src/axis.test.ts +131 -0
- package/src/axis.ts +765 -0
- package/src/colorbar.d.ts +69 -0
- package/src/colorbar.ts +294 -0
- package/src/colors.d.ts +57 -0
- package/src/colors.test.ts +28 -0
- package/src/colors.ts +486 -0
- package/src/core.ts +299 -0
- package/src/format.d.ts +54 -0
- package/src/format.ts +138 -0
- package/src/grammar/accessibility.d.ts +147 -0
- package/src/grammar/accessibility.test.ts +199 -0
- package/src/grammar/accessibility.ts +443 -0
- package/src/grammar/aes.d.ts +35 -0
- package/src/grammar/aes.test.ts +75 -0
- package/src/grammar/aes.ts +120 -0
- package/src/grammar/annotations.d.ts +86 -0
- package/src/grammar/annotations.test.ts +68 -0
- package/src/grammar/annotations.ts +336 -0
- package/src/grammar/attach-brush.d.ts +44 -0
- package/src/grammar/attach-brush.test.ts +214 -0
- package/src/grammar/attach-brush.ts +111 -0
- package/src/grammar/attach-presets.d.ts +33 -0
- package/src/grammar/attach-presets.test.ts +106 -0
- package/src/grammar/attach-presets.ts +215 -0
- package/src/grammar/chart.d.ts +952 -0
- package/src/grammar/chart.test.ts +118 -0
- package/src/grammar/chart.ts +1172 -0
- package/src/grammar/color-utils.d.ts +29 -0
- package/src/grammar/color-utils.test.ts +53 -0
- package/src/grammar/color-utils.ts +66 -0
- package/src/grammar/constants.d.ts +45 -0
- package/src/grammar/constants.ts +61 -0
- package/src/grammar/coord.d.ts +183 -0
- package/src/grammar/coord.test.ts +355 -0
- package/src/grammar/coord.ts +619 -0
- package/src/grammar/data/pivot.d.ts +57 -0
- package/src/grammar/data/pivot.ts +107 -0
- package/src/grammar/emphasis-driver.d.ts +69 -0
- package/src/grammar/emphasis-driver.test.ts +199 -0
- package/src/grammar/emphasis-driver.ts +205 -0
- package/src/grammar/equality.d.ts +3 -0
- package/src/grammar/equality.ts +40 -0
- package/src/grammar/facet.d.ts +63 -0
- package/src/grammar/facet.test.ts +60 -0
- package/src/grammar/facet.ts +175 -0
- package/src/grammar/geoms/_categorical.d.ts +94 -0
- package/src/grammar/geoms/_categorical.ts +0 -0
- package/src/grammar/geoms/_distribution.d.ts +52 -0
- package/src/grammar/geoms/_distribution.ts +125 -0
- package/src/grammar/geoms/_mark.d.ts +69 -0
- package/src/grammar/geoms/_mark.ts +136 -0
- package/src/grammar/geoms/_shape.d.ts +41 -0
- package/src/grammar/geoms/_shape.ts +74 -0
- package/src/grammar/geoms/aggregate.d.ts +95 -0
- package/src/grammar/geoms/aggregate.test.ts +554 -0
- package/src/grammar/geoms/aggregate.ts +840 -0
- package/src/grammar/geoms/area.d.ts +32 -0
- package/src/grammar/geoms/area.test.ts +165 -0
- package/src/grammar/geoms/area.ts +578 -0
- package/src/grammar/geoms/band.d.ts +27 -0
- package/src/grammar/geoms/band.test.ts +57 -0
- package/src/grammar/geoms/band.ts +126 -0
- package/src/grammar/geoms/bar.d.ts +56 -0
- package/src/grammar/geoms/bar.test.ts +367 -0
- package/src/grammar/geoms/bar.ts +1054 -0
- package/src/grammar/geoms/boxplot.d.ts +129 -0
- package/src/grammar/geoms/boxplot.test.ts +299 -0
- package/src/grammar/geoms/boxplot.ts +834 -0
- package/src/grammar/geoms/connected-scatter.d.ts +27 -0
- package/src/grammar/geoms/connected-scatter.test.ts +157 -0
- package/src/grammar/geoms/connected-scatter.ts +63 -0
- package/src/grammar/geoms/emphasis.d.ts +76 -0
- package/src/grammar/geoms/emphasis.test.ts +135 -0
- package/src/grammar/geoms/emphasis.ts +162 -0
- package/src/grammar/geoms/histogram.d.ts +75 -0
- package/src/grammar/geoms/histogram.test.ts +262 -0
- package/src/grammar/geoms/histogram.ts +740 -0
- package/src/grammar/geoms/index.d.ts +20 -0
- package/src/grammar/geoms/index.ts +77 -0
- package/src/grammar/geoms/interval.d.ts +31 -0
- package/src/grammar/geoms/interval.test.ts +154 -0
- package/src/grammar/geoms/interval.ts +342 -0
- package/src/grammar/geoms/line.d.ts +38 -0
- package/src/grammar/geoms/line.test.ts +247 -0
- package/src/grammar/geoms/line.ts +659 -0
- package/src/grammar/geoms/point.d.ts +57 -0
- package/src/grammar/geoms/point.test.ts +163 -0
- package/src/grammar/geoms/point.ts +545 -0
- package/src/grammar/geoms/polar.test.ts +216 -0
- package/src/grammar/geoms/ribbon.d.ts +21 -0
- package/src/grammar/geoms/ribbon.test.ts +170 -0
- package/src/grammar/geoms/ribbon.ts +87 -0
- package/src/grammar/geoms/ridgeline.d.ts +89 -0
- package/src/grammar/geoms/ridgeline.test.ts +247 -0
- package/src/grammar/geoms/ridgeline.ts +1164 -0
- package/src/grammar/geoms/rolling.d.ts +43 -0
- package/src/grammar/geoms/rolling.test.ts +217 -0
- package/src/grammar/geoms/rolling.ts +387 -0
- package/src/grammar/geoms/rug.d.ts +28 -0
- package/src/grammar/geoms/rug.test.ts +126 -0
- package/src/grammar/geoms/rug.ts +214 -0
- package/src/grammar/geoms/rule.d.ts +23 -0
- package/src/grammar/geoms/rule.test.ts +69 -0
- package/src/grammar/geoms/rule.ts +212 -0
- package/src/grammar/geoms/smooth.d.ts +54 -0
- package/src/grammar/geoms/smooth.test.ts +78 -0
- package/src/grammar/geoms/smooth.ts +337 -0
- package/src/grammar/geoms/text.d.ts +29 -0
- package/src/grammar/geoms/text.test.ts +64 -0
- package/src/grammar/geoms/text.ts +234 -0
- package/src/grammar/geoms/tile.d.ts +61 -0
- package/src/grammar/geoms/tile.test.ts +157 -0
- package/src/grammar/geoms/tile.ts +621 -0
- package/src/grammar/geoms/types.d.ts +319 -0
- package/src/grammar/geoms/types.ts +362 -0
- package/src/grammar/geoms/violin.d.ts +85 -0
- package/src/grammar/geoms/violin.test.ts +187 -0
- package/src/grammar/geoms/violin.ts +672 -0
- package/src/grammar/index.d.ts +22 -0
- package/src/grammar/index.ts +269 -0
- package/src/grammar/interactions/_disposable.d.ts +5 -0
- package/src/grammar/interactions/_disposable.ts +23 -0
- package/src/grammar/interactions/_z.d.ts +4 -0
- package/src/grammar/interactions/_z.ts +16 -0
- package/src/grammar/interactions/brush-selection.test.ts +262 -0
- package/src/grammar/interactions/brush.d.ts +63 -0
- package/src/grammar/interactions/brush.test.ts +483 -0
- package/src/grammar/interactions/brush.ts +452 -0
- package/src/grammar/interactions/crosshair.d.ts +19 -0
- package/src/grammar/interactions/crosshair.test.ts +127 -0
- package/src/grammar/interactions/crosshair.ts +76 -0
- package/src/grammar/interactions/hit-layer.d.ts +64 -0
- package/src/grammar/interactions/hit-layer.ts +246 -0
- package/src/grammar/interactions/legend.d.ts +19 -0
- package/src/grammar/interactions/legend.ts +101 -0
- package/src/grammar/interactions/menu.d.ts +93 -0
- package/src/grammar/interactions/menu.test.ts +373 -0
- package/src/grammar/interactions/menu.ts +342 -0
- package/src/grammar/interactions/selection.d.ts +25 -0
- package/src/grammar/interactions/selection.test.ts +289 -0
- package/src/grammar/interactions/selection.ts +142 -0
- package/src/grammar/interactions/series-readout.d.ts +91 -0
- package/src/grammar/interactions/series-readout.test.ts +668 -0
- package/src/grammar/interactions/series-readout.ts +422 -0
- package/src/grammar/interactions/series-snap.d.ts +70 -0
- package/src/grammar/interactions/series-snap.test.ts +214 -0
- package/src/grammar/interactions/series-snap.ts +218 -0
- package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
- package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
- package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
- package/src/grammar/interactions/tooltip.d.ts +140 -0
- package/src/grammar/interactions/tooltip.test.ts +406 -0
- package/src/grammar/interactions/tooltip.ts +622 -0
- package/src/grammar/interactions/transitions.d.ts +34 -0
- package/src/grammar/interactions/transitions.test.ts +172 -0
- package/src/grammar/interactions/transitions.ts +160 -0
- package/src/grammar/layout.d.ts +68 -0
- package/src/grammar/layout.ts +186 -0
- package/src/grammar/legend-merge.test.ts +332 -0
- package/src/grammar/mount.d.ts +78 -0
- package/src/grammar/mount.test.ts +479 -0
- package/src/grammar/mount.ts +2112 -0
- package/src/grammar/palettes.d.ts +54 -0
- package/src/grammar/palettes.test.ts +80 -0
- package/src/grammar/palettes.ts +167 -0
- package/src/grammar/pan-zoom.test.ts +398 -0
- package/src/grammar/phylo.d.ts +65 -0
- package/src/grammar/phylo.test.ts +59 -0
- package/src/grammar/phylo.ts +112 -0
- package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
- package/src/grammar/pipeline.d.ts +158 -0
- package/src/grammar/pipeline.test.ts +463 -0
- package/src/grammar/pipeline.ts +1233 -0
- package/src/grammar/profiling.d.ts +8 -0
- package/src/grammar/profiling.ts +24 -0
- package/src/grammar/scales.d.ts +188 -0
- package/src/grammar/scales.test.ts +181 -0
- package/src/grammar/scales.ts +800 -0
- package/src/grammar/svg.d.ts +3 -0
- package/src/grammar/svg.ts +39 -0
- package/src/grammar/theme.d.ts +261 -0
- package/src/grammar/theme.test.ts +105 -0
- package/src/grammar/theme.ts +490 -0
- package/src/heatmap/cpu.ts +109 -0
- package/src/heatmap/gpu.ts +565 -0
- package/src/heatmap/types.ts +177 -0
- package/src/heatmap.browser.test.ts +308 -0
- package/src/heatmap.test.ts +320 -0
- package/src/heatmap.ts +123 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/interactions.d.ts +48 -0
- package/src/interactions.test.ts +226 -0
- package/src/interactions.ts +394 -0
- package/src/layout/box.d.ts +48 -0
- package/src/layout/box.test.ts +107 -0
- package/src/layout/box.ts +143 -0
- package/src/legend.d.ts +115 -0
- package/src/legend.ts +422 -0
- package/src/marks/curve.d.ts +43 -0
- package/src/marks/curve.ts +244 -0
- package/src/marks/stack.d.ts +53 -0
- package/src/marks/stack.ts +184 -0
- package/src/marks.d.ts +273 -0
- package/src/marks.test.ts +541 -0
- package/src/marks.ts +1292 -0
- package/src/navigator.test.ts +174 -0
- package/src/navigator.ts +393 -0
- package/src/range-presets.d.ts +113 -0
- package/src/range-presets.test.ts +345 -0
- package/src/range-presets.ts +349 -0
- package/src/scales.d.ts +98 -0
- package/src/scales.test.ts +103 -0
- package/src/scales.ts +695 -0
- package/src/stats/index.d.ts +200 -0
- package/src/stats/index.test.ts +349 -0
- package/src/stats/index.ts +740 -0
- package/src/stats/regression.d.ts +38 -0
- package/src/stats/regression.test.ts +56 -0
- package/src/stats/regression.ts +396 -0
- package/src/stats/rolling-window.d.ts +55 -0
- package/src/stats/rolling-window.test.ts +237 -0
- package/src/stats/rolling-window.ts +256 -0
- package/src/test-setup.ts +19 -0
- package/src/viewport/axis-state.d.ts +72 -0
- package/src/viewport/axis-state.ts +476 -0
- package/src/viewport.d.ts +170 -0
- package/src/viewport.test.ts +363 -0
- package/src/viewport.ts +510 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export { confidenceBand, linearFit, loessFit, polyFit, type ConfidenceBandOptions, type ConfidenceBandPoint, type LoessOptions, type RegressionFit, } from "./regression.ts";
|
|
2
|
+
export { rollingWindow, type RollingAxis, type RollingPoint, type RollingStatistic, type RollingStatisticKind, type RollingWindow, type RollingWindowOptions, } from "./rolling-window.ts";
|
|
3
|
+
export type QuantileMethod = "type-7" | "type-1";
|
|
4
|
+
/**
|
|
5
|
+
* Quantile of an already-sorted ascending numeric array.
|
|
6
|
+
*
|
|
7
|
+
* - `"type-7"` (default): R's default — linear interpolation between order
|
|
8
|
+
* stats with `h = (n - 1) * p`. The quantile method most users expect.
|
|
9
|
+
* - `"type-1"`: inverse-of-empirical-CDF (step function). No interpolation.
|
|
10
|
+
*
|
|
11
|
+
* Returns `NaN` for empty input. Clamps `p` to `[0, 1]`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function quantile(sorted: readonly number[], p: number, method?: QuantileMethod): number;
|
|
14
|
+
export type WhiskerRule = number | "minmax";
|
|
15
|
+
export interface BoxStats {
|
|
16
|
+
/** Sample size (after filtering non-finite). */
|
|
17
|
+
n: number;
|
|
18
|
+
/** Smallest value in the sample. */
|
|
19
|
+
min: number;
|
|
20
|
+
/** Lower whisker endpoint (clamped to data range). */
|
|
21
|
+
lowerWhisker: number;
|
|
22
|
+
q1: number;
|
|
23
|
+
median: number;
|
|
24
|
+
q3: number;
|
|
25
|
+
/** Upper whisker endpoint (clamped to data range). */
|
|
26
|
+
upperWhisker: number;
|
|
27
|
+
/** Largest value in the sample. */
|
|
28
|
+
max: number;
|
|
29
|
+
/** Q3 − Q1. */
|
|
30
|
+
iqr: number;
|
|
31
|
+
/** Values strictly outside the whiskers. Order preserves input order. */
|
|
32
|
+
outliers: number[];
|
|
33
|
+
/** Mean of the sample. Useful as an annotation; not used for box geometry. */
|
|
34
|
+
mean: number;
|
|
35
|
+
}
|
|
36
|
+
export interface BoxStatsOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Whisker rule.
|
|
39
|
+
*
|
|
40
|
+
* - A number `k` (default `1.5`) — Tukey-style: whiskers extend to the most
|
|
41
|
+
* extreme value within `k * IQR` of Q1/Q3. Anything beyond is an outlier.
|
|
42
|
+
* - `"minmax"` — whiskers extend to the data min/max; no outliers.
|
|
43
|
+
*/
|
|
44
|
+
whisker?: WhiskerRule;
|
|
45
|
+
quantile?: QuantileMethod;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute Tukey-style box statistics. Filters non-finite inputs.
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` for empty input — caller decides how to render `n=0`.
|
|
51
|
+
* For `n=1`, every quartile equals the single value and `outliers` is empty.
|
|
52
|
+
*/
|
|
53
|
+
export declare function boxStats(values: readonly number[], options?: BoxStatsOptions): BoxStats | null;
|
|
54
|
+
export type KdeKernel = "gaussian" | "epanechnikov";
|
|
55
|
+
export type KdeBandwidth = number | "silverman" | "scott";
|
|
56
|
+
export interface KdeOptions {
|
|
57
|
+
/** Number of evaluation points. Default `64`, capped to `512`. */
|
|
58
|
+
gridSize?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Either a positive number (fixed bandwidth in data units) or a rule:
|
|
61
|
+
* - `"silverman"` (default) — `1.06 · scale · n^(-1/5)` with the robust
|
|
62
|
+
* scale `min(σ, IQR/1.34)`. Less sensitive to heavy tails / skew.
|
|
63
|
+
* - `"scott"` — `1.06 · σ · n^(-1/5)` using the plain standard deviation
|
|
64
|
+
* (no IQR robustification). Wider on skewed/heavy-tailed samples.
|
|
65
|
+
*/
|
|
66
|
+
bandwidth?: KdeBandwidth;
|
|
67
|
+
kernel?: KdeKernel;
|
|
68
|
+
/**
|
|
69
|
+
* Evaluate over `[min, max]` of the sample (`true`, default) or pad outward
|
|
70
|
+
* by ~3 bandwidths so density tails reach near-zero (`false`).
|
|
71
|
+
*/
|
|
72
|
+
trim?: boolean;
|
|
73
|
+
/** Override the evaluation domain. Bypasses `trim`. */
|
|
74
|
+
domain?: readonly [number, number];
|
|
75
|
+
}
|
|
76
|
+
export interface KdeResult {
|
|
77
|
+
/** Evaluation grid (length `gridSize`). */
|
|
78
|
+
x: number[];
|
|
79
|
+
/** Density at each grid point (length `gridSize`). */
|
|
80
|
+
y: number[];
|
|
81
|
+
/** Resolved bandwidth in data units. */
|
|
82
|
+
bandwidth: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Kernel density estimate.
|
|
86
|
+
*
|
|
87
|
+
* Returns `null` for empty input. For all-equal samples falls back to a tiny
|
|
88
|
+
* bandwidth so callers don't divide by zero — the result is a narrow spike.
|
|
89
|
+
*/
|
|
90
|
+
export declare function kde(values: readonly number[], options?: KdeOptions): KdeResult | null;
|
|
91
|
+
/**
|
|
92
|
+
* Group an iterable into a `Map` keyed by `key(item)`. Insertion order
|
|
93
|
+
* preserved per key; the map's key order is the order of first occurrence.
|
|
94
|
+
*/
|
|
95
|
+
export declare function groupBy<T, K>(items: Iterable<T>, key: (item: T, index: number) => K): Map<K, T[]>;
|
|
96
|
+
export type BinRule = "sturges" | "scott" | "fd" | "rice";
|
|
97
|
+
export type BinClosed = "left" | "right";
|
|
98
|
+
export interface BinBreaksOptions {
|
|
99
|
+
/** Explicit bin count. Ignored if `binwidth` or `breaks` is set. */
|
|
100
|
+
bins?: number;
|
|
101
|
+
/** Explicit bin width in data units. Beats `bins` and `rule`. */
|
|
102
|
+
binwidth?: number;
|
|
103
|
+
/** Fully explicit edge array — bypasses rules entirely. */
|
|
104
|
+
breaks?: readonly number[];
|
|
105
|
+
/**
|
|
106
|
+
* Rule used to pick a bin count when neither `bins`, `binwidth`, nor
|
|
107
|
+
* `breaks` is provided.
|
|
108
|
+
*
|
|
109
|
+
* - `"sturges"` (default) — `⌈log₂ n⌉ + 1`. R / ggplot default.
|
|
110
|
+
* - `"rice"` — `⌈2 · n^(1/3)⌉`. Slightly higher resolution.
|
|
111
|
+
* - `"scott"` — `3.49 σ n^(-1/3)` width. Good for roughly normal data.
|
|
112
|
+
* - `"fd"` — Freedman-Diaconis `2 · IQR · n^(-1/3)` width. Robust on skew.
|
|
113
|
+
*/
|
|
114
|
+
rule?: BinRule;
|
|
115
|
+
/** Clip edges to this. Default = `[min, max]` of data. */
|
|
116
|
+
domain?: readonly [number, number];
|
|
117
|
+
/** Round outer edges + step to nice numbers. Default `true`. */
|
|
118
|
+
nice?: boolean;
|
|
119
|
+
}
|
|
120
|
+
export interface BinResult {
|
|
121
|
+
/** Inclusive lower edge for `closed: "left"` (the default). */
|
|
122
|
+
x0: number;
|
|
123
|
+
/** Exclusive upper edge for `closed: "left"`, except the final bin. */
|
|
124
|
+
x1: number;
|
|
125
|
+
/** Number of input values that fell in this bin. */
|
|
126
|
+
count: number;
|
|
127
|
+
/** `count / (n · width)`. Sums to 1 across all bins (∑ density · width). */
|
|
128
|
+
density: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* How a histogram bin's `count` is converted into a y-axis value:
|
|
132
|
+
* - `"count"` / `"frequency"`: raw `count`
|
|
133
|
+
* - `"density"`: `count / (n · width)` — area sums to 1
|
|
134
|
+
* - `"proportion"`: `count / n` — values sum to 1
|
|
135
|
+
*/
|
|
136
|
+
export type HistogramMeasure = "count" | "density" | "proportion" | "frequency";
|
|
137
|
+
/**
|
|
138
|
+
* Convert a `BinResult` to its measured value under `measure`. `n` is the
|
|
139
|
+
* total sample count for the histogram (or per-group total for stacked /
|
|
140
|
+
* faceted cases) — used to normalise `density` and `proportion`.
|
|
141
|
+
*/
|
|
142
|
+
export declare function histogramMeasureValue(bin: BinResult, n: number, measure: HistogramMeasure): number;
|
|
143
|
+
export interface BinOptions extends BinBreaksOptions {
|
|
144
|
+
/**
|
|
145
|
+
* Edge-closure convention. With `"left"` (default) bins are `[x0, x1)` and
|
|
146
|
+
* the final bin includes the max as `[x_{k-1}, x_k]`. With `"right"` the
|
|
147
|
+
* first bin is `[x_0, x_1]` and the rest are `(x_i, x_{i+1}]`.
|
|
148
|
+
*/
|
|
149
|
+
closed?: BinClosed;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Compute bin edges for the given data. Returns an array of length
|
|
153
|
+
* `binCount + 1` describing the boundaries of each bin from left to right.
|
|
154
|
+
*
|
|
155
|
+
* Resolution order (highest precedence first): explicit `breaks` → `binwidth`
|
|
156
|
+
* → `bins` → `rule` (default `"sturges"`).
|
|
157
|
+
*
|
|
158
|
+
* Returns `[]` for empty / all-non-finite input.
|
|
159
|
+
*/
|
|
160
|
+
export declare function binBreaks(values: readonly number[], options?: BinBreaksOptions): number[];
|
|
161
|
+
/**
|
|
162
|
+
* Bin a numeric sample into a sorted array of `BinResult`. Filters non-finite.
|
|
163
|
+
*
|
|
164
|
+
* Bins are half-open `[x0, x1)` by default (`closed: "left"`); the last bin
|
|
165
|
+
* is closed on both ends so the maximum value is counted. With
|
|
166
|
+
* `closed: "right"` the first bin is closed on both ends and the rest are
|
|
167
|
+
* `(x0, x1]`.
|
|
168
|
+
*
|
|
169
|
+
* Values strictly outside the resolved domain are dropped.
|
|
170
|
+
*/
|
|
171
|
+
export declare function bin(values: readonly number[], options?: BinOptions): BinResult[];
|
|
172
|
+
/**
|
|
173
|
+
* Assign already-resolved values into known bin edges. Useful when the caller
|
|
174
|
+
* has computed `breaks` once across multiple groups (so per-group histograms
|
|
175
|
+
* align horizontally).
|
|
176
|
+
*/
|
|
177
|
+
export declare function binWithBreaks(values: readonly number[], breaks: readonly number[], closed?: BinClosed): BinResult[];
|
|
178
|
+
export interface BinByOptions extends BinOptions {
|
|
179
|
+
/**
|
|
180
|
+
* Use one shared edge array for every group (default `true`) so per-group
|
|
181
|
+
* histograms align horizontally for stack/dodge/overlay. With `false`, each
|
|
182
|
+
* group computes its own breaks against its own data.
|
|
183
|
+
*/
|
|
184
|
+
sharedBreaks?: boolean;
|
|
185
|
+
}
|
|
186
|
+
export interface BinByResult<K> {
|
|
187
|
+
breaks: number[];
|
|
188
|
+
groups: Map<K, BinResult[]>;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Bin a flat numeric array partitioned by key. Group iteration order matches
|
|
192
|
+
* first-occurrence order in the input.
|
|
193
|
+
*/
|
|
194
|
+
export declare function binBy<K>(values: readonly number[], keys: readonly K[], options?: BinByOptions): BinByResult<K>;
|
|
195
|
+
/**
|
|
196
|
+
* Deterministic uniform jitter offsets in `[-width/2, +width/2]`. Same
|
|
197
|
+
* `(seed, count)` pair always produces the same sequence — important so that
|
|
198
|
+
* jittered overlay points don't dance between renders.
|
|
199
|
+
*/
|
|
200
|
+
export declare function jitter(seed: number, count: number, width: number): number[];
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
bin,
|
|
5
|
+
binBreaks,
|
|
6
|
+
binBy,
|
|
7
|
+
binWithBreaks,
|
|
8
|
+
boxStats,
|
|
9
|
+
groupBy,
|
|
10
|
+
jitter,
|
|
11
|
+
kde,
|
|
12
|
+
quantile,
|
|
13
|
+
} from "./index.ts";
|
|
14
|
+
|
|
15
|
+
// ============ Fixtures ============
|
|
16
|
+
const fixture = {
|
|
17
|
+
// 11 evenly-spaced values 0..10 — quartiles fall on integers under type-7.
|
|
18
|
+
evenlySpaced: () => Array.from({ length: 11 }, (_, i) => i),
|
|
19
|
+
// Skewed sample with two outliers above 1.5 * IQR.
|
|
20
|
+
withOutliers: () => [1, 2, 2, 3, 3, 3, 4, 4, 5, 12, 18],
|
|
21
|
+
bimodal: () => [
|
|
22
|
+
...Array.from({ length: 30 }, (_, i) => 0.2 + i * 0.01),
|
|
23
|
+
...Array.from({ length: 30 }, (_, i) => 0.8 + i * 0.01),
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("quantile", () => {
|
|
28
|
+
test("type-7 matches R defaults on evenly-spaced data", () => {
|
|
29
|
+
const sorted = fixture.evenlySpaced();
|
|
30
|
+
expect(quantile(sorted, 0.0)).toBe(0);
|
|
31
|
+
expect(quantile(sorted, 0.25)).toBe(2.5);
|
|
32
|
+
expect(quantile(sorted, 0.5)).toBe(5);
|
|
33
|
+
expect(quantile(sorted, 0.75)).toBe(7.5);
|
|
34
|
+
expect(quantile(sorted, 1.0)).toBe(10);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("type-1 returns order-statistics without interpolation", () => {
|
|
38
|
+
const sorted = fixture.evenlySpaced();
|
|
39
|
+
expect(quantile(sorted, 0.5, "type-1")).toBe(5);
|
|
40
|
+
expect(quantile(sorted, 0.25, "type-1")).toBe(2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("clamps p to [0, 1]", () => {
|
|
44
|
+
const sorted = [10, 20, 30];
|
|
45
|
+
expect(quantile(sorted, -1)).toBe(10);
|
|
46
|
+
expect(quantile(sorted, 2)).toBe(30);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns NaN on empty input and the value itself for n=1", () => {
|
|
50
|
+
expect(Number.isNaN(quantile([], 0.5))).toBe(true);
|
|
51
|
+
expect(quantile([42], 0.5)).toBe(42);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("boxStats", () => {
|
|
56
|
+
test("computes Tukey stats and isolates outliers via 1.5 * IQR", () => {
|
|
57
|
+
const stats = boxStats(fixture.withOutliers())!;
|
|
58
|
+
expect(stats.n).toBe(11);
|
|
59
|
+
expect(stats.median).toBe(3);
|
|
60
|
+
expect(stats.q1).toBe(2.5);
|
|
61
|
+
expect(stats.q3).toBe(4.5);
|
|
62
|
+
expect(stats.iqr).toBe(2);
|
|
63
|
+
// Upper fence = 4.5 + 1.5*2 = 7.5 → 12 and 18 are outliers.
|
|
64
|
+
expect(stats.outliers).toEqual([12, 18]);
|
|
65
|
+
// Whiskers clamp to the most extreme inlier.
|
|
66
|
+
expect(stats.upperWhisker).toBe(5);
|
|
67
|
+
expect(stats.lowerWhisker).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("'minmax' rule extends whiskers to data extremes with no outliers", () => {
|
|
71
|
+
const stats = boxStats(fixture.withOutliers(), { whisker: "minmax" })!;
|
|
72
|
+
expect(stats.lowerWhisker).toBe(1);
|
|
73
|
+
expect(stats.upperWhisker).toBe(18);
|
|
74
|
+
expect(stats.outliers).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns null on empty after filtering non-finite", () => {
|
|
78
|
+
expect(boxStats([])).toBe(null);
|
|
79
|
+
expect(boxStats([Number.NaN, Number.POSITIVE_INFINITY])).toBe(null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("collapses to the single value for n=1", () => {
|
|
83
|
+
const stats = boxStats([7])!;
|
|
84
|
+
expect(stats.n).toBe(1);
|
|
85
|
+
expect(stats.q1).toBe(7);
|
|
86
|
+
expect(stats.median).toBe(7);
|
|
87
|
+
expect(stats.q3).toBe(7);
|
|
88
|
+
expect(stats.iqr).toBe(0);
|
|
89
|
+
expect(stats.outliers).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("handles all-equal samples with IQR 0 and no outliers", () => {
|
|
93
|
+
const stats = boxStats([5, 5, 5, 5])!;
|
|
94
|
+
expect(stats.iqr).toBe(0);
|
|
95
|
+
expect(stats.outliers).toEqual([]);
|
|
96
|
+
expect(stats.lowerWhisker).toBe(5);
|
|
97
|
+
expect(stats.upperWhisker).toBe(5);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("kde", () => {
|
|
102
|
+
test("returns gridSize evaluation points across the data range", () => {
|
|
103
|
+
const result = kde(fixture.bimodal(), { gridSize: 32 })!;
|
|
104
|
+
expect(result.x).toHaveLength(32);
|
|
105
|
+
expect(result.y).toHaveLength(32);
|
|
106
|
+
expect(result.bandwidth).toBeGreaterThan(0);
|
|
107
|
+
// Trim default → grid spans the data extent exactly.
|
|
108
|
+
const finite = fixture.bimodal();
|
|
109
|
+
const min = Math.min(...finite);
|
|
110
|
+
const max = Math.max(...finite);
|
|
111
|
+
expect(result.x[0]).toBeCloseTo(min, 8);
|
|
112
|
+
expect(result.x[31]).toBeCloseTo(max, 8);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("density integrates to roughly 1.0 (trapezoidal)", () => {
|
|
116
|
+
const result = kde(fixture.bimodal(), { gridSize: 256, trim: false })!;
|
|
117
|
+
let area = 0;
|
|
118
|
+
for (let i = 1; i < result.x.length; i++) {
|
|
119
|
+
const dx = result.x[i]! - result.x[i - 1]!;
|
|
120
|
+
area += 0.5 * (result.y[i]! + result.y[i - 1]!) * dx;
|
|
121
|
+
}
|
|
122
|
+
// Wide grid (3σ pad) catches most of the mass; ~1 with kernel & boundary slack.
|
|
123
|
+
expect(area).toBeGreaterThan(0.95);
|
|
124
|
+
expect(area).toBeLessThan(1.05);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("falls back to a tiny bandwidth on all-equal samples without throwing", () => {
|
|
128
|
+
const result = kde([3, 3, 3, 3], { gridSize: 8 })!;
|
|
129
|
+
expect(result.bandwidth).toBeGreaterThan(0);
|
|
130
|
+
expect(result.y.every((v) => Number.isFinite(v))).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("'scott' differs from 'silverman' on heavy-tailed data and matches 1.06 σ n^(-1/5)", () => {
|
|
134
|
+
// Skewed/heavy-tailed: IQR/1.34 ≈ 1.49 < σ, so the robust scale Silverman
|
|
135
|
+
// uses diverges from Scott's plain standard deviation.
|
|
136
|
+
const sample = fixture.withOutliers();
|
|
137
|
+
|
|
138
|
+
const scott = kde(sample, { bandwidth: "scott" })!;
|
|
139
|
+
const silverman = kde(sample, { bandwidth: "silverman" })!;
|
|
140
|
+
|
|
141
|
+
// The two rules must NOT coincide here.
|
|
142
|
+
expect(scott.bandwidth).not.toBeCloseTo(silverman.bandwidth, 8);
|
|
143
|
+
// Robust scale (min(σ, IQR/1.34)) ⇒ Silverman is the narrower of the two.
|
|
144
|
+
expect(silverman.bandwidth).toBeLessThan(scott.bandwidth);
|
|
145
|
+
|
|
146
|
+
// Hand-computed Scott: 1.06 · σ · n^(-1/5) with the sample (n-1) stddev.
|
|
147
|
+
const n = sample.length;
|
|
148
|
+
const mean = sample.reduce((s, v) => s + v, 0) / n;
|
|
149
|
+
const variance = sample.reduce((s, v) => s + (v - mean) ** 2, 0) / (n - 1);
|
|
150
|
+
const sd = Math.sqrt(variance);
|
|
151
|
+
const expected = 1.06 * sd * n ** (-1 / 5);
|
|
152
|
+
expect(scott.bandwidth).toBeCloseTo(expected, 10);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("'scott' and 'silverman' coincide when σ ≤ IQR/1.34", () => {
|
|
156
|
+
// Evenly-spaced 0..10: σ ≈ 3.32, IQR/1.34 ≈ 3.73, so min(σ, IQR/1.34) = σ
|
|
157
|
+
// and the robust scale equals the plain stddev — both rules agree.
|
|
158
|
+
const sample = fixture.evenlySpaced();
|
|
159
|
+
const scott = kde(sample, { bandwidth: "scott" })!;
|
|
160
|
+
const silverman = kde(sample, { bandwidth: "silverman" })!;
|
|
161
|
+
expect(scott.bandwidth).toBeCloseTo(silverman.bandwidth, 10);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("returns null on empty input", () => {
|
|
165
|
+
expect(kde([])).toBe(null);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("groupBy", () => {
|
|
170
|
+
test("preserves insertion order both in keys and within buckets", () => {
|
|
171
|
+
const items = [
|
|
172
|
+
{ k: "a", v: 1 },
|
|
173
|
+
{ k: "b", v: 2 },
|
|
174
|
+
{ k: "a", v: 3 },
|
|
175
|
+
{ k: "c", v: 4 },
|
|
176
|
+
{ k: "b", v: 5 },
|
|
177
|
+
];
|
|
178
|
+
const grouped = groupBy(items, (item) => item.k);
|
|
179
|
+
expect([...grouped.keys()]).toEqual(["a", "b", "c"]);
|
|
180
|
+
expect(grouped.get("a")!.map((i) => i.v)).toEqual([1, 3]);
|
|
181
|
+
expect(grouped.get("b")!.map((i) => i.v)).toEqual([2, 5]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("binBreaks", () => {
|
|
186
|
+
test("explicit `breaks` short-circuits all rules", () => {
|
|
187
|
+
const breaks = binBreaks([0, 1, 2, 3], { breaks: [-1, 0, 1, 2, 3, 4] });
|
|
188
|
+
expect(breaks).toEqual([-1, 0, 1, 2, 3, 4]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("`binwidth` produces nice-aligned edges by default", () => {
|
|
192
|
+
const breaks = binBreaks([0.3, 1.7, 4.2, 7.9], { binwidth: 1 });
|
|
193
|
+
expect(breaks[0]).toBe(0);
|
|
194
|
+
expect(breaks[breaks.length - 1]).toBe(8);
|
|
195
|
+
expect(breaks).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("`binwidth` with `nice: false` lands on raw min/max", () => {
|
|
199
|
+
const breaks = binBreaks([0.3, 7.9], { binwidth: 1, nice: false });
|
|
200
|
+
expect(breaks[0]).toBeCloseTo(0.3, 12);
|
|
201
|
+
expect(breaks.length).toBeGreaterThan(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("`bins` with `nice: false` divides domain evenly", () => {
|
|
205
|
+
const breaks = binBreaks([0, 10], { bins: 4, nice: false });
|
|
206
|
+
expect(breaks).toEqual([0, 2.5, 5, 7.5, 10]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("Sturges rule yields ⌈log2 n⌉ + 1 bins (default)", () => {
|
|
210
|
+
// n=8 → log2 8 = 3 → 4 bins
|
|
211
|
+
const breaks = binBreaks([1, 2, 3, 4, 5, 6, 7, 8], { nice: false });
|
|
212
|
+
expect(breaks.length - 1).toBe(4);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("returns `[]` for empty / non-finite input", () => {
|
|
216
|
+
expect(binBreaks([])).toEqual([]);
|
|
217
|
+
expect(binBreaks([Number.NaN, Number.POSITIVE_INFINITY])).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("`domain` clips edges and `nice: false` honors exact bounds", () => {
|
|
221
|
+
const breaks = binBreaks([0, 1, 2, 3, 4, 5], {
|
|
222
|
+
bins: 5,
|
|
223
|
+
domain: [0, 5],
|
|
224
|
+
nice: false,
|
|
225
|
+
});
|
|
226
|
+
expect(breaks).toEqual([0, 1, 2, 3, 4, 5]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("bin", () => {
|
|
231
|
+
test("`closed: 'left'` is half-open and includes the max in the last bin", () => {
|
|
232
|
+
const result = bin([0, 0.5, 1, 1.5, 2, 2.5, 3], {
|
|
233
|
+
breaks: [0, 1, 2, 3],
|
|
234
|
+
closed: "left",
|
|
235
|
+
});
|
|
236
|
+
expect(result).toHaveLength(3);
|
|
237
|
+
expect(result.map((b) => b.count)).toEqual([2, 2, 3]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("`closed: 'right'` includes the min in the first bin", () => {
|
|
241
|
+
const result = bin([0, 0.5, 1, 1.5, 2, 2.5, 3], {
|
|
242
|
+
breaks: [0, 1, 2, 3],
|
|
243
|
+
closed: "right",
|
|
244
|
+
});
|
|
245
|
+
// 0 → bin0 (special), 0.5 → bin0, 1.0 → bin0 ((0,1]),
|
|
246
|
+
// 1.5 → bin1, 2.0 → bin1, 2.5 → bin2, 3.0 → bin2.
|
|
247
|
+
expect(result.map((b) => b.count)).toEqual([3, 2, 2]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("density integrates to 1 across bins", () => {
|
|
251
|
+
const result = bin([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], { bins: 5, nice: false });
|
|
252
|
+
let area = 0;
|
|
253
|
+
for (const b of result) area += b.density * (b.x1 - b.x0);
|
|
254
|
+
expect(area).toBeCloseTo(1, 8);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("drops values strictly outside `domain`", () => {
|
|
258
|
+
const result = bin([-5, 0, 5, 10, 15], {
|
|
259
|
+
breaks: [0, 5, 10],
|
|
260
|
+
closed: "left",
|
|
261
|
+
});
|
|
262
|
+
// -5 dropped; 0 → bin0; 5 → bin1; 10 → bin1 (closed at top); 15 dropped.
|
|
263
|
+
expect(result.map((b) => b.count)).toEqual([1, 2]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("returns `[]` for empty input", () => {
|
|
267
|
+
expect(bin([])).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("collapses to a single padded bin for all-equal values", () => {
|
|
271
|
+
const result = bin([7, 7, 7]);
|
|
272
|
+
expect(result).toHaveLength(1);
|
|
273
|
+
expect(result[0]!.count).toBe(3);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("filters non-finite values silently", () => {
|
|
277
|
+
const result = bin([1, 2, Number.NaN, 3, Number.POSITIVE_INFINITY], {
|
|
278
|
+
breaks: [0, 1, 2, 3, 4],
|
|
279
|
+
});
|
|
280
|
+
expect(result.map((b) => b.count)).toEqual([0, 1, 1, 1]);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("binWithBreaks", () => {
|
|
285
|
+
test("counts against caller-provided edges without recomputing", () => {
|
|
286
|
+
const result = binWithBreaks([0.5, 1.5, 1.5, 2.5], [0, 1, 2, 3]);
|
|
287
|
+
expect(result.map((b) => b.count)).toEqual([1, 2, 1]);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("binBy", () => {
|
|
292
|
+
test("uses shared edges so groups align horizontally", () => {
|
|
293
|
+
const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
294
|
+
const keys = ["a", "a", "a", "a", "a", "b", "b", "b", "b", "b"] as const;
|
|
295
|
+
const { breaks, groups } = binBy(values, keys, { bins: 5, nice: false });
|
|
296
|
+
expect(breaks.length).toBe(6);
|
|
297
|
+
expect(groups.size).toBe(2);
|
|
298
|
+
const a = groups.get("a")!;
|
|
299
|
+
const b = groups.get("b")!;
|
|
300
|
+
expect(a).toHaveLength(5);
|
|
301
|
+
expect(b).toHaveLength(5);
|
|
302
|
+
// Each group's edges line up.
|
|
303
|
+
for (let i = 0; i < a.length; i++) {
|
|
304
|
+
expect(a[i]!.x0).toBe(b[i]!.x0);
|
|
305
|
+
expect(a[i]!.x1).toBe(b[i]!.x1);
|
|
306
|
+
}
|
|
307
|
+
// Per-group totals match the input partition.
|
|
308
|
+
const sum = (xs: { count: number }[]) => xs.reduce((s, x) => s + x.count, 0);
|
|
309
|
+
expect(sum(a)).toBe(5);
|
|
310
|
+
expect(sum(b)).toBe(5);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("preserves insertion order in groups", () => {
|
|
314
|
+
const { groups } = binBy([1, 2, 3], ["b", "a", "b"] as const, { bins: 2, nice: false });
|
|
315
|
+
expect([...groups.keys()]).toEqual(["b", "a"]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("`sharedBreaks: false` lets each group bin independently", () => {
|
|
319
|
+
const { groups } = binBy(
|
|
320
|
+
[0, 1, 2, 100, 200, 300],
|
|
321
|
+
["lo", "lo", "lo", "hi", "hi", "hi"] as const,
|
|
322
|
+
{ bins: 3, nice: false, sharedBreaks: false },
|
|
323
|
+
);
|
|
324
|
+
const lo = groups.get("lo")!;
|
|
325
|
+
const hi = groups.get("hi")!;
|
|
326
|
+
expect(lo[0]!.x0).toBe(0);
|
|
327
|
+
expect(lo[lo.length - 1]!.x1).toBe(2);
|
|
328
|
+
expect(hi[0]!.x0).toBe(100);
|
|
329
|
+
expect(hi[hi.length - 1]!.x1).toBe(300);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("jitter", () => {
|
|
334
|
+
test("is deterministic for the same seed", () => {
|
|
335
|
+
const a = jitter(42, 16, 1);
|
|
336
|
+
const b = jitter(42, 16, 1);
|
|
337
|
+
expect(a).toEqual(b);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("differs across seeds and stays within [-w/2, w/2]", () => {
|
|
341
|
+
const a = jitter(1, 32, 4);
|
|
342
|
+
const b = jitter(2, 32, 4);
|
|
343
|
+
expect(a).not.toEqual(b);
|
|
344
|
+
for (const v of a) {
|
|
345
|
+
expect(v).toBeGreaterThanOrEqual(-2);
|
|
346
|
+
expect(v).toBeLessThanOrEqual(2);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|