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,554 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createFrame } from "insomni";
|
|
3
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
4
|
+
|
|
5
|
+
import { resolveAes } from "../aes.ts";
|
|
6
|
+
import { buildPositionScale, type ScaleBundle } from "../scales.ts";
|
|
7
|
+
import { themeDefault } from "../theme.ts";
|
|
8
|
+
import { aggregate } from "./aggregate.ts";
|
|
9
|
+
import type { CompileContext } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
interface Row {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
accepted: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const RANGE_X: readonly [number, number] = [0, 100];
|
|
18
|
+
const RANGE_Y: readonly [number, number] = [200, 0];
|
|
19
|
+
|
|
20
|
+
function makeCtx<T extends Row>(rows: readonly T[]): CompileContext<T> {
|
|
21
|
+
const xAes = resolveAes<T, unknown>("x");
|
|
22
|
+
const yAes = resolveAes<T, unknown>("y");
|
|
23
|
+
// Pin the visible domain to a known span so bin maths is deterministic
|
|
24
|
+
// (otherwise auto-inferred extents can shift bin anchoring).
|
|
25
|
+
const xScale = buildPositionScale(xAes, rows, RANGE_X, { domain: [0, 100] });
|
|
26
|
+
const yScale = buildPositionScale(yAes, rows, RANGE_Y, { domain: [0, 100] });
|
|
27
|
+
const scales: ScaleBundle = { x: xScale, y: yScale };
|
|
28
|
+
return {
|
|
29
|
+
data: rows,
|
|
30
|
+
scales,
|
|
31
|
+
plot: createFrame({ x: 0, y: 0, width: 100, height: 200 }),
|
|
32
|
+
theme: themeDefault,
|
|
33
|
+
atlas: undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Captures the rects a bar mark emits via `layer.pushRect`, so baseline
|
|
38
|
+
// geometry can be asserted without a real GPU layer.
|
|
39
|
+
function fakeRectLayer(): {
|
|
40
|
+
rects: Array<{ x: number; y: number; width: number; height: number }>;
|
|
41
|
+
layer: { pushRect: (shape: { x: number; y: number; width: number; height: number }) => void };
|
|
42
|
+
} {
|
|
43
|
+
const rects: Array<{ x: number; y: number; width: number; height: number }> = [];
|
|
44
|
+
return {
|
|
45
|
+
rects,
|
|
46
|
+
layer: {
|
|
47
|
+
pushRect(shape) {
|
|
48
|
+
rects.push(shape);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Uniform synthetic dataset: 100 rows spread evenly across x ∈ [0, 100).
|
|
55
|
+
function uniform(count: number): Row[] {
|
|
56
|
+
const out: Row[] = Array.from({ length: count });
|
|
57
|
+
for (let i = 0; i < count; i++) {
|
|
58
|
+
out[i] = { x: (i / count) * 100, y: i, accepted: true };
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("aggregate geom", () => {
|
|
64
|
+
test("bin counts match analytic expectation for a uniform distribution", () => {
|
|
65
|
+
const rows = uniform(100);
|
|
66
|
+
const ctx = makeCtx(rows);
|
|
67
|
+
const geom = aggregate<Row>(
|
|
68
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
69
|
+
{ binSize: 10, summary: "count", dissolveAt: 0 },
|
|
70
|
+
);
|
|
71
|
+
const builders = geom.compile(ctx);
|
|
72
|
+
expect(builders.length).toBe(1);
|
|
73
|
+
// 100 points across 10 bins of width 10 → 10 non-empty bins.
|
|
74
|
+
expect(builders[0]!.length).toBe(10);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("explicit auto bin size targets ~7 px per bin", () => {
|
|
78
|
+
// 100 px range, target 7 px per bin → ~14 bins of ~7.14 domain units.
|
|
79
|
+
const rows = uniform(200);
|
|
80
|
+
const ctx = makeCtx(rows);
|
|
81
|
+
const geom = aggregate<Row>(
|
|
82
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
83
|
+
{ binSize: "auto", summary: "count", dissolveAt: 0 },
|
|
84
|
+
);
|
|
85
|
+
const builders = geom.compile(ctx);
|
|
86
|
+
// 100 / 7.14 = 14.0 — accept 13 / 14 / 15 to absorb rounding.
|
|
87
|
+
expect(builders[0]!.length).toBeGreaterThanOrEqual(13);
|
|
88
|
+
expect(builders[0]!.length).toBeLessThanOrEqual(15);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("dissolve threshold flips to raw geom when avg/bin drops below cutoff", () => {
|
|
92
|
+
// 4 rows across 10 bins (width 10) → 4 non-empty bins, avg/bin = 0.4
|
|
93
|
+
// which is below default dissolveAt of 1.2 → render raw.
|
|
94
|
+
const sparse: Row[] = [
|
|
95
|
+
{ x: 5, y: 5, accepted: true },
|
|
96
|
+
{ x: 35, y: 35, accepted: true },
|
|
97
|
+
{ x: 60, y: 60, accepted: true },
|
|
98
|
+
{ x: 95, y: 95, accepted: true },
|
|
99
|
+
];
|
|
100
|
+
const ctx = makeCtx(sparse);
|
|
101
|
+
const geom = aggregate<Row>({ x: (d) => d.x, y: (d) => d.y }, { binSize: 10, summary: "mean" });
|
|
102
|
+
const builders = geom.compile(ctx);
|
|
103
|
+
// Raw path → one builder, length === raw row count (4).
|
|
104
|
+
expect(builders.length).toBe(1);
|
|
105
|
+
expect(builders[0]!.length).toBe(4);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("low dissolveAt holds the aggregate path even on sparse data", () => {
|
|
109
|
+
const sparse: Row[] = [
|
|
110
|
+
{ x: 5, y: 5, accepted: true },
|
|
111
|
+
{ x: 35, y: 35, accepted: true },
|
|
112
|
+
{ x: 60, y: 60, accepted: true },
|
|
113
|
+
{ x: 95, y: 95, accepted: true },
|
|
114
|
+
];
|
|
115
|
+
const ctx = makeCtx(sparse);
|
|
116
|
+
const geom = aggregate<Row>(
|
|
117
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
118
|
+
{ binSize: 10, summary: "mean", dissolveAt: 0 },
|
|
119
|
+
);
|
|
120
|
+
const builders = geom.compile(ctx);
|
|
121
|
+
// 10 bins covering [0, 100], 4 with data → 4 bin points emitted.
|
|
122
|
+
expect(builders[0]!.length).toBe(4);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("filter removes rows from both bin contents and bin emission", () => {
|
|
126
|
+
// 100 rows but only every 5th is accepted → 20 rows passing the filter.
|
|
127
|
+
const rows = uniform(100).map((r, i) => ({ ...r, accepted: i % 5 === 0 }));
|
|
128
|
+
const ctx = makeCtx(rows);
|
|
129
|
+
const geom = aggregate<Row>(
|
|
130
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
131
|
+
{
|
|
132
|
+
binSize: 25, // 4 bins across [0, 100]
|
|
133
|
+
summary: "count",
|
|
134
|
+
filter: (d) => d.accepted,
|
|
135
|
+
dissolveAt: 0,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
const builders = geom.compile(ctx);
|
|
139
|
+
// 20 accepted points across 4 bins → 4 non-empty bins emitted; the count
|
|
140
|
+
// summary should yield 5 per bin.
|
|
141
|
+
expect(builders[0]!.length).toBe(4);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("supports point inner geom (default)", () => {
|
|
145
|
+
const rows = uniform(40);
|
|
146
|
+
const ctx = makeCtx(rows);
|
|
147
|
+
const geom = aggregate<Row>(
|
|
148
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
149
|
+
{ binSize: 10, summary: "mean", dissolveAt: 0 },
|
|
150
|
+
);
|
|
151
|
+
expect(geom.kind).toBe("point");
|
|
152
|
+
expect(geom.compile(ctx).length).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("supports line inner geom", () => {
|
|
156
|
+
const rows = uniform(40);
|
|
157
|
+
const ctx = makeCtx(rows);
|
|
158
|
+
const geom = aggregate<Row>(
|
|
159
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
160
|
+
{ binSize: 10, summary: "mean", geom: "line", dissolveAt: 0 },
|
|
161
|
+
);
|
|
162
|
+
expect(geom.kind).toBe("line");
|
|
163
|
+
expect(geom.compile(ctx).length).toBeGreaterThan(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("supports bar inner geom", () => {
|
|
167
|
+
const rows = uniform(40);
|
|
168
|
+
const ctx = makeCtx(rows);
|
|
169
|
+
const geom = aggregate<Row>(
|
|
170
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
171
|
+
{ binSize: 10, summary: "count", geom: "bar", dissolveAt: 0 },
|
|
172
|
+
);
|
|
173
|
+
expect(geom.kind).toBe("bar");
|
|
174
|
+
expect(geom.compile(ctx).length).toBeGreaterThan(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("bar baseline uses the y range floor on a log axis (not yScale(0))", () => {
|
|
178
|
+
// Log y axis: domain [10, 1000], range [200, 0]. `yScale(0)` is garbage
|
|
179
|
+
// here — logTransform(0)=0 normalizes to t=-0.5 → interpolate(200,0,-0.5)
|
|
180
|
+
// = 300px, i.e. *off the bottom* of the 200px plot. The bars must instead
|
|
181
|
+
// sit on the range floor (range[0] = 200, paired with domain min 10, the
|
|
182
|
+
// endpoint nearest zero).
|
|
183
|
+
//
|
|
184
|
+
// Rows all carry y∈[10,1000] so the binned means land inside the domain.
|
|
185
|
+
interface LRow {
|
|
186
|
+
x: number;
|
|
187
|
+
y: number;
|
|
188
|
+
}
|
|
189
|
+
const rows: LRow[] = Array.from({ length: 40 }, (_, i) => ({
|
|
190
|
+
x: (i / 40) * 100,
|
|
191
|
+
y: 10 + (i % 10) * 99, // 10..901, comfortably inside [10, 1000]
|
|
192
|
+
}));
|
|
193
|
+
const xAes = resolveAes<LRow, unknown>("x");
|
|
194
|
+
const yAes = resolveAes<LRow, unknown>("y");
|
|
195
|
+
const xScale = buildPositionScale(xAes, rows, RANGE_X, { domain: [0, 100] });
|
|
196
|
+
const yScale = buildPositionScale(yAes, rows, RANGE_Y, {
|
|
197
|
+
type: "log",
|
|
198
|
+
domain: [10, 1000],
|
|
199
|
+
});
|
|
200
|
+
const ctx: CompileContext<LRow> = {
|
|
201
|
+
data: rows,
|
|
202
|
+
scales: { x: xScale, y: yScale },
|
|
203
|
+
plot: createFrame({ x: 0, y: 0, width: 100, height: 200 }),
|
|
204
|
+
theme: themeDefault,
|
|
205
|
+
atlas: undefined,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Sanity: on a log axis the bug's baseline (yScale(0)) is non-finite —
|
|
209
|
+
// log(0) = -∞ — so feeding it to bar geometry yields NaN rects. The
|
|
210
|
+
// assertions below confirm the fix uses the range floor instead.
|
|
211
|
+
expect(Number.isFinite(yScale.fn(0))).toBe(false);
|
|
212
|
+
|
|
213
|
+
const geom = aggregate<LRow>(
|
|
214
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
215
|
+
{ binSize: 10, summary: "mean", geom: "bar", dissolveAt: 0 },
|
|
216
|
+
);
|
|
217
|
+
const builders = geom.compile(ctx);
|
|
218
|
+
expect(builders.length).toBe(1);
|
|
219
|
+
|
|
220
|
+
const { rects, layer } = fakeRectLayer();
|
|
221
|
+
for (const b of builders) b.addTo(layer as never);
|
|
222
|
+
expect(rects.length).toBeGreaterThan(0);
|
|
223
|
+
|
|
224
|
+
// Every bar's bottom edge (y + height) must rest on the range floor (200),
|
|
225
|
+
// NOT the clamped yScale(0)=300. Tops vary with each bin's mean.
|
|
226
|
+
for (const r of rects) {
|
|
227
|
+
expect(r.y + r.height).toBeCloseTo(200, 3);
|
|
228
|
+
// And the top must be a sane in-plot pixel (0..200), never beyond.
|
|
229
|
+
expect(r.y).toBeGreaterThanOrEqual(0);
|
|
230
|
+
expect(r.y).toBeLessThanOrEqual(200);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("bar baseline stays at yScale(0) when 0 is within the domain", () => {
|
|
235
|
+
// Control: a linear y axis spanning zero must keep the existing behavior —
|
|
236
|
+
// the baseline is yScale(0), the bars' floor.
|
|
237
|
+
const rows = uniform(40);
|
|
238
|
+
const ctx = makeCtx(rows); // y domain [0, 100], range [200, 0] → yScale(0)=200
|
|
239
|
+
expect(ctx.scales.y.fn(0)).toBeCloseTo(200, 3);
|
|
240
|
+
|
|
241
|
+
const geom = aggregate<Row>(
|
|
242
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
243
|
+
{ binSize: 10, summary: "mean", geom: "bar", dissolveAt: 0 },
|
|
244
|
+
);
|
|
245
|
+
const builders = geom.compile(ctx);
|
|
246
|
+
const { rects, layer } = fakeRectLayer();
|
|
247
|
+
for (const b of builders) b.addTo(layer as never);
|
|
248
|
+
expect(rects.length).toBeGreaterThan(0);
|
|
249
|
+
for (const r of rects) {
|
|
250
|
+
expect(r.y + r.height).toBeCloseTo(200, 3);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("aggregates Date-valued x with binSize in ms", () => {
|
|
255
|
+
interface TRow {
|
|
256
|
+
ts: Date;
|
|
257
|
+
v: number;
|
|
258
|
+
}
|
|
259
|
+
// 10 days, one row per day at midnight UTC. Pick a base aligned to a
|
|
260
|
+
// multiple of `binSize` so the floor-to-bin anchor lands on day 0.
|
|
261
|
+
const day = 86_400_000;
|
|
262
|
+
const binMs = 2 * day;
|
|
263
|
+
const base = Math.floor(Date.UTC(2024, 0, 1) / binMs) * binMs;
|
|
264
|
+
const rows: TRow[] = Array.from(
|
|
265
|
+
{ length: 10 },
|
|
266
|
+
(_, i) => ({ ts: new Date(base + i * day), v: i }) as TRow,
|
|
267
|
+
);
|
|
268
|
+
const xAes = resolveAes<TRow, unknown>("ts");
|
|
269
|
+
const yAes = resolveAes<TRow, unknown>("v");
|
|
270
|
+
const xScale = buildPositionScale(xAes, rows, RANGE_X, {
|
|
271
|
+
type: "time",
|
|
272
|
+
domain: [new Date(base), new Date(base + 10 * day)],
|
|
273
|
+
});
|
|
274
|
+
const yScale = buildPositionScale(yAes, rows, RANGE_Y, { domain: [0, 10] });
|
|
275
|
+
const ctx: CompileContext<TRow> = {
|
|
276
|
+
data: rows,
|
|
277
|
+
scales: { x: xScale, y: yScale },
|
|
278
|
+
plot: createFrame({ x: 0, y: 0, width: 100, height: 200 }),
|
|
279
|
+
theme: themeDefault,
|
|
280
|
+
atlas: undefined,
|
|
281
|
+
};
|
|
282
|
+
const geom = aggregate<TRow>(
|
|
283
|
+
{ x: (d) => d.ts, y: (d) => d.v },
|
|
284
|
+
{ binSize: binMs, summary: "mean", dissolveAt: 0 },
|
|
285
|
+
);
|
|
286
|
+
const builders = geom.compile(ctx);
|
|
287
|
+
// 10 days / 2-day bins → 5 non-empty bins.
|
|
288
|
+
expect(builders[0]!.length).toBe(5);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("custom function summary fires with full bin view", () => {
|
|
292
|
+
const rows = uniform(40);
|
|
293
|
+
const ctx = makeCtx(rows);
|
|
294
|
+
let sawNonEmpty = false;
|
|
295
|
+
const geom = aggregate<Row>(
|
|
296
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
297
|
+
{
|
|
298
|
+
binSize: 25,
|
|
299
|
+
summary: (bin) => {
|
|
300
|
+
if (bin.count > 0 && bin.items.length === bin.count) sawNonEmpty = true;
|
|
301
|
+
return bin.values.reduce((s, v) => s + v, 0);
|
|
302
|
+
},
|
|
303
|
+
dissolveAt: 0,
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
const builders = geom.compile(ctx);
|
|
307
|
+
expect(builders[0]!.length).toBe(4);
|
|
308
|
+
expect(sawNonEmpty).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("empty data returns no builders", () => {
|
|
312
|
+
const ctx = makeCtx([] as Row[]);
|
|
313
|
+
const geom = aggregate<Row>({ x: (d) => d.x, y: (d) => d.y }, { binSize: 10 });
|
|
314
|
+
expect(geom.compile(ctx).length).toBe(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("filter dropping every row yields no builders", () => {
|
|
318
|
+
const rows = uniform(20);
|
|
319
|
+
const ctx = makeCtx(rows);
|
|
320
|
+
const geom = aggregate<Row>(
|
|
321
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
322
|
+
{ binSize: 10, filter: () => false },
|
|
323
|
+
);
|
|
324
|
+
expect(geom.compile(ctx).length).toBe(0);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("binBy: 'y' flips bin axis and aggregates x", () => {
|
|
328
|
+
const rows = uniform(40);
|
|
329
|
+
const ctx = makeCtx(rows);
|
|
330
|
+
const geom = aggregate<Row>(
|
|
331
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
332
|
+
{ binSize: 10, binBy: "y", summary: "mean", dissolveAt: 0 },
|
|
333
|
+
);
|
|
334
|
+
const builders = geom.compile(ctx);
|
|
335
|
+
expect(builders[0]!.length).toBeGreaterThan(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("end-to-end via plot().toSVG() does not throw", async () => {
|
|
339
|
+
const { plot } = await import("../chart.ts");
|
|
340
|
+
const rows = uniform(80);
|
|
341
|
+
expect(() =>
|
|
342
|
+
plot({ data: rows, width: 320, height: 200 })
|
|
343
|
+
.layer(
|
|
344
|
+
aggregate<Row>(
|
|
345
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
346
|
+
{ binSize: 10, summary: "mean", dissolveAt: 0 },
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
.toSVG(),
|
|
350
|
+
).not.toThrow();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Bundle summaries — mean+ci / median+iqr / quantile triple
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
test("mean+ci produces interval marks with z*stderr half-width", () => {
|
|
358
|
+
// Build bins where each contains constant-spaced values so the mean/CI
|
|
359
|
+
// are analytically tractable. Two rows per bin at ±d from the mean ⇒
|
|
360
|
+
// sample stddev = d*sqrt(2/(n-1)) = d, stderr = d/sqrt(2). For 95% CI
|
|
361
|
+
// (z≈1.96) the half-width is z*d/sqrt(2) ≈ 1.3859 * d.
|
|
362
|
+
//
|
|
363
|
+
// We use d = 10 across two bins (x∈[0,10) and x∈[10,20)) and check that
|
|
364
|
+
// the interval geom emits the expected number of line builders (1
|
|
365
|
+
// builder containing 1 spine + 2 caps per bin = 6 line segments).
|
|
366
|
+
const rows: Row[] = [
|
|
367
|
+
{ x: 1, y: 90, accepted: true },
|
|
368
|
+
{ x: 2, y: 110, accepted: true },
|
|
369
|
+
{ x: 11, y: 40, accepted: true },
|
|
370
|
+
{ x: 12, y: 60, accepted: true },
|
|
371
|
+
];
|
|
372
|
+
const ctx = makeCtx(rows);
|
|
373
|
+
const geom = aggregate<Row>(
|
|
374
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
375
|
+
{ binSize: 10, summary: "mean+ci", geom: "interval", ciLevel: 0.95 },
|
|
376
|
+
);
|
|
377
|
+
expect(geom.kind).toBe("interval");
|
|
378
|
+
const builders = geom.compile(ctx);
|
|
379
|
+
// interval emits one builder; length = spines + caps = 1 + 2 per bin.
|
|
380
|
+
expect(builders.length).toBe(1);
|
|
381
|
+
expect(builders[0]!.length).toBe(6);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("mean+ci collapses to a point when count<2 per bin", () => {
|
|
385
|
+
// One row per bin → stderr undefined; impl clamps to lo=hi=center.
|
|
386
|
+
const rows: Row[] = [
|
|
387
|
+
{ x: 5, y: 50, accepted: true },
|
|
388
|
+
{ x: 15, y: 60, accepted: true },
|
|
389
|
+
];
|
|
390
|
+
const ctx = makeCtx(rows);
|
|
391
|
+
const geom = aggregate<Row>(
|
|
392
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
393
|
+
{ binSize: 10, summary: "mean+ci", geom: "interval" },
|
|
394
|
+
);
|
|
395
|
+
const builders = geom.compile(ctx);
|
|
396
|
+
// Two bins × (spine + 2 caps) = 6.
|
|
397
|
+
expect(builders[0]!.length).toBe(6);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("median+iqr emits an interval with [Q1, Q3] bounds", () => {
|
|
401
|
+
// Five rows in a single bin at y = 10, 20, 30, 40, 50 →
|
|
402
|
+
// median = 30, Q1 = 20, Q3 = 40 (R type-7).
|
|
403
|
+
const rows: Row[] = [
|
|
404
|
+
{ x: 1, y: 10, accepted: true },
|
|
405
|
+
{ x: 2, y: 20, accepted: true },
|
|
406
|
+
{ x: 3, y: 30, accepted: true },
|
|
407
|
+
{ x: 4, y: 40, accepted: true },
|
|
408
|
+
{ x: 5, y: 50, accepted: true },
|
|
409
|
+
];
|
|
410
|
+
const ctx = makeCtx(rows);
|
|
411
|
+
const geom = aggregate<Row>(
|
|
412
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
413
|
+
{ binSize: 10, summary: "median+iqr", geom: "interval" },
|
|
414
|
+
);
|
|
415
|
+
const builders = geom.compile(ctx);
|
|
416
|
+
// One bin → 1 spine + 2 caps = 3 segments.
|
|
417
|
+
expect(builders[0]!.length).toBe(3);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("median+iqr without caps drops the cap segments", () => {
|
|
421
|
+
const rows: Row[] = [
|
|
422
|
+
{ x: 1, y: 10, accepted: true },
|
|
423
|
+
{ x: 2, y: 20, accepted: true },
|
|
424
|
+
{ x: 3, y: 30, accepted: true },
|
|
425
|
+
];
|
|
426
|
+
const ctx = makeCtx(rows);
|
|
427
|
+
const geom = aggregate<Row>(
|
|
428
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
429
|
+
{ binSize: 10, summary: "median+iqr", geom: "interval", caps: false },
|
|
430
|
+
);
|
|
431
|
+
const builders = geom.compile(ctx);
|
|
432
|
+
// 1 spine, 0 caps.
|
|
433
|
+
expect(builders[0]!.length).toBe(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("quantile triple summary picks the requested order statistics", () => {
|
|
437
|
+
// 10 rows in a single bin, y = 1..10. For quantiles [0.1, 0.5, 0.9]
|
|
438
|
+
// with R type-7 interpolation: q0.1 = 1.9, q0.5 = 5.5, q0.9 = 9.1.
|
|
439
|
+
const rows: Row[] = Array.from({ length: 10 }, (_, i) => ({
|
|
440
|
+
x: i % 10, // all in [0, 10) → single bin of size 10
|
|
441
|
+
y: i + 1,
|
|
442
|
+
accepted: true,
|
|
443
|
+
}));
|
|
444
|
+
const ctx = makeCtx(rows);
|
|
445
|
+
const geom = aggregate<Row>(
|
|
446
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
447
|
+
{
|
|
448
|
+
binSize: 10,
|
|
449
|
+
summary: { quantiles: [0.1, 0.5, 0.9] },
|
|
450
|
+
geom: "interval",
|
|
451
|
+
caps: false,
|
|
452
|
+
},
|
|
453
|
+
);
|
|
454
|
+
const builders = geom.compile(ctx);
|
|
455
|
+
// 1 spine, no caps.
|
|
456
|
+
expect(builders[0]!.length).toBe(1);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("ribbon inner emits one area builder with bin-center vertices", () => {
|
|
460
|
+
// Three contiguous bins. Ribbon stitches them into a single area mark
|
|
461
|
+
// — areaMark returns one builder per (sub-)segment, and with no NaN
|
|
462
|
+
// discontinuities the entire band is one piece.
|
|
463
|
+
const rows: Row[] = [
|
|
464
|
+
{ x: 1, y: 10, accepted: true },
|
|
465
|
+
{ x: 2, y: 30, accepted: true },
|
|
466
|
+
{ x: 11, y: 20, accepted: true },
|
|
467
|
+
{ x: 12, y: 40, accepted: true },
|
|
468
|
+
{ x: 21, y: 30, accepted: true },
|
|
469
|
+
{ x: 22, y: 50, accepted: true },
|
|
470
|
+
];
|
|
471
|
+
const ctx = makeCtx(rows);
|
|
472
|
+
const geom = aggregate<Row>(
|
|
473
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
474
|
+
{ binSize: 10, summary: "mean+ci", geom: "ribbon" },
|
|
475
|
+
);
|
|
476
|
+
expect(geom.kind).toBe("area");
|
|
477
|
+
const builders = geom.compile(ctx);
|
|
478
|
+
expect(builders.length).toBeGreaterThan(0);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("throws when bundle summary is paired with a scalar inner geom", () => {
|
|
482
|
+
expect(() =>
|
|
483
|
+
aggregate<Row>({ x: (d) => d.x, y: (d) => d.y }, { summary: "mean+ci", geom: "point" }),
|
|
484
|
+
).toThrowError(/bundle summary requires geom/);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("throws when a bundle inner geom gets a scalar summary kind", () => {
|
|
488
|
+
expect(() =>
|
|
489
|
+
aggregate<Row>({ x: (d) => d.x, y: (d) => d.y }, { summary: "mean", geom: "interval" }),
|
|
490
|
+
).toThrowError(/requires a bundle summary/);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("throws on ribbon + binBy: 'y' (horizontal ribbons not supported)", () => {
|
|
494
|
+
expect(() =>
|
|
495
|
+
aggregate<Row>(
|
|
496
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
497
|
+
{ summary: "mean+ci", geom: "ribbon", binBy: "y" },
|
|
498
|
+
),
|
|
499
|
+
).toThrowError(/only supports binBy: "x"/);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("custom function summary returning {center,lo,hi} feeds interval", () => {
|
|
503
|
+
// Function form is disambiguated by inner-geom kind, not by typing.
|
|
504
|
+
const rows: Row[] = [
|
|
505
|
+
{ x: 1, y: 5, accepted: true },
|
|
506
|
+
{ x: 2, y: 15, accepted: true },
|
|
507
|
+
{ x: 11, y: 25, accepted: true },
|
|
508
|
+
{ x: 12, y: 35, accepted: true },
|
|
509
|
+
];
|
|
510
|
+
const ctx = makeCtx(rows);
|
|
511
|
+
const geom = aggregate<Row>(
|
|
512
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
513
|
+
{
|
|
514
|
+
binSize: 10,
|
|
515
|
+
summary: (bin) => {
|
|
516
|
+
// emit min/max as the bundle range; mean as center.
|
|
517
|
+
let mn = Number.POSITIVE_INFINITY;
|
|
518
|
+
let mx = Number.NEGATIVE_INFINITY;
|
|
519
|
+
let s = 0;
|
|
520
|
+
for (const v of bin.values) {
|
|
521
|
+
if (v < mn) mn = v;
|
|
522
|
+
if (v > mx) mx = v;
|
|
523
|
+
s += v;
|
|
524
|
+
}
|
|
525
|
+
return { center: s / bin.count, lo: mn, hi: mx };
|
|
526
|
+
},
|
|
527
|
+
geom: "interval",
|
|
528
|
+
caps: false,
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
const builders = geom.compile(ctx);
|
|
532
|
+
// Two bins × 1 spine each (caps off) = 2 segments.
|
|
533
|
+
expect(builders[0]!.length).toBe(2);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("end-to-end via plot().toSVG() does not throw for interval bundle", async () => {
|
|
537
|
+
const { plot } = await import("../chart.ts");
|
|
538
|
+
const rows: Row[] = Array.from({ length: 60 }, (_, i) => ({
|
|
539
|
+
x: i,
|
|
540
|
+
y: 50 + Math.sin(i / 5) * 20,
|
|
541
|
+
accepted: true,
|
|
542
|
+
}));
|
|
543
|
+
expect(() =>
|
|
544
|
+
plot({ data: rows, width: 320, height: 200 })
|
|
545
|
+
.layer(
|
|
546
|
+
aggregate<Row>(
|
|
547
|
+
{ x: (d) => d.x, y: (d) => d.y },
|
|
548
|
+
{ binSize: 6, summary: "median+iqr", geom: "interval" },
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
.toSVG(),
|
|
552
|
+
).not.toThrow();
|
|
553
|
+
});
|
|
554
|
+
});
|