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.
Files changed (242) hide show
  1. package/LICENSE.md +674 -0
  2. package/README.md +81 -0
  3. package/dist/core.d.mts +340 -0
  4. package/dist/core.mjs +1047 -0
  5. package/dist/index.d.mts +3426 -0
  6. package/dist/index.mjs +12762 -0
  7. package/dist/interactions-DEFL_F4E.mjs +5395 -0
  8. package/dist/range-presets-CzECsu3V.d.mts +1523 -0
  9. package/package.json +34 -0
  10. package/src/annotations.d.ts +121 -0
  11. package/src/annotations.ts +438 -0
  12. package/src/axis.d.ts +184 -0
  13. package/src/axis.test.ts +131 -0
  14. package/src/axis.ts +765 -0
  15. package/src/colorbar.d.ts +69 -0
  16. package/src/colorbar.ts +294 -0
  17. package/src/colors.d.ts +57 -0
  18. package/src/colors.test.ts +28 -0
  19. package/src/colors.ts +486 -0
  20. package/src/core.ts +299 -0
  21. package/src/format.d.ts +54 -0
  22. package/src/format.ts +138 -0
  23. package/src/grammar/accessibility.d.ts +147 -0
  24. package/src/grammar/accessibility.test.ts +199 -0
  25. package/src/grammar/accessibility.ts +443 -0
  26. package/src/grammar/aes.d.ts +35 -0
  27. package/src/grammar/aes.test.ts +75 -0
  28. package/src/grammar/aes.ts +120 -0
  29. package/src/grammar/annotations.d.ts +86 -0
  30. package/src/grammar/annotations.test.ts +68 -0
  31. package/src/grammar/annotations.ts +336 -0
  32. package/src/grammar/attach-brush.d.ts +44 -0
  33. package/src/grammar/attach-brush.test.ts +214 -0
  34. package/src/grammar/attach-brush.ts +111 -0
  35. package/src/grammar/attach-presets.d.ts +33 -0
  36. package/src/grammar/attach-presets.test.ts +106 -0
  37. package/src/grammar/attach-presets.ts +215 -0
  38. package/src/grammar/chart.d.ts +952 -0
  39. package/src/grammar/chart.test.ts +118 -0
  40. package/src/grammar/chart.ts +1172 -0
  41. package/src/grammar/color-utils.d.ts +29 -0
  42. package/src/grammar/color-utils.test.ts +53 -0
  43. package/src/grammar/color-utils.ts +66 -0
  44. package/src/grammar/constants.d.ts +45 -0
  45. package/src/grammar/constants.ts +61 -0
  46. package/src/grammar/coord.d.ts +183 -0
  47. package/src/grammar/coord.test.ts +355 -0
  48. package/src/grammar/coord.ts +619 -0
  49. package/src/grammar/data/pivot.d.ts +57 -0
  50. package/src/grammar/data/pivot.ts +107 -0
  51. package/src/grammar/emphasis-driver.d.ts +69 -0
  52. package/src/grammar/emphasis-driver.test.ts +199 -0
  53. package/src/grammar/emphasis-driver.ts +205 -0
  54. package/src/grammar/equality.d.ts +3 -0
  55. package/src/grammar/equality.ts +40 -0
  56. package/src/grammar/facet.d.ts +63 -0
  57. package/src/grammar/facet.test.ts +60 -0
  58. package/src/grammar/facet.ts +175 -0
  59. package/src/grammar/geoms/_categorical.d.ts +94 -0
  60. package/src/grammar/geoms/_categorical.ts +0 -0
  61. package/src/grammar/geoms/_distribution.d.ts +52 -0
  62. package/src/grammar/geoms/_distribution.ts +125 -0
  63. package/src/grammar/geoms/_mark.d.ts +69 -0
  64. package/src/grammar/geoms/_mark.ts +136 -0
  65. package/src/grammar/geoms/_shape.d.ts +41 -0
  66. package/src/grammar/geoms/_shape.ts +74 -0
  67. package/src/grammar/geoms/aggregate.d.ts +95 -0
  68. package/src/grammar/geoms/aggregate.test.ts +554 -0
  69. package/src/grammar/geoms/aggregate.ts +840 -0
  70. package/src/grammar/geoms/area.d.ts +32 -0
  71. package/src/grammar/geoms/area.test.ts +165 -0
  72. package/src/grammar/geoms/area.ts +578 -0
  73. package/src/grammar/geoms/band.d.ts +27 -0
  74. package/src/grammar/geoms/band.test.ts +57 -0
  75. package/src/grammar/geoms/band.ts +126 -0
  76. package/src/grammar/geoms/bar.d.ts +56 -0
  77. package/src/grammar/geoms/bar.test.ts +367 -0
  78. package/src/grammar/geoms/bar.ts +1054 -0
  79. package/src/grammar/geoms/boxplot.d.ts +129 -0
  80. package/src/grammar/geoms/boxplot.test.ts +299 -0
  81. package/src/grammar/geoms/boxplot.ts +834 -0
  82. package/src/grammar/geoms/connected-scatter.d.ts +27 -0
  83. package/src/grammar/geoms/connected-scatter.test.ts +157 -0
  84. package/src/grammar/geoms/connected-scatter.ts +63 -0
  85. package/src/grammar/geoms/emphasis.d.ts +76 -0
  86. package/src/grammar/geoms/emphasis.test.ts +135 -0
  87. package/src/grammar/geoms/emphasis.ts +162 -0
  88. package/src/grammar/geoms/histogram.d.ts +75 -0
  89. package/src/grammar/geoms/histogram.test.ts +262 -0
  90. package/src/grammar/geoms/histogram.ts +740 -0
  91. package/src/grammar/geoms/index.d.ts +20 -0
  92. package/src/grammar/geoms/index.ts +77 -0
  93. package/src/grammar/geoms/interval.d.ts +31 -0
  94. package/src/grammar/geoms/interval.test.ts +154 -0
  95. package/src/grammar/geoms/interval.ts +342 -0
  96. package/src/grammar/geoms/line.d.ts +38 -0
  97. package/src/grammar/geoms/line.test.ts +247 -0
  98. package/src/grammar/geoms/line.ts +659 -0
  99. package/src/grammar/geoms/point.d.ts +57 -0
  100. package/src/grammar/geoms/point.test.ts +163 -0
  101. package/src/grammar/geoms/point.ts +545 -0
  102. package/src/grammar/geoms/polar.test.ts +216 -0
  103. package/src/grammar/geoms/ribbon.d.ts +21 -0
  104. package/src/grammar/geoms/ribbon.test.ts +170 -0
  105. package/src/grammar/geoms/ribbon.ts +87 -0
  106. package/src/grammar/geoms/ridgeline.d.ts +89 -0
  107. package/src/grammar/geoms/ridgeline.test.ts +247 -0
  108. package/src/grammar/geoms/ridgeline.ts +1164 -0
  109. package/src/grammar/geoms/rolling.d.ts +43 -0
  110. package/src/grammar/geoms/rolling.test.ts +217 -0
  111. package/src/grammar/geoms/rolling.ts +387 -0
  112. package/src/grammar/geoms/rug.d.ts +28 -0
  113. package/src/grammar/geoms/rug.test.ts +126 -0
  114. package/src/grammar/geoms/rug.ts +214 -0
  115. package/src/grammar/geoms/rule.d.ts +23 -0
  116. package/src/grammar/geoms/rule.test.ts +69 -0
  117. package/src/grammar/geoms/rule.ts +212 -0
  118. package/src/grammar/geoms/smooth.d.ts +54 -0
  119. package/src/grammar/geoms/smooth.test.ts +78 -0
  120. package/src/grammar/geoms/smooth.ts +337 -0
  121. package/src/grammar/geoms/text.d.ts +29 -0
  122. package/src/grammar/geoms/text.test.ts +64 -0
  123. package/src/grammar/geoms/text.ts +234 -0
  124. package/src/grammar/geoms/tile.d.ts +61 -0
  125. package/src/grammar/geoms/tile.test.ts +157 -0
  126. package/src/grammar/geoms/tile.ts +621 -0
  127. package/src/grammar/geoms/types.d.ts +319 -0
  128. package/src/grammar/geoms/types.ts +362 -0
  129. package/src/grammar/geoms/violin.d.ts +85 -0
  130. package/src/grammar/geoms/violin.test.ts +187 -0
  131. package/src/grammar/geoms/violin.ts +672 -0
  132. package/src/grammar/index.d.ts +22 -0
  133. package/src/grammar/index.ts +269 -0
  134. package/src/grammar/interactions/_disposable.d.ts +5 -0
  135. package/src/grammar/interactions/_disposable.ts +23 -0
  136. package/src/grammar/interactions/_z.d.ts +4 -0
  137. package/src/grammar/interactions/_z.ts +16 -0
  138. package/src/grammar/interactions/brush-selection.test.ts +262 -0
  139. package/src/grammar/interactions/brush.d.ts +63 -0
  140. package/src/grammar/interactions/brush.test.ts +483 -0
  141. package/src/grammar/interactions/brush.ts +452 -0
  142. package/src/grammar/interactions/crosshair.d.ts +19 -0
  143. package/src/grammar/interactions/crosshair.test.ts +127 -0
  144. package/src/grammar/interactions/crosshair.ts +76 -0
  145. package/src/grammar/interactions/hit-layer.d.ts +64 -0
  146. package/src/grammar/interactions/hit-layer.ts +246 -0
  147. package/src/grammar/interactions/legend.d.ts +19 -0
  148. package/src/grammar/interactions/legend.ts +101 -0
  149. package/src/grammar/interactions/menu.d.ts +93 -0
  150. package/src/grammar/interactions/menu.test.ts +373 -0
  151. package/src/grammar/interactions/menu.ts +342 -0
  152. package/src/grammar/interactions/selection.d.ts +25 -0
  153. package/src/grammar/interactions/selection.test.ts +289 -0
  154. package/src/grammar/interactions/selection.ts +142 -0
  155. package/src/grammar/interactions/series-readout.d.ts +91 -0
  156. package/src/grammar/interactions/series-readout.test.ts +668 -0
  157. package/src/grammar/interactions/series-readout.ts +422 -0
  158. package/src/grammar/interactions/series-snap.d.ts +70 -0
  159. package/src/grammar/interactions/series-snap.test.ts +214 -0
  160. package/src/grammar/interactions/series-snap.ts +218 -0
  161. package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
  162. package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
  163. package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
  164. package/src/grammar/interactions/tooltip.d.ts +140 -0
  165. package/src/grammar/interactions/tooltip.test.ts +406 -0
  166. package/src/grammar/interactions/tooltip.ts +622 -0
  167. package/src/grammar/interactions/transitions.d.ts +34 -0
  168. package/src/grammar/interactions/transitions.test.ts +172 -0
  169. package/src/grammar/interactions/transitions.ts +160 -0
  170. package/src/grammar/layout.d.ts +68 -0
  171. package/src/grammar/layout.ts +186 -0
  172. package/src/grammar/legend-merge.test.ts +332 -0
  173. package/src/grammar/mount.d.ts +78 -0
  174. package/src/grammar/mount.test.ts +479 -0
  175. package/src/grammar/mount.ts +2112 -0
  176. package/src/grammar/palettes.d.ts +54 -0
  177. package/src/grammar/palettes.test.ts +80 -0
  178. package/src/grammar/palettes.ts +167 -0
  179. package/src/grammar/pan-zoom.test.ts +398 -0
  180. package/src/grammar/phylo.d.ts +65 -0
  181. package/src/grammar/phylo.test.ts +59 -0
  182. package/src/grammar/phylo.ts +112 -0
  183. package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
  184. package/src/grammar/pipeline.d.ts +158 -0
  185. package/src/grammar/pipeline.test.ts +463 -0
  186. package/src/grammar/pipeline.ts +1233 -0
  187. package/src/grammar/profiling.d.ts +8 -0
  188. package/src/grammar/profiling.ts +24 -0
  189. package/src/grammar/scales.d.ts +188 -0
  190. package/src/grammar/scales.test.ts +181 -0
  191. package/src/grammar/scales.ts +800 -0
  192. package/src/grammar/svg.d.ts +3 -0
  193. package/src/grammar/svg.ts +39 -0
  194. package/src/grammar/theme.d.ts +261 -0
  195. package/src/grammar/theme.test.ts +105 -0
  196. package/src/grammar/theme.ts +490 -0
  197. package/src/heatmap/cpu.ts +109 -0
  198. package/src/heatmap/gpu.ts +565 -0
  199. package/src/heatmap/types.ts +177 -0
  200. package/src/heatmap.browser.test.ts +308 -0
  201. package/src/heatmap.test.ts +320 -0
  202. package/src/heatmap.ts +123 -0
  203. package/src/index.d.ts +1 -0
  204. package/src/index.ts +8 -0
  205. package/src/interactions.d.ts +48 -0
  206. package/src/interactions.test.ts +226 -0
  207. package/src/interactions.ts +394 -0
  208. package/src/layout/box.d.ts +48 -0
  209. package/src/layout/box.test.ts +107 -0
  210. package/src/layout/box.ts +143 -0
  211. package/src/legend.d.ts +115 -0
  212. package/src/legend.ts +422 -0
  213. package/src/marks/curve.d.ts +43 -0
  214. package/src/marks/curve.ts +244 -0
  215. package/src/marks/stack.d.ts +53 -0
  216. package/src/marks/stack.ts +184 -0
  217. package/src/marks.d.ts +273 -0
  218. package/src/marks.test.ts +541 -0
  219. package/src/marks.ts +1292 -0
  220. package/src/navigator.test.ts +174 -0
  221. package/src/navigator.ts +393 -0
  222. package/src/range-presets.d.ts +113 -0
  223. package/src/range-presets.test.ts +345 -0
  224. package/src/range-presets.ts +349 -0
  225. package/src/scales.d.ts +98 -0
  226. package/src/scales.test.ts +103 -0
  227. package/src/scales.ts +695 -0
  228. package/src/stats/index.d.ts +200 -0
  229. package/src/stats/index.test.ts +349 -0
  230. package/src/stats/index.ts +740 -0
  231. package/src/stats/regression.d.ts +38 -0
  232. package/src/stats/regression.test.ts +56 -0
  233. package/src/stats/regression.ts +396 -0
  234. package/src/stats/rolling-window.d.ts +55 -0
  235. package/src/stats/rolling-window.test.ts +237 -0
  236. package/src/stats/rolling-window.ts +256 -0
  237. package/src/test-setup.ts +19 -0
  238. package/src/viewport/axis-state.d.ts +72 -0
  239. package/src/viewport/axis-state.ts +476 -0
  240. package/src/viewport.d.ts +170 -0
  241. package/src/viewport.test.ts +363 -0
  242. 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 };