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,483 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DragPointerInfo,
|
|
3
|
+
InteractionManager,
|
|
4
|
+
InteractionNode,
|
|
5
|
+
InteractionNodeSpec,
|
|
6
|
+
PointerInfo,
|
|
7
|
+
} from "insomni";
|
|
8
|
+
import { describe, expect, test, vi } from "vite-plus/test";
|
|
9
|
+
|
|
10
|
+
// Mock isCoarsePointer so we can control its return value per test.
|
|
11
|
+
// All other exports from "insomni" are preserved via importActual.
|
|
12
|
+
vi.mock("insomni", async (importActual) => {
|
|
13
|
+
const actual = await importActual<typeof import("insomni")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
isCoarsePointer: vi.fn(() => false),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
import type { CompiledHitTest, HoveredHit } from "../geoms/types.ts";
|
|
21
|
+
import { themeDefault } from "../theme.ts";
|
|
22
|
+
import { createGrammarBrush } from "./brush.ts";
|
|
23
|
+
|
|
24
|
+
interface CapturedNode {
|
|
25
|
+
spec: InteractionNodeSpec;
|
|
26
|
+
destroyed: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fakeManager(): {
|
|
30
|
+
manager: InteractionManager;
|
|
31
|
+
nodes: CapturedNode[];
|
|
32
|
+
fireBackgroundTap: () => void;
|
|
33
|
+
} {
|
|
34
|
+
const nodes: CapturedNode[] = [];
|
|
35
|
+
let bgHandler: (() => void) | null = null;
|
|
36
|
+
const manager = {
|
|
37
|
+
element: {} as HTMLElement,
|
|
38
|
+
add(spec: InteractionNodeSpec): InteractionNode {
|
|
39
|
+
const captured: CapturedNode = { spec, destroyed: false };
|
|
40
|
+
nodes.push(captured);
|
|
41
|
+
const id = Symbol("node");
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
update(patch) {
|
|
45
|
+
captured.spec = { ...captured.spec, ...patch };
|
|
46
|
+
},
|
|
47
|
+
destroy() {
|
|
48
|
+
captured.destroyed = true;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
addPointCloud() {
|
|
53
|
+
throw new Error("not used");
|
|
54
|
+
},
|
|
55
|
+
onBackgroundTap(handler: () => void) {
|
|
56
|
+
bgHandler = handler;
|
|
57
|
+
return () => {
|
|
58
|
+
if (bgHandler === handler) bgHandler = null;
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
onChange() {
|
|
62
|
+
return () => {};
|
|
63
|
+
},
|
|
64
|
+
destroy() {},
|
|
65
|
+
} as unknown as InteractionManager;
|
|
66
|
+
return {
|
|
67
|
+
manager,
|
|
68
|
+
nodes,
|
|
69
|
+
fireBackgroundTap: () => bgHandler?.(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const noMods = { shift: false, ctrl: false, meta: false, alt: false };
|
|
74
|
+
function pointer(x: number, y: number): PointerInfo {
|
|
75
|
+
return {
|
|
76
|
+
pointerId: 1,
|
|
77
|
+
type: "mouse",
|
|
78
|
+
x,
|
|
79
|
+
y,
|
|
80
|
+
localX: x,
|
|
81
|
+
localY: y,
|
|
82
|
+
buttons: 1,
|
|
83
|
+
mods: noMods,
|
|
84
|
+
stopPropagation: () => {},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function dragInfo(x: number, y: number, dx = 0, dy = 0): DragPointerInfo {
|
|
88
|
+
return { ...pointer(x, y), dx, dy };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Row {
|
|
92
|
+
x: number;
|
|
93
|
+
y: number;
|
|
94
|
+
}
|
|
95
|
+
function hitFor(
|
|
96
|
+
positions: number[],
|
|
97
|
+
indices: number[],
|
|
98
|
+
data: readonly Row[],
|
|
99
|
+
): CompiledHitTest<Row> {
|
|
100
|
+
return {
|
|
101
|
+
geomKind: "point",
|
|
102
|
+
positions: Float32Array.from(positions),
|
|
103
|
+
dataIndex: Int32Array.from(indices),
|
|
104
|
+
pickRadius: 5,
|
|
105
|
+
channels: {},
|
|
106
|
+
data,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fullBounds = () => ({ x: 0, y: 0, width: 100, height: 200 });
|
|
111
|
+
|
|
112
|
+
function setup(opts: { axis?: "x" | "y" | "xy" } = {}) {
|
|
113
|
+
const { manager, nodes, fireBackgroundTap } = fakeManager();
|
|
114
|
+
const events: HoveredHit[][] = [];
|
|
115
|
+
const layerCalls: { rects: number } = { rects: 0 };
|
|
116
|
+
const hudLayer = {
|
|
117
|
+
pushRect: () => {
|
|
118
|
+
layerCalls.rects++;
|
|
119
|
+
return hudLayer;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const inv = { invalidate: () => {}, dirty: false } as never;
|
|
123
|
+
const brush = createGrammarBrush(
|
|
124
|
+
{
|
|
125
|
+
manager,
|
|
126
|
+
bounds: fullBounds,
|
|
127
|
+
hudLayer: () => hudLayer as never,
|
|
128
|
+
theme: () => themeDefault,
|
|
129
|
+
invalidator: inv,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
axis: opts.axis,
|
|
133
|
+
onChange: (s) => events.push(s),
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
return { brush, nodes, events, fireBackgroundTap, layerCalls };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
describe("grammar brush", () => {
|
|
140
|
+
test("registers a drag-capable node bounded to the plot frame, a translate node, plus 8 resize handles", () => {
|
|
141
|
+
const { nodes } = setup();
|
|
142
|
+
// 1 create-drag node + 1 translate node + 8 handle nodes for axis "xy".
|
|
143
|
+
expect(nodes).toHaveLength(10);
|
|
144
|
+
const node0 = nodes[0]!.spec;
|
|
145
|
+
expect(typeof node0.onDragStart).toBe("function");
|
|
146
|
+
expect(typeof node0.onDragMove).toBe("function");
|
|
147
|
+
expect(typeof node0.onDragEnd).toBe("function");
|
|
148
|
+
// oxlint-disable-next-line unbound-method -- checking that onPress is absent (undefined); not invoking the method
|
|
149
|
+
expect(node0.onPress).toBeUndefined();
|
|
150
|
+
expect(node0.bounds!()).toEqual(fullBounds());
|
|
151
|
+
// Translate + handle nodes all claim press to block background-tap clears
|
|
152
|
+
// on click-without-drag inside the rect.
|
|
153
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
154
|
+
const nodeI = nodes[i]!.spec;
|
|
155
|
+
expect(typeof nodeI.onPress).toBe("function");
|
|
156
|
+
expect(typeof nodeI.onDragStart).toBe("function");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("axis='x' only emits east/west handles", () => {
|
|
161
|
+
const { nodes } = setup({ axis: "x" });
|
|
162
|
+
// 1 create node + 1 translate node + 2 handles ("e", "w").
|
|
163
|
+
expect(nodes).toHaveLength(4);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("handle bounds collapse to zero while there is no rect", () => {
|
|
167
|
+
const { nodes } = setup();
|
|
168
|
+
// Order: [create, translate, n, s, e, w, ne, nw, se, sw]; east = index 4.
|
|
169
|
+
const eHandle = nodes[4]!;
|
|
170
|
+
const b = eHandle.spec.bounds!();
|
|
171
|
+
expect(b.width).toBe(0);
|
|
172
|
+
expect(b.height).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("dragging a corner handle resizes the committed rect", () => {
|
|
176
|
+
const { brush, nodes } = setup();
|
|
177
|
+
const data: Row[] = [{ x: 60, y: 70 }];
|
|
178
|
+
brush.sync([hitFor([60, 70], [0], data)]);
|
|
179
|
+
// Commit an initial rect.
|
|
180
|
+
nodes[0]!.spec.onDragStart!(pointer(10, 20));
|
|
181
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 50));
|
|
182
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 50));
|
|
183
|
+
expect(brush.rect()).toEqual({ x: 10, y: 20, width: 30, height: 30 });
|
|
184
|
+
|
|
185
|
+
// Order: [create, translate, n, s, e, w, ne, nw, se, sw]; SE corner = index 8.
|
|
186
|
+
const seHandle = nodes[8]!;
|
|
187
|
+
seHandle.spec.onDragStart!(pointer(40, 50));
|
|
188
|
+
seHandle.spec.onDragMove!(dragInfo(80, 100));
|
|
189
|
+
seHandle.spec.onDragEnd!(pointer(80, 100));
|
|
190
|
+
expect(brush.rect()).toEqual({ x: 10, y: 20, width: 70, height: 80 });
|
|
191
|
+
// Row at (60,70) is now inside.
|
|
192
|
+
expect(brush.current().map((h) => h.dataIndex)).toEqual([0]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("opts.handles=false skips handle node registration", () => {
|
|
196
|
+
const { manager, nodes } = fakeManager();
|
|
197
|
+
const hudLayer = { pushRect: () => hudLayer };
|
|
198
|
+
const inv = { invalidate: () => {}, dirty: false } as never;
|
|
199
|
+
createGrammarBrush(
|
|
200
|
+
{
|
|
201
|
+
manager,
|
|
202
|
+
bounds: fullBounds,
|
|
203
|
+
hudLayer: () => hudLayer as never,
|
|
204
|
+
theme: () => themeDefault,
|
|
205
|
+
invalidator: inv,
|
|
206
|
+
},
|
|
207
|
+
{ handles: false, translate: false },
|
|
208
|
+
);
|
|
209
|
+
expect(nodes).toHaveLength(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("dragging the rect interior translates without resizing", () => {
|
|
213
|
+
const { brush, nodes } = setup();
|
|
214
|
+
// Commit an initial rect at (10,20) 30x30.
|
|
215
|
+
nodes[0]!.spec.onDragStart!(pointer(10, 20));
|
|
216
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 50));
|
|
217
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 50));
|
|
218
|
+
expect(brush.rect()).toEqual({ x: 10, y: 20, width: 30, height: 30 });
|
|
219
|
+
|
|
220
|
+
// Translate node is at index 1. Drag from inside the rect by (+15, +5).
|
|
221
|
+
const tNode = nodes[1]!;
|
|
222
|
+
tNode.spec.onDragStart!(pointer(25, 35));
|
|
223
|
+
tNode.spec.onDragMove!(dragInfo(40, 40));
|
|
224
|
+
tNode.spec.onDragEnd!(pointer(40, 40));
|
|
225
|
+
expect(brush.rect()).toEqual({ x: 25, y: 25, width: 30, height: 30 });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("snap=true snaps rect x edges to nearest hit-test x for axis='x'", () => {
|
|
229
|
+
const { manager, nodes } = fakeManager();
|
|
230
|
+
const hudLayer = { pushRect: () => hudLayer };
|
|
231
|
+
const inv = { invalidate: () => {}, dirty: false } as never;
|
|
232
|
+
const brush = createGrammarBrush(
|
|
233
|
+
{
|
|
234
|
+
manager,
|
|
235
|
+
bounds: fullBounds,
|
|
236
|
+
hudLayer: () => hudLayer as never,
|
|
237
|
+
theme: () => themeDefault,
|
|
238
|
+
invalidator: inv,
|
|
239
|
+
},
|
|
240
|
+
{ axis: "x", snap: true },
|
|
241
|
+
);
|
|
242
|
+
const data: Row[] = [
|
|
243
|
+
{ x: 10, y: 100 },
|
|
244
|
+
{ x: 30, y: 100 },
|
|
245
|
+
{ x: 50, y: 100 },
|
|
246
|
+
{ x: 70, y: 100 },
|
|
247
|
+
{ x: 90, y: 100 },
|
|
248
|
+
];
|
|
249
|
+
brush.sync([hitFor([10, 100, 30, 100, 50, 100, 70, 100, 90, 100], [0, 1, 2, 3, 4], data)]);
|
|
250
|
+
|
|
251
|
+
nodes[0]!.spec.onDragStart!(pointer(12, 0));
|
|
252
|
+
nodes[0]!.spec.onDragMove!(dragInfo(48, 0));
|
|
253
|
+
nodes[0]!.spec.onDragEnd!(pointer(48, 0));
|
|
254
|
+
// x edges snap to nearest ticks: 12 → 10, 48 → 50.
|
|
255
|
+
expect(brush.rect()).toEqual({ x: 10, y: 0, width: 40, height: 200 });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("snap=false leaves the rect alone", () => {
|
|
259
|
+
const { manager, nodes } = fakeManager();
|
|
260
|
+
const hudLayer = { pushRect: () => hudLayer };
|
|
261
|
+
const inv = { invalidate: () => {}, dirty: false } as never;
|
|
262
|
+
const brush = createGrammarBrush(
|
|
263
|
+
{
|
|
264
|
+
manager,
|
|
265
|
+
bounds: fullBounds,
|
|
266
|
+
hudLayer: () => hudLayer as never,
|
|
267
|
+
theme: () => themeDefault,
|
|
268
|
+
invalidator: inv,
|
|
269
|
+
},
|
|
270
|
+
{ axis: "x" },
|
|
271
|
+
);
|
|
272
|
+
const data: Row[] = [{ x: 10, y: 100 }];
|
|
273
|
+
brush.sync([hitFor([10, 100], [0], data)]);
|
|
274
|
+
|
|
275
|
+
nodes[0]!.spec.onDragStart!(pointer(12, 0));
|
|
276
|
+
nodes[0]!.spec.onDragMove!(dragInfo(48, 0));
|
|
277
|
+
nodes[0]!.spec.onDragEnd!(pointer(48, 0));
|
|
278
|
+
expect(brush.rect()).toEqual({ x: 12, y: 0, width: 36, height: 200 });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("translate clamps to bounds without changing size", () => {
|
|
282
|
+
const { brush, nodes } = setup();
|
|
283
|
+
// Initial rect (10,20) 30x30 — bounds are 0..100 × 0..200.
|
|
284
|
+
nodes[0]!.spec.onDragStart!(pointer(10, 20));
|
|
285
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 50));
|
|
286
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 50));
|
|
287
|
+
|
|
288
|
+
// Try to drag far past the right edge — should clamp at x = 70 (100 - 30).
|
|
289
|
+
const tNode = nodes[1]!;
|
|
290
|
+
tNode.spec.onDragStart!(pointer(25, 35));
|
|
291
|
+
tNode.spec.onDragMove!(dragInfo(500, 35));
|
|
292
|
+
tNode.spec.onDragEnd!(pointer(500, 35));
|
|
293
|
+
expect(brush.rect()).toEqual({ x: 70, y: 20, width: 30, height: 30 });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("drag selects rows whose hit position falls inside the rect", () => {
|
|
297
|
+
const { brush, nodes, events } = setup();
|
|
298
|
+
const data: Row[] = [
|
|
299
|
+
{ x: 10, y: 20 },
|
|
300
|
+
{ x: 50, y: 60 },
|
|
301
|
+
{ x: 80, y: 90 },
|
|
302
|
+
];
|
|
303
|
+
brush.sync([hitFor([10, 20, 50, 60, 80, 90], [0, 1, 2], data)]);
|
|
304
|
+
|
|
305
|
+
nodes[0]!.spec.onDragStart!(pointer(5, 15));
|
|
306
|
+
nodes[0]!.spec.onDragMove!(dragInfo(60, 70));
|
|
307
|
+
expect(brush.current().map((h) => h.dataIndex)).toEqual([0, 1]);
|
|
308
|
+
nodes[0]!.spec.onDragEnd!(pointer(60, 70));
|
|
309
|
+
expect(brush.current().map((h) => h.dataIndex)).toEqual([0, 1]);
|
|
310
|
+
expect(events.at(-1)!.map((h) => h.dataIndex)).toEqual([0, 1]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("backwards drag normalizes — same hits regardless of direction", () => {
|
|
314
|
+
const { brush, nodes } = setup();
|
|
315
|
+
const data: Row[] = [
|
|
316
|
+
{ x: 25, y: 30 },
|
|
317
|
+
{ x: 70, y: 80 },
|
|
318
|
+
];
|
|
319
|
+
brush.sync([hitFor([25, 30, 70, 80], [0, 1], data)]);
|
|
320
|
+
|
|
321
|
+
nodes[0]!.spec.onDragStart!(pointer(80, 90));
|
|
322
|
+
nodes[0]!.spec.onDragMove!(dragInfo(20, 25));
|
|
323
|
+
nodes[0]!.spec.onDragEnd!(pointer(20, 25));
|
|
324
|
+
expect(
|
|
325
|
+
brush
|
|
326
|
+
.current()
|
|
327
|
+
.map((h) => h.dataIndex)
|
|
328
|
+
.sort((a, b) => a - b),
|
|
329
|
+
).toEqual([0, 1]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("axis=x ignores y position when matching rows", () => {
|
|
333
|
+
const { brush, nodes } = setup({ axis: "x" });
|
|
334
|
+
const data: Row[] = [
|
|
335
|
+
{ x: 10, y: 5 },
|
|
336
|
+
{ x: 30, y: 195 },
|
|
337
|
+
{ x: 80, y: 100 },
|
|
338
|
+
];
|
|
339
|
+
brush.sync([hitFor([10, 5, 30, 195, 80, 100], [0, 1, 2], data)]);
|
|
340
|
+
|
|
341
|
+
// Drag horizontally over x=10..40; y range shouldn't filter anything in
|
|
342
|
+
// that x band.
|
|
343
|
+
nodes[0]!.spec.onDragStart!(pointer(5, 100));
|
|
344
|
+
nodes[0]!.spec.onDragMove!(dragInfo(45, 110));
|
|
345
|
+
expect(
|
|
346
|
+
brush
|
|
347
|
+
.current()
|
|
348
|
+
.map((h) => h.dataIndex)
|
|
349
|
+
.sort((a, b) => a - b),
|
|
350
|
+
).toEqual([0, 1]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("background tap clears the brush", () => {
|
|
354
|
+
const { brush, nodes, events, fireBackgroundTap } = setup();
|
|
355
|
+
const data: Row[] = [{ x: 10, y: 20 }];
|
|
356
|
+
brush.sync([hitFor([10, 20], [0], data)]);
|
|
357
|
+
|
|
358
|
+
nodes[0]!.spec.onDragStart!(pointer(5, 15));
|
|
359
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 60));
|
|
360
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 60));
|
|
361
|
+
expect(brush.current()).toHaveLength(1);
|
|
362
|
+
expect(brush.rect()).not.toBeNull();
|
|
363
|
+
|
|
364
|
+
fireBackgroundTap();
|
|
365
|
+
expect(brush.current()).toHaveLength(0);
|
|
366
|
+
expect(brush.rect()).toBeNull();
|
|
367
|
+
expect(events.at(-1)!).toEqual([]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("clear() empties brush state and fires onChange when needed", () => {
|
|
371
|
+
const { brush, nodes, events } = setup();
|
|
372
|
+
const data: Row[] = [{ x: 10, y: 20 }];
|
|
373
|
+
brush.sync([hitFor([10, 20], [0], data)]);
|
|
374
|
+
nodes[0]!.spec.onDragStart!(pointer(5, 15));
|
|
375
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 60));
|
|
376
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 60));
|
|
377
|
+
const before = events.length;
|
|
378
|
+
brush.clear();
|
|
379
|
+
expect(brush.rect()).toBeNull();
|
|
380
|
+
expect(brush.current()).toHaveLength(0);
|
|
381
|
+
expect(events.length).toBeGreaterThan(before);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("sync() refreshes membership when positions change beneath a static rect", () => {
|
|
385
|
+
const { brush, nodes, events } = setup();
|
|
386
|
+
const data: Row[] = [
|
|
387
|
+
{ x: 10, y: 20 },
|
|
388
|
+
{ x: 50, y: 60 },
|
|
389
|
+
];
|
|
390
|
+
brush.sync([hitFor([10, 20, 50, 60], [0, 1], data)]);
|
|
391
|
+
nodes[0]!.spec.onDragStart!(pointer(0, 0));
|
|
392
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 40));
|
|
393
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 40));
|
|
394
|
+
expect(brush.current().map((h) => h.dataIndex)).toEqual([0]);
|
|
395
|
+
|
|
396
|
+
// Row 1 moves into the rect; row 0 stays.
|
|
397
|
+
const before = events.length;
|
|
398
|
+
brush.sync([hitFor([10, 20, 25, 30], [0, 1], data)]);
|
|
399
|
+
expect(
|
|
400
|
+
brush
|
|
401
|
+
.current()
|
|
402
|
+
.map((h) => h.dataIndex)
|
|
403
|
+
.sort((a, b) => a - b),
|
|
404
|
+
).toEqual([0, 1]);
|
|
405
|
+
expect(events.length).toBeGreaterThan(before);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("dispose tears down the main node + handles and stops bg-tap from clearing", () => {
|
|
409
|
+
const { brush, nodes, fireBackgroundTap } = setup();
|
|
410
|
+
brush.dispose();
|
|
411
|
+
for (const n of nodes) expect(n.destroyed).toBe(true);
|
|
412
|
+
fireBackgroundTap(); // should be a no-op now
|
|
413
|
+
expect(brush.current()).toHaveLength(0);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Handle size: coarse-pointer auto-scaling
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
import { isCoarsePointer } from "insomni";
|
|
422
|
+
|
|
423
|
+
/** Commit an initial rect and return the SE corner handle width == handleSize. */
|
|
424
|
+
function handleSizeFromSetup(): number {
|
|
425
|
+
const { manager, nodes } = fakeManager();
|
|
426
|
+
const hudLayer = { pushRect: () => hudLayer };
|
|
427
|
+
const inv = { invalidate: () => {}, dirty: false } as never;
|
|
428
|
+
const brush = createGrammarBrush(
|
|
429
|
+
{
|
|
430
|
+
manager,
|
|
431
|
+
bounds: fullBounds,
|
|
432
|
+
hudLayer: () => hudLayer as never,
|
|
433
|
+
theme: () => themeDefault,
|
|
434
|
+
invalidator: inv,
|
|
435
|
+
},
|
|
436
|
+
{},
|
|
437
|
+
);
|
|
438
|
+
// Commit a rect so handle bounds are non-zero.
|
|
439
|
+
nodes[0]!.spec.onDragStart!(pointer(10, 20));
|
|
440
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 50));
|
|
441
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 50));
|
|
442
|
+
// SE corner is at index 8; its width == handleSize (h × h square).
|
|
443
|
+
// Read bounds BEFORE dispose, which collapses the rect to null.
|
|
444
|
+
const width = nodes[8]!.spec.bounds!().width;
|
|
445
|
+
brush.dispose();
|
|
446
|
+
return width;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
describe("grammar brush handle size", () => {
|
|
450
|
+
test("defaults to 8px on fine pointer (mouse)", () => {
|
|
451
|
+
vi.mocked(isCoarsePointer).mockReturnValue(false);
|
|
452
|
+
expect(handleSizeFromSetup()).toBe(8);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("defaults to 24px on coarse pointer (touch)", () => {
|
|
456
|
+
vi.mocked(isCoarsePointer).mockReturnValue(true);
|
|
457
|
+
expect(handleSizeFromSetup()).toBe(24);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("explicit opts.handleSize always wins over auto-scaling", () => {
|
|
461
|
+
vi.mocked(isCoarsePointer).mockReturnValue(true);
|
|
462
|
+
const { manager, nodes } = fakeManager();
|
|
463
|
+
const hudLayer = { pushRect: () => hudLayer };
|
|
464
|
+
const inv = { invalidate: () => {}, dirty: false } as never;
|
|
465
|
+
const brush = createGrammarBrush(
|
|
466
|
+
{
|
|
467
|
+
manager,
|
|
468
|
+
bounds: fullBounds,
|
|
469
|
+
hudLayer: () => hudLayer as never,
|
|
470
|
+
theme: () => themeDefault,
|
|
471
|
+
invalidator: inv,
|
|
472
|
+
},
|
|
473
|
+
{ handleSize: 12 },
|
|
474
|
+
);
|
|
475
|
+
nodes[0]!.spec.onDragStart!(pointer(10, 20));
|
|
476
|
+
nodes[0]!.spec.onDragMove!(dragInfo(40, 50));
|
|
477
|
+
nodes[0]!.spec.onDragEnd!(pointer(40, 50));
|
|
478
|
+
// SE corner bounds.width == handleSize = 12 (not 24). Read before dispose.
|
|
479
|
+
const width = nodes[8]!.spec.bounds!().width;
|
|
480
|
+
brush.dispose();
|
|
481
|
+
expect(width).toBe(12);
|
|
482
|
+
});
|
|
483
|
+
});
|