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,64 @@
|
|
|
1
|
+
import { type InteractionManager, type PointerInfo } from "insomni";
|
|
2
|
+
import type { CompiledHitTest, HoveredHit } from "../geoms/types.ts";
|
|
3
|
+
export interface HitEventContext {
|
|
4
|
+
/** Resolved hit projected to the public `HoveredHit` shape. */
|
|
5
|
+
hit: HoveredHit;
|
|
6
|
+
/** The CompiledHitTest the hit belongs to (for channel introspection). */
|
|
7
|
+
compiled: CompiledHitTest<unknown>;
|
|
8
|
+
/** Index into `compiled.positions` / `compiled.dataIndex`. */
|
|
9
|
+
hitIndex: number;
|
|
10
|
+
/** Raw pointer event from the manager. */
|
|
11
|
+
pointer: PointerInfo;
|
|
12
|
+
}
|
|
13
|
+
export interface HitLayerSubscriber {
|
|
14
|
+
/** Stable key for debugging. e.g. "tooltip", "selection". */
|
|
15
|
+
key: string;
|
|
16
|
+
onHoverEnter?(ctx: HitEventContext): void;
|
|
17
|
+
onHoverLeave?(ctx: HitEventContext): void;
|
|
18
|
+
onPress?(ctx: HitEventContext): void;
|
|
19
|
+
}
|
|
20
|
+
export interface GrammarHitLayerDeps {
|
|
21
|
+
manager: InteractionManager;
|
|
22
|
+
element: HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
export interface PickAtOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Which slot kinds participate in the lookup.
|
|
27
|
+
* - `"point"`: only point clouds (uses `pickRadius`).
|
|
28
|
+
* - `"region"`: only region clouds (rect containment).
|
|
29
|
+
* - `"any"`: both, with region containment winning ties.
|
|
30
|
+
* Default `"any"`.
|
|
31
|
+
*/
|
|
32
|
+
mode?: "point" | "region" | "any";
|
|
33
|
+
}
|
|
34
|
+
export interface HitLayerActive {
|
|
35
|
+
layerIdx: number;
|
|
36
|
+
hitIndex: number;
|
|
37
|
+
compiled: CompiledHitTest<unknown>;
|
|
38
|
+
hit: HoveredHit;
|
|
39
|
+
}
|
|
40
|
+
export interface HitLayerState {
|
|
41
|
+
active: HitLayerActive | null;
|
|
42
|
+
}
|
|
43
|
+
export interface PickResult {
|
|
44
|
+
hit: HoveredHit;
|
|
45
|
+
compiled: CompiledHitTest<unknown>;
|
|
46
|
+
hitIndex: number;
|
|
47
|
+
}
|
|
48
|
+
export interface GrammarHitLayer {
|
|
49
|
+
/** Replace the active hit-test set after each pipeline run. */
|
|
50
|
+
sync<T>(hits: readonly CompiledHitTest<T>[]): void;
|
|
51
|
+
/** Register a fan-out target. Returns an unsubscribe fn. */
|
|
52
|
+
subscribe(sub: HitLayerSubscriber): () => void;
|
|
53
|
+
/** Live view of the currently-dispatched hit (or null). */
|
|
54
|
+
state(): HitLayerState;
|
|
55
|
+
/** Subscribe to changes in `state()`. Fires after each transition. */
|
|
56
|
+
subscribeState(fn: (state: HitLayerState) => void): () => void;
|
|
57
|
+
/**
|
|
58
|
+
* One-shot lookup at element-local CSS coordinates. Highest-z slot wins
|
|
59
|
+
* (matches the pointer-event dispatch z-order).
|
|
60
|
+
*/
|
|
61
|
+
pickAt(x: number, y: number, opts?: PickAtOptions): PickResult | null;
|
|
62
|
+
dispose(): void;
|
|
63
|
+
}
|
|
64
|
+
export declare function createGrammarHitLayer(deps: GrammarHitLayerDeps): GrammarHitLayer;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Grammar-level hit-test projection
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Thin adapter on top of insomni's generic `HitFanOut`. Takes the grammar's
|
|
5
|
+
// `CompiledHitTest[]` (which carries data/channels/seriesKey/geomKind), turns
|
|
6
|
+
// each into an opaque `HitSlot` the fan-out can index, and projects events
|
|
7
|
+
// back into grammar shapes (`HoveredHit`, `HitEventContext`).
|
|
8
|
+
//
|
|
9
|
+
// Everything load-bearing — slot lifecycle, multi-subscriber dispatch,
|
|
10
|
+
// primary-state survival across in-place re-syncs, synthesized leaves,
|
|
11
|
+
// `pickAt` — lives in insomni. This file only adds:
|
|
12
|
+
// 1. CompiledHitTest → HitSlot construction
|
|
13
|
+
// 2. Generic event → HoveredHit projection
|
|
14
|
+
//
|
|
15
|
+
// Brush still consumes the raw `CompiledHitTest[]` directly via
|
|
16
|
+
// point-in-rect tests on the positions buffer; that path doesn't need the
|
|
17
|
+
// fan-out and so it isn't routed through here.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
createHitFanOut,
|
|
21
|
+
type HitFanOut,
|
|
22
|
+
type HitFanOutEventContext,
|
|
23
|
+
type HitSlot,
|
|
24
|
+
type InteractionManager,
|
|
25
|
+
type PointerInfo,
|
|
26
|
+
} from "insomni";
|
|
27
|
+
|
|
28
|
+
import type { CompiledHitTest, HoveredHit } from "../geoms/types.ts";
|
|
29
|
+
import { Z_HIT_CLOUD_BASE } from "./_z.ts";
|
|
30
|
+
|
|
31
|
+
export interface HitEventContext {
|
|
32
|
+
/** Resolved hit projected to the public `HoveredHit` shape. */
|
|
33
|
+
hit: HoveredHit;
|
|
34
|
+
/** The CompiledHitTest the hit belongs to (for channel introspection). */
|
|
35
|
+
compiled: CompiledHitTest<unknown>;
|
|
36
|
+
/** Index into `compiled.positions` / `compiled.dataIndex`. */
|
|
37
|
+
hitIndex: number;
|
|
38
|
+
/** Raw pointer event from the manager. */
|
|
39
|
+
pointer: PointerInfo;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface HitLayerSubscriber {
|
|
43
|
+
/** Stable key for debugging. e.g. "tooltip", "selection". */
|
|
44
|
+
key: string;
|
|
45
|
+
onHoverEnter?(ctx: HitEventContext): void;
|
|
46
|
+
onHoverLeave?(ctx: HitEventContext): void;
|
|
47
|
+
onPress?(ctx: HitEventContext): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GrammarHitLayerDeps {
|
|
51
|
+
manager: InteractionManager;
|
|
52
|
+
element: HTMLElement;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PickAtOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Which slot kinds participate in the lookup.
|
|
58
|
+
* - `"point"`: only point clouds (uses `pickRadius`).
|
|
59
|
+
* - `"region"`: only region clouds (rect containment).
|
|
60
|
+
* - `"any"`: both, with region containment winning ties.
|
|
61
|
+
* Default `"any"`.
|
|
62
|
+
*/
|
|
63
|
+
mode?: "point" | "region" | "any";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface HitLayerActive {
|
|
67
|
+
layerIdx: number;
|
|
68
|
+
hitIndex: number;
|
|
69
|
+
compiled: CompiledHitTest<unknown>;
|
|
70
|
+
hit: HoveredHit;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HitLayerState {
|
|
74
|
+
active: HitLayerActive | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface PickResult {
|
|
78
|
+
hit: HoveredHit;
|
|
79
|
+
compiled: CompiledHitTest<unknown>;
|
|
80
|
+
hitIndex: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GrammarHitLayer {
|
|
84
|
+
/** Replace the active hit-test set after each pipeline run. */
|
|
85
|
+
sync<T>(hits: readonly CompiledHitTest<T>[]): void;
|
|
86
|
+
/** Register a fan-out target. Returns an unsubscribe fn. */
|
|
87
|
+
subscribe(sub: HitLayerSubscriber): () => void;
|
|
88
|
+
/** Live view of the currently-dispatched hit (or null). */
|
|
89
|
+
state(): HitLayerState;
|
|
90
|
+
/** Subscribe to changes in `state()`. Fires after each transition. */
|
|
91
|
+
subscribeState(fn: (state: HitLayerState) => void): () => void;
|
|
92
|
+
/**
|
|
93
|
+
* One-shot lookup at element-local CSS coordinates. Highest-z slot wins
|
|
94
|
+
* (matches the pointer-event dispatch z-order).
|
|
95
|
+
*/
|
|
96
|
+
pickAt(x: number, y: number, opts?: PickAtOptions): PickResult | null;
|
|
97
|
+
dispose(): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// CompiledHitTest → HitSlot
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function buildSlot(hit: CompiledHitTest<unknown>, layerIdx: number): HitSlot {
|
|
105
|
+
const useRegions = hit.rects !== undefined && hit.rects.length > 0;
|
|
106
|
+
return useRegions
|
|
107
|
+
? {
|
|
108
|
+
id: layerIdx,
|
|
109
|
+
rects: hit.rects!,
|
|
110
|
+
pickRadius: hit.pickRadius,
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
id: layerIdx,
|
|
114
|
+
positions: hit.positions,
|
|
115
|
+
pickRadius: hit.pickRadius,
|
|
116
|
+
pickAxis: hit.pickAxis,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function projectHit(
|
|
121
|
+
compiled: CompiledHitTest<unknown>,
|
|
122
|
+
hitIndex: number,
|
|
123
|
+
position: { x: number; y: number },
|
|
124
|
+
): HoveredHit {
|
|
125
|
+
return {
|
|
126
|
+
geomKind: compiled.geomKind,
|
|
127
|
+
dataIndex: compiled.dataIndex[hitIndex] ?? hitIndex,
|
|
128
|
+
seriesKey: compiled.seriesKey?.[hitIndex],
|
|
129
|
+
data: compiled.data as readonly unknown[],
|
|
130
|
+
x: position.x,
|
|
131
|
+
y: position.y,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Factory
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
export function createGrammarHitLayer(deps: GrammarHitLayerDeps): GrammarHitLayer {
|
|
140
|
+
// The fan-out is the engine. We keep a parallel `compiledList` so we can
|
|
141
|
+
// recover the CompiledHitTest for each event/state — the fan-out's HitSlot
|
|
142
|
+
// is intentionally opaque (no `data`, no `channels`).
|
|
143
|
+
let compiledList: readonly CompiledHitTest<unknown>[] = [];
|
|
144
|
+
|
|
145
|
+
const fanOut: HitFanOut = createHitFanOut({
|
|
146
|
+
manager: deps.manager,
|
|
147
|
+
element: deps.element,
|
|
148
|
+
baseZIndex: Z_HIT_CLOUD_BASE,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function projectCtx(ctx: HitFanOutEventContext): HitEventContext | null {
|
|
152
|
+
const compiled = compiledList[ctx.slotIdx];
|
|
153
|
+
if (!compiled) return null;
|
|
154
|
+
return {
|
|
155
|
+
hit: projectHit(compiled, ctx.hitIndex, ctx.position),
|
|
156
|
+
compiled,
|
|
157
|
+
hitIndex: ctx.hitIndex,
|
|
158
|
+
pointer: ctx.pointer,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
sync<T>(hits: readonly CompiledHitTest<T>[]) {
|
|
164
|
+
const next = hits as readonly CompiledHitTest<unknown>[];
|
|
165
|
+
const slots: HitSlot[] = next.map((hit, i) => buildSlot(hit, i));
|
|
166
|
+
// Sync the fan-out FIRST so any synthetic leave events for removed
|
|
167
|
+
// slots still reference the OLD compiledList (their slotIdx values
|
|
168
|
+
// are from the previous sync). Only then replace compiledList.
|
|
169
|
+
fanOut.sync(slots);
|
|
170
|
+
compiledList = next;
|
|
171
|
+
},
|
|
172
|
+
subscribe(sub) {
|
|
173
|
+
return fanOut.subscribe({
|
|
174
|
+
key: sub.key,
|
|
175
|
+
onHoverEnter: sub.onHoverEnter
|
|
176
|
+
? (ctx) => {
|
|
177
|
+
const projected = projectCtx(ctx);
|
|
178
|
+
if (projected) sub.onHoverEnter!(projected);
|
|
179
|
+
}
|
|
180
|
+
: undefined,
|
|
181
|
+
onHoverLeave: sub.onHoverLeave
|
|
182
|
+
? (ctx) => {
|
|
183
|
+
const projected = projectCtx(ctx);
|
|
184
|
+
if (projected) sub.onHoverLeave!(projected);
|
|
185
|
+
}
|
|
186
|
+
: undefined,
|
|
187
|
+
onPress: sub.onPress
|
|
188
|
+
? (ctx) => {
|
|
189
|
+
const projected = projectCtx(ctx);
|
|
190
|
+
if (projected) sub.onPress!(projected);
|
|
191
|
+
}
|
|
192
|
+
: undefined,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
state() {
|
|
196
|
+
const s = fanOut.state();
|
|
197
|
+
if (s.active === null) return { active: null };
|
|
198
|
+
const compiled = compiledList[s.active.slotIdx];
|
|
199
|
+
if (!compiled) return { active: null };
|
|
200
|
+
return {
|
|
201
|
+
active: {
|
|
202
|
+
layerIdx: s.active.slotIdx,
|
|
203
|
+
hitIndex: s.active.hitIndex,
|
|
204
|
+
compiled,
|
|
205
|
+
hit: projectHit(compiled, s.active.hitIndex, s.active.position),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
subscribeState(fn) {
|
|
210
|
+
return fanOut.subscribeState((s) => {
|
|
211
|
+
if (s.active === null) {
|
|
212
|
+
fn({ active: null });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const compiled = compiledList[s.active.slotIdx];
|
|
216
|
+
if (!compiled) {
|
|
217
|
+
fn({ active: null });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
fn({
|
|
221
|
+
active: {
|
|
222
|
+
layerIdx: s.active.slotIdx,
|
|
223
|
+
hitIndex: s.active.hitIndex,
|
|
224
|
+
compiled,
|
|
225
|
+
hit: projectHit(compiled, s.active.hitIndex, s.active.position),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
pickAt(x, y, opts) {
|
|
231
|
+
const r = fanOut.pickAt(x, y, opts);
|
|
232
|
+
if (r === null) return null;
|
|
233
|
+
const compiled = compiledList[r.slotIdx];
|
|
234
|
+
if (!compiled) return null;
|
|
235
|
+
return {
|
|
236
|
+
hit: projectHit(compiled, r.hitIndex, r.position),
|
|
237
|
+
compiled,
|
|
238
|
+
hitIndex: r.hitIndex,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
dispose() {
|
|
242
|
+
fanOut.dispose();
|
|
243
|
+
compiledList = [];
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InteractionManager, Invalidator } from "insomni";
|
|
2
|
+
import type { PipelineOutput } from "../pipeline.ts";
|
|
3
|
+
import type { Signal } from "insomni/reactivity";
|
|
4
|
+
export interface GrammarLegendDeps {
|
|
5
|
+
manager: InteractionManager;
|
|
6
|
+
hidden: Signal<ReadonlySet<string>>;
|
|
7
|
+
invalidator: Invalidator;
|
|
8
|
+
}
|
|
9
|
+
export interface GrammarLegend {
|
|
10
|
+
/** Sync interaction nodes with the latest pipeline output. */
|
|
11
|
+
sync<T>(out: PipelineOutput<T>): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Legend interactivity — owns press-only InteractionNodes over each legend
|
|
16
|
+
* entry's bounding box. Toggled keys are written to the `hidden` signal,
|
|
17
|
+
* which the pipeline respects to filter marks and dim legend entries.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createGrammarLegend(deps: GrammarLegendDeps): GrammarLegend;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { InteractionManager, InteractionNode, Invalidator } from "insomni";
|
|
2
|
+
import type { PipelineOutput } from "../pipeline.ts";
|
|
3
|
+
import type { Signal } from "insomni/reactivity";
|
|
4
|
+
import { createDisposable } from "./_disposable.ts";
|
|
5
|
+
|
|
6
|
+
export interface GrammarLegendDeps {
|
|
7
|
+
manager: InteractionManager;
|
|
8
|
+
hidden: Signal<ReadonlySet<string>>;
|
|
9
|
+
invalidator: Invalidator;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GrammarLegend {
|
|
13
|
+
/** Sync interaction nodes with the latest pipeline output. */
|
|
14
|
+
sync<T>(out: PipelineOutput<T>): void;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Legend interactivity — owns press-only InteractionNodes over each legend
|
|
20
|
+
* entry's bounding box. Toggled keys are written to the `hidden` signal,
|
|
21
|
+
* which the pipeline respects to filter marks and dim legend entries.
|
|
22
|
+
*/
|
|
23
|
+
export function createGrammarLegend(deps: GrammarLegendDeps): GrammarLegend {
|
|
24
|
+
let nodes: InteractionNode[] = [];
|
|
25
|
+
// Latest legend ref so node `bounds()` closures pick up new origins/bboxes
|
|
26
|
+
// each frame without rebuilding the InteractionNodes.
|
|
27
|
+
let latest: {
|
|
28
|
+
origin: { x: number; y: number };
|
|
29
|
+
bboxes: ReturnType<NonNullable<PipelineOutput["legend"]>["builder"]["getEntryBboxes"]>;
|
|
30
|
+
} | null = null;
|
|
31
|
+
let labelsKey = "";
|
|
32
|
+
|
|
33
|
+
const rebuild = () => {
|
|
34
|
+
for (const n of nodes) n.destroy();
|
|
35
|
+
if (!latest) {
|
|
36
|
+
nodes = [];
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
nodes = latest.bboxes.map((_b, idx) => {
|
|
40
|
+
return deps.manager.add({
|
|
41
|
+
space: "ui",
|
|
42
|
+
bounds: () => {
|
|
43
|
+
const cur = latest;
|
|
44
|
+
if (!cur) return { x: 0, y: 0, width: 0, height: 0 };
|
|
45
|
+
const bb = cur.bboxes[idx];
|
|
46
|
+
if (!bb) return { x: 0, y: 0, width: 0, height: 0 };
|
|
47
|
+
return {
|
|
48
|
+
x: cur.origin.x + bb.x,
|
|
49
|
+
y: cur.origin.y + bb.y,
|
|
50
|
+
width: bb.width,
|
|
51
|
+
height: bb.height,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
onPress: () => {
|
|
55
|
+
const cur = latest;
|
|
56
|
+
if (!cur) return;
|
|
57
|
+
const bb = cur.bboxes[idx];
|
|
58
|
+
if (!bb) return;
|
|
59
|
+
const next = new Set(deps.hidden.peek());
|
|
60
|
+
if (next.has(bb.label)) next.delete(bb.label);
|
|
61
|
+
else next.add(bb.label);
|
|
62
|
+
deps.hidden.set(next);
|
|
63
|
+
deps.invalidator.invalidate();
|
|
64
|
+
},
|
|
65
|
+
cursor: "pointer",
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const d = createDisposable(() => {
|
|
71
|
+
for (const n of nodes) n.destroy();
|
|
72
|
+
nodes = [];
|
|
73
|
+
latest = null;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
sync(out) {
|
|
78
|
+
if (d.isDisposed) return;
|
|
79
|
+
if (!out.legend) {
|
|
80
|
+
if (nodes.length > 0) {
|
|
81
|
+
for (const n of nodes) n.destroy();
|
|
82
|
+
nodes = [];
|
|
83
|
+
}
|
|
84
|
+
latest = null;
|
|
85
|
+
labelsKey = "";
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const bboxes = out.legend.builder.getEntryBboxes();
|
|
89
|
+
latest = { origin: out.legend.origin, bboxes };
|
|
90
|
+
// Only rebuild InteractionNodes when the legend's structural shape
|
|
91
|
+
// (label list) changes — origin/position changes are picked up by the
|
|
92
|
+
// `bounds()` closure reading `latest`.
|
|
93
|
+
const nextKey = bboxes.map((b) => b.label).join("\0");
|
|
94
|
+
if (nextKey !== labelsKey) {
|
|
95
|
+
labelsKey = nextKey;
|
|
96
|
+
rebuild();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
dispose: () => d.dispose(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ContextMenuOpts, type GlyphAtlas, type InteractionManager, type Invalidator, type Layer, type MenuPlacement, type MenuStyle, type Mods } from "insomni";
|
|
2
|
+
import type { ContextMenuItem, ContextMenuTriggerPayload } from "../chart.ts";
|
|
3
|
+
import type { GeomKind, HoveredHit } from "../geoms/types.ts";
|
|
4
|
+
import type { Theme } from "../theme.ts";
|
|
5
|
+
import type { GrammarHitLayer } from "./hit-layer.ts";
|
|
6
|
+
export type ContextMenuHitMode = "nearest-point" | "any-mark" | "background";
|
|
7
|
+
export interface ContextMenuTriggerInfo {
|
|
8
|
+
/**
|
|
9
|
+
* Resolved hit at the trigger point, or `null` for a background trigger.
|
|
10
|
+
* `nearest-point` resolution uses each compiled hit-test's `pickRadius`;
|
|
11
|
+
* `any-mark` additionally includes region-based geoms (bars, tiles).
|
|
12
|
+
* `background` mode always returns `null`.
|
|
13
|
+
*/
|
|
14
|
+
hit: HoveredHit | null;
|
|
15
|
+
/** Convenience: the resolved data row, or `null` for background. */
|
|
16
|
+
datum: unknown | null;
|
|
17
|
+
/** Convenience: the geom kind that produced the hit (e.g. "point", "bar"). */
|
|
18
|
+
mark: GeomKind | null;
|
|
19
|
+
/** Element-local CSS px where the menu should anchor. */
|
|
20
|
+
screenX: number;
|
|
21
|
+
screenY: number;
|
|
22
|
+
source: "mouse" | "touch" | "pen" | "keyboard";
|
|
23
|
+
mods: Mods;
|
|
24
|
+
originalEvent: Event | null;
|
|
25
|
+
}
|
|
26
|
+
export interface GrammarContextMenuOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Resolution policy for the trigger point. Default `"nearest-point"`.
|
|
29
|
+
*
|
|
30
|
+
* - `"nearest-point"`: walks point-based geoms (scatter, line vertices, ...)
|
|
31
|
+
* only. Honors each compiled hit-test's `pickRadius`. **Suppresses the
|
|
32
|
+
* trigger entirely when no mark is hit.**
|
|
33
|
+
* - `"any-mark"`: includes region-based geoms (bars, histograms, tiles).
|
|
34
|
+
* Same suppression behavior as `"nearest-point"`.
|
|
35
|
+
* - `"background"`: never resolves to a mark; `hit` / `datum` / `mark` are
|
|
36
|
+
* always null. Fires unconditionally — useful for empty-space menus.
|
|
37
|
+
*/
|
|
38
|
+
hitMode?: ContextMenuHitMode;
|
|
39
|
+
/**
|
|
40
|
+
* Optional gate: when this returns true (e.g. an active pan/zoom drag), the
|
|
41
|
+
* context-menu trigger is suppressed. The InteractionManager already cancels
|
|
42
|
+
* touch long-press on drag promotion, but a right-click during a left-button
|
|
43
|
+
* drag still arrives as a native `contextmenu` event — this hook lets the
|
|
44
|
+
* mount tie suppression to its own panning state.
|
|
45
|
+
*/
|
|
46
|
+
isSuppressed?(): boolean;
|
|
47
|
+
/** Override default ContextMenuOpts (holdMs, slopPx) for touch long-press. */
|
|
48
|
+
managerOpts?: ContextMenuOpts;
|
|
49
|
+
/** Low-level trigger emitter (fires alongside the rendered menu, if any). */
|
|
50
|
+
onTrigger?(info: ContextMenuTriggerInfo): void;
|
|
51
|
+
/**
|
|
52
|
+
* Menu items (static or per-trigger). When set, the library renders a
|
|
53
|
+
* canvas menu and routes clicks through `onAction`. When omitted, only
|
|
54
|
+
* `onTrigger` fires.
|
|
55
|
+
*/
|
|
56
|
+
items?: readonly ContextMenuItem[] | ((info: ContextMenuTriggerInfo) => readonly ContextMenuItem[]);
|
|
57
|
+
onAction?(itemId: string, info: ContextMenuTriggerInfo): void;
|
|
58
|
+
placement?: MenuPlacement;
|
|
59
|
+
style?: MenuStyle;
|
|
60
|
+
}
|
|
61
|
+
export interface GrammarContextMenuDeps {
|
|
62
|
+
manager: InteractionManager;
|
|
63
|
+
hitLayer: GrammarHitLayer;
|
|
64
|
+
/** Required only when `items` is configured. Where the menu draws. */
|
|
65
|
+
hudLayer?: () => Layer;
|
|
66
|
+
/** Required only when `items` is configured. For text measurement. */
|
|
67
|
+
atlas?: () => GlyphAtlas | undefined;
|
|
68
|
+
/** Required only when `items` is configured. For default styling tokens. */
|
|
69
|
+
theme?: () => Theme;
|
|
70
|
+
/** Required only when `items` is configured. Bounds for menu clamp/flip. */
|
|
71
|
+
bounds?: () => {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
width: number;
|
|
75
|
+
height: number;
|
|
76
|
+
};
|
|
77
|
+
/** Required only when `items` is configured. Wakes the loop on fade ticks. */
|
|
78
|
+
invalidator?: Invalidator;
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to pan/zoom changes (so the menu closes when the chart moves
|
|
81
|
+
* under it). Returns an unsubscribe, or null when pan/zoom is disabled.
|
|
82
|
+
*/
|
|
83
|
+
onViewportChange?(cb: () => void): (() => void) | null;
|
|
84
|
+
}
|
|
85
|
+
export interface GrammarContextMenu {
|
|
86
|
+
/** Tick fade timers. */
|
|
87
|
+
step(dt: number): void;
|
|
88
|
+
/** Draw the menu into the hud layer. */
|
|
89
|
+
draw(): void;
|
|
90
|
+
dispose(): void;
|
|
91
|
+
}
|
|
92
|
+
export declare function createGrammarContextMenu(deps: GrammarContextMenuDeps, opts: GrammarContextMenuOptions): GrammarContextMenu;
|
|
93
|
+
export type { ContextMenuTriggerPayload };
|