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,355 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
3
|
+
import { createFrame } from "insomni";
|
|
4
|
+
import {
|
|
5
|
+
coordCartesian,
|
|
6
|
+
coordPolar,
|
|
7
|
+
coordRadial,
|
|
8
|
+
type CoordViewportHandle,
|
|
9
|
+
type Coord,
|
|
10
|
+
} from "./coord.ts";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const F = createFrame({ x: 0, y: 0, width: 200, height: 200 });
|
|
17
|
+
|
|
18
|
+
/** Minimal viewport stub that captures pan/zoom calls for assertions. */
|
|
19
|
+
function makeViewportStub() {
|
|
20
|
+
const panCalls: { dx: number; dy: number }[] = [];
|
|
21
|
+
const zoomCalls: { x: number; y: number; factor: number | { x?: number; y?: number } }[] = [];
|
|
22
|
+
const handle: CoordViewportHandle = {
|
|
23
|
+
panBy(dx, dy) {
|
|
24
|
+
panCalls.push({ dx, dy });
|
|
25
|
+
},
|
|
26
|
+
zoomAt(x, y, factor) {
|
|
27
|
+
zoomCalls.push({ x, y, factor });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return { handle, panCalls, zoomCalls };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Polar coords expose their internal angular state via `__polar__` for testing.
|
|
34
|
+
function polarState(c: Coord) {
|
|
35
|
+
// Cast: every coord returned by `coordPolar` carries the __polar__ readout.
|
|
36
|
+
return (
|
|
37
|
+
c as Coord & {
|
|
38
|
+
__polar__: {
|
|
39
|
+
startAngle: () => number;
|
|
40
|
+
endAngle: () => number;
|
|
41
|
+
innerRadius: () => number;
|
|
42
|
+
outerRadius: () => number;
|
|
43
|
+
angleChannel: "x" | "y";
|
|
44
|
+
direction: 1 | -1;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
).__polar__;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// coordPolar — project / unproject
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
describe("coordPolar project ↔ unproject", () => {
|
|
55
|
+
test("default polar (angleChannel y, full circle) round-trips", () => {
|
|
56
|
+
// angleChannel default = 'y'. y=0 (bottom) → θ=startAngle=-π/2; y=plotH
|
|
57
|
+
// (top) → θ ≈ startAngle - 2π (full revolution). x is the radial channel:
|
|
58
|
+
// x=0 → r=0; x=plotW → r=outerR.
|
|
59
|
+
const polar = coordPolar();
|
|
60
|
+
polar.bindFrame(F);
|
|
61
|
+
// Pick a few mid-arc points; avoid r=0 (the centre is degenerate under unproject).
|
|
62
|
+
const samples = [
|
|
63
|
+
{ x: 50, y: 50 },
|
|
64
|
+
{ x: 100, y: 100 },
|
|
65
|
+
{ x: 80, y: 20 },
|
|
66
|
+
{ x: 150, y: 175 },
|
|
67
|
+
];
|
|
68
|
+
for (const p of samples) {
|
|
69
|
+
const projected = polar.project(p);
|
|
70
|
+
const back = polar.unproject(projected);
|
|
71
|
+
expect(back).not.toBeNull();
|
|
72
|
+
expect(back!.x).toBeCloseTo(p.x, 4);
|
|
73
|
+
expect(back!.y).toBeCloseTo(p.y, 4);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("unproject returns null outside the outerRadius", () => {
|
|
78
|
+
const polar = coordPolar({ outerRadius: 50 });
|
|
79
|
+
polar.bindFrame(F);
|
|
80
|
+
// (cx, cy) = (100, 100); pick a point at radius 80 > 50.
|
|
81
|
+
expect(polar.unproject({ x: 180, y: 100 })).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("unproject returns null inside the innerRadius", () => {
|
|
85
|
+
const polar = coordPolar({ innerRadius: 40 });
|
|
86
|
+
polar.bindFrame(F);
|
|
87
|
+
// r = 10 < 40.
|
|
88
|
+
expect(polar.unproject({ x: 110, y: 100 })).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("explicit outerRadius overrides the frame-min default", () => {
|
|
92
|
+
const polar = coordPolar({ outerRadius: 40 });
|
|
93
|
+
polar.bindFrame(F);
|
|
94
|
+
// y=0 → θ=startAngle=-π/2; x=plotW → r=outerR=40.
|
|
95
|
+
// (cx, cy) = (100, 100); θ=-π/2 → (cx, cy - r) = (100, 60).
|
|
96
|
+
const p = polar.project({ x: 200, y: 0 });
|
|
97
|
+
expect(p.x).toBeCloseTo(100, 4);
|
|
98
|
+
expect(p.y).toBeCloseTo(60, 4);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// coordPolar — segment tessellation
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe("coordPolar segment", () => {
|
|
107
|
+
test("radial line (shared θ) emits exactly two endpoints", () => {
|
|
108
|
+
const polar = coordPolar();
|
|
109
|
+
polar.bindFrame(F);
|
|
110
|
+
// Same y → same θ; vary x (the radial channel).
|
|
111
|
+
const points = polar.segment({ x: 20, y: 100 }, { x: 180, y: 100 });
|
|
112
|
+
expect(points).toHaveLength(2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("arc (shared r) tessellates with ~1° steps and angular monotonicity", () => {
|
|
116
|
+
const polar = coordPolar();
|
|
117
|
+
polar.bindFrame(F);
|
|
118
|
+
// Same x → same r (≠ 0); arc from y=20 to y=180.
|
|
119
|
+
const points = polar.segment({ x: 100, y: 20 }, { x: 100, y: 180 });
|
|
120
|
+
expect(points.length).toBeGreaterThan(20); // ~160° of arc at 1° step.
|
|
121
|
+
// Re-derive (θ, r) for each projected point: dx = x - cx, dy = y - cy.
|
|
122
|
+
// The arc should keep r approximately constant.
|
|
123
|
+
const radii = points.map((p) => Math.hypot(p.x - 100, p.y - 100));
|
|
124
|
+
const r0 = radii[0]!;
|
|
125
|
+
for (const r of radii) {
|
|
126
|
+
expect(r).toBeCloseTo(r0, 3);
|
|
127
|
+
}
|
|
128
|
+
// Angular monotonicity: consecutive angles strictly increase (or strictly
|
|
129
|
+
// decrease) — no backtracking.
|
|
130
|
+
const angles = points.map((p) => Math.atan2(p.y - 100, p.x - 100));
|
|
131
|
+
// Unwrap once across the ±π boundary.
|
|
132
|
+
const unwrapped = [angles[0]!];
|
|
133
|
+
for (let i = 1; i < angles.length; i++) {
|
|
134
|
+
let a = angles[i]!;
|
|
135
|
+
const prev = unwrapped[i - 1]!;
|
|
136
|
+
while (a - prev > Math.PI) a -= 2 * Math.PI;
|
|
137
|
+
while (a - prev < -Math.PI) a += 2 * Math.PI;
|
|
138
|
+
unwrapped.push(a);
|
|
139
|
+
}
|
|
140
|
+
const dir = Math.sign(unwrapped[1]! - unwrapped[0]!);
|
|
141
|
+
expect(dir).not.toBe(0);
|
|
142
|
+
for (let i = 1; i < unwrapped.length; i++) {
|
|
143
|
+
expect(Math.sign(unwrapped[i]! - unwrapped[i - 1]!)).toBe(dir);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// coordPolar — renderAxes
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe("coordPolar renderAxes", () => {
|
|
153
|
+
test("draws one line per angular tick (spokes)", () => {
|
|
154
|
+
const polar = coordPolar();
|
|
155
|
+
polar.bindFrame(F);
|
|
156
|
+
const lines: unknown[] = [];
|
|
157
|
+
const layer = makeFakeLayer(lines);
|
|
158
|
+
const angleTicks = [0, 25, 50, 75, 100];
|
|
159
|
+
const angleScale = makeFakePositionScale(angleTicks, [0, 200]);
|
|
160
|
+
const radiusScale = makeFakePositionScale([0, 50, 100], [200, 0]);
|
|
161
|
+
polar.renderAxes({
|
|
162
|
+
axisLayer: layer,
|
|
163
|
+
// Only `scales.x.axisScale` / `scales.x.fn` / `scales.y.*` are read by polar.
|
|
164
|
+
scales: { x: radiusScale, y: angleScale } as never,
|
|
165
|
+
plotFrame: F,
|
|
166
|
+
hasX: true,
|
|
167
|
+
hasY: true,
|
|
168
|
+
xAxisOptions: {},
|
|
169
|
+
yAxisOptions: {},
|
|
170
|
+
atlas: undefined,
|
|
171
|
+
});
|
|
172
|
+
// Default angleChannel is 'y'. There should be exactly `angleTicks.length`
|
|
173
|
+
// spokes — each one a single line from inner radius to outer radius.
|
|
174
|
+
// Concentric circles are tessellated into many lines, but a spoke is a
|
|
175
|
+
// single line. Counting lines via line-length: a circle's lines all share
|
|
176
|
+
// a (cx,cy)-centred radius, while spokes pass through the centre.
|
|
177
|
+
const spokeCount = countSpokes(lines, F.width / 2, F.height / 2);
|
|
178
|
+
expect(spokeCount).toBe(angleTicks.length);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// coordCartesian — handlePan / handleZoom regression
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe("coordCartesian pan/zoom forwarding", () => {
|
|
187
|
+
test("handlePan calls viewport.panBy with the same delta", () => {
|
|
188
|
+
const cart = coordCartesian();
|
|
189
|
+
const { handle, panCalls } = makeViewportStub();
|
|
190
|
+
cart.handlePan({ dx: 7, dy: -3, plotFrame: F, viewport: handle });
|
|
191
|
+
expect(panCalls).toEqual([{ dx: 7, dy: -3 }]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("handleZoom calls viewport.zoomAt with the same anchor + factor", () => {
|
|
195
|
+
const cart = coordCartesian();
|
|
196
|
+
const { handle, zoomCalls } = makeViewportStub();
|
|
197
|
+
cart.handleZoom({ factor: 1.25, cx: 80, cy: 90, plotFrame: F, viewport: handle });
|
|
198
|
+
expect(zoomCalls).toEqual([{ x: 80, y: 90, factor: 1.25 }]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("bindFrame is a no-op (does not throw, leaves project identity)", () => {
|
|
202
|
+
const cart = coordCartesian();
|
|
203
|
+
cart.bindFrame(F);
|
|
204
|
+
expect(cart.project({ x: 12, y: 34 })).toEqual({ x: 12, y: 34 });
|
|
205
|
+
expect(cart.unproject({ x: 12, y: 34 })).toEqual({ x: 12, y: 34 });
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// coordPolar — handlePan rotation + handleZoom radial scaling
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
describe("coordPolar handlePan", () => {
|
|
214
|
+
test("tangential delta (dx) rotates startAngle by (dx / width) * arc", () => {
|
|
215
|
+
const polar = coordPolar();
|
|
216
|
+
polar.bindFrame(F);
|
|
217
|
+
const stateBefore = polarState(polar).startAngle();
|
|
218
|
+
const { handle } = makeViewportStub();
|
|
219
|
+
polar.handlePan({ dx: 50, dy: 0, plotFrame: F, viewport: handle });
|
|
220
|
+
const stateAfter = polarState(polar).startAngle();
|
|
221
|
+
// arc span = 2π by default; rotation = (50 / 200) * 2π = π/2 in default
|
|
222
|
+
// direction = 1.
|
|
223
|
+
expect(stateAfter - stateBefore).toBeCloseTo(Math.PI / 2, 4);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("radial delta (dy) translates the radius scale via viewport.panBy(0, dy)", () => {
|
|
227
|
+
const polar = coordPolar();
|
|
228
|
+
polar.bindFrame(F);
|
|
229
|
+
const { handle, panCalls } = makeViewportStub();
|
|
230
|
+
polar.handlePan({ dx: 0, dy: -12, plotFrame: F, viewport: handle });
|
|
231
|
+
// Default angleChannel='y', so radius is the x channel. The current impl
|
|
232
|
+
// translates the *radius* domain — radius corresponds to viewport's x
|
|
233
|
+
// channel when angleChannel='y'.
|
|
234
|
+
expect(panCalls).toEqual([{ dx: -12, dy: 0 }]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("angleChannel='x' inverts which viewport axis receives the radial pan", () => {
|
|
238
|
+
const polar = coordPolar({ angleChannel: "x" });
|
|
239
|
+
polar.bindFrame(F);
|
|
240
|
+
const { handle, panCalls } = makeViewportStub();
|
|
241
|
+
polar.handlePan({ dx: 0, dy: 5, plotFrame: F, viewport: handle });
|
|
242
|
+
// With angleChannel='x', radius is the y channel.
|
|
243
|
+
expect(panCalls).toEqual([{ dx: 0, dy: 5 }]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("non-full-circle (openAngle) still rotates startAngle on tangential pan", () => {
|
|
247
|
+
const polar = coordPolar({ openAngle: Math.PI / 2 }); // arc = 3π/2
|
|
248
|
+
polar.bindFrame(F);
|
|
249
|
+
const before = polarState(polar).startAngle();
|
|
250
|
+
const { handle } = makeViewportStub();
|
|
251
|
+
polar.handlePan({ dx: 50, dy: 0, plotFrame: F, viewport: handle });
|
|
252
|
+
const after = polarState(polar).startAngle();
|
|
253
|
+
// rotation = (50/200) * (3π/2) = 3π/8.
|
|
254
|
+
expect(after - before).toBeCloseTo((3 * Math.PI) / 8, 4);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("coordPolar handleZoom", () => {
|
|
259
|
+
test("zooms only the radius scale (angle axis unchanged)", () => {
|
|
260
|
+
const polar = coordPolar();
|
|
261
|
+
polar.bindFrame(F);
|
|
262
|
+
const { handle, zoomCalls } = makeViewportStub();
|
|
263
|
+
polar.handleZoom({ factor: 1.5, cx: 130, cy: 100, plotFrame: F, viewport: handle });
|
|
264
|
+
// angleChannel='y', so radius is x.
|
|
265
|
+
expect(zoomCalls).toHaveLength(1);
|
|
266
|
+
expect(zoomCalls[0]!.x).toBe(130);
|
|
267
|
+
expect(zoomCalls[0]!.y).toBe(100);
|
|
268
|
+
expect(zoomCalls[0]!.factor).toEqual({ x: 1.5 });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("startAngle is unchanged by zoom", () => {
|
|
272
|
+
const polar = coordPolar();
|
|
273
|
+
polar.bindFrame(F);
|
|
274
|
+
const before = polarState(polar).startAngle();
|
|
275
|
+
const { handle } = makeViewportStub();
|
|
276
|
+
polar.handleZoom({ factor: 2, cx: 0, cy: 0, plotFrame: F, viewport: handle });
|
|
277
|
+
const after = polarState(polar).startAngle();
|
|
278
|
+
expect(after).toBe(before);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// coordRadial = coordPolar({ openAngle: 0, innerRadius: 0 })
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
describe("coordRadial", () => {
|
|
287
|
+
test("full circle, zero inner radius", () => {
|
|
288
|
+
const radial = coordRadial();
|
|
289
|
+
radial.bindFrame(F);
|
|
290
|
+
const ps = polarState(radial);
|
|
291
|
+
expect(ps.innerRadius()).toBe(0);
|
|
292
|
+
expect(ps.endAngle() - ps.startAngle()).toBeCloseTo(2 * Math.PI, 6);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Fake-layer helpers
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
/** A minimal Layer stub that only captures pushLine shapes. */
|
|
301
|
+
function makeFakeLayer(sink: unknown[]) {
|
|
302
|
+
const noop = () => layer;
|
|
303
|
+
const layer = {
|
|
304
|
+
pushLine: (s: unknown) => {
|
|
305
|
+
sink.push(s);
|
|
306
|
+
return layer;
|
|
307
|
+
},
|
|
308
|
+
pushCircle: noop,
|
|
309
|
+
pushEllipse: noop,
|
|
310
|
+
pushRect: noop,
|
|
311
|
+
pushTriangles: noop,
|
|
312
|
+
pushPolygon: noop,
|
|
313
|
+
pushText: () => ({ width: 0, height: 0, baseline: 0, lines: [] }),
|
|
314
|
+
pushString: noop,
|
|
315
|
+
setClipRect: noop,
|
|
316
|
+
} as unknown as import("insomni").Layer;
|
|
317
|
+
return layer;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Build a fake PositionScale whose `axisScale.ticks()` returns the supplied values. */
|
|
321
|
+
function makeFakePositionScale(ticks: number[], range: [number, number]) {
|
|
322
|
+
const [r0, r1] = range;
|
|
323
|
+
const dom: [number, number] = [Math.min(...ticks), Math.max(...ticks)];
|
|
324
|
+
const apply = (v: number) => r0 + ((v - dom[0]) / (dom[1] - dom[0])) * (r1 - r0);
|
|
325
|
+
return {
|
|
326
|
+
kind: "position",
|
|
327
|
+
type: "linear" as const,
|
|
328
|
+
dataType: "continuous" as const,
|
|
329
|
+
fn: (v: unknown) => apply(v as number),
|
|
330
|
+
axisScale: {
|
|
331
|
+
domain: dom,
|
|
332
|
+
range,
|
|
333
|
+
ticks: () => ticks,
|
|
334
|
+
tickFormat: () => String,
|
|
335
|
+
invert: (px: number) => dom[0] + ((px - r0) / (r1 - r0)) * (dom[1] - dom[0]),
|
|
336
|
+
} as never,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Spokes pass through (cx, cy) (one endpoint at the centre or close to it
|
|
342
|
+
* when innerRadius is 0); concentric arcs do not. Count lines whose endpoints
|
|
343
|
+
* straddle the centre.
|
|
344
|
+
*/
|
|
345
|
+
function countSpokes(lines: unknown[], cx: number, cy: number): number {
|
|
346
|
+
let count = 0;
|
|
347
|
+
for (const raw of lines) {
|
|
348
|
+
const s = raw as { x1: number; y1: number; x2: number; y2: number };
|
|
349
|
+
const onCentre =
|
|
350
|
+
(Math.abs(s.x1 - cx) < 1e-6 && Math.abs(s.y1 - cy) < 1e-6) ||
|
|
351
|
+
(Math.abs(s.x2 - cx) < 1e-6 && Math.abs(s.y2 - cy) < 1e-6);
|
|
352
|
+
if (onCentre) count++;
|
|
353
|
+
}
|
|
354
|
+
return count;
|
|
355
|
+
}
|