@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21

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 (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
  7. package/dist/chunk-KNT5AZ2O.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -1,367 +0,0 @@
1
- /**
2
- * Graph interaction manager.
3
- *
4
- * Handles mouse/touch events on the canvas and translates them into
5
- * high-level graph interactions: pan, zoom, hover, select, drag nodes.
6
- * Uses the spatial index for hit testing and ZoomTransform for coordinate
7
- * conversion.
8
- */
9
-
10
- import type { SpatialIndex } from './spatial-index';
11
- import { ZoomTransform } from './zoom';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Constants
15
- // ---------------------------------------------------------------------------
16
-
17
- const ZOOM_MIN = 0.05;
18
- const ZOOM_MAX = 15;
19
- const ZOOM_STEP = -0.001;
20
- const HIT_DISTANCE = 5;
21
-
22
- // ---------------------------------------------------------------------------
23
- // Callback interface
24
- // ---------------------------------------------------------------------------
25
-
26
- export interface InteractionCallbacks {
27
- onTransformChange(transform: ZoomTransform): void;
28
- onHoverChange(nodeId: string | null): void;
29
- /** Called during mouse move when no node is hit, with graph-space coordinates for edge hit testing. */
30
- onBackgroundHover?(graphX: number, graphY: number, screenX: number, screenY: number): void;
31
- onSelectionChange(nodeIds: string[]): void;
32
- onNodeDragStart(nodeId: string): void;
33
- onNodeDrag(nodeId: string, x: number, y: number): void;
34
- onNodeDragEnd(nodeId: string): void;
35
- onDoubleClick(nodeId: string): void;
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // Internal state
40
- // ---------------------------------------------------------------------------
41
-
42
- interface DragState {
43
- nodeId: string;
44
- started: boolean;
45
- }
46
-
47
- interface PanState {
48
- startX: number;
49
- startY: number;
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // GraphInteractionManager
54
- // ---------------------------------------------------------------------------
55
-
56
- export class GraphInteractionManager {
57
- private canvas: HTMLCanvasElement;
58
- private spatialIndex: SpatialIndex;
59
- private callbacks: InteractionCallbacks;
60
- private transform = ZoomTransform.identity();
61
-
62
- private dragState: DragState | null = null;
63
- private panState: PanState | null = null;
64
- private mousedownNodeId: string | null = null;
65
- private selectedIds: Set<string> = new Set();
66
-
67
- // Touch state
68
- private lastTouchDist: number | null = null;
69
- private lastTouchCenter: { x: number; y: number } | null = null;
70
-
71
- // Bound handlers for cleanup
72
- private boundWheel: (e: WheelEvent) => void;
73
- private boundMouseDown: (e: MouseEvent) => void;
74
- private boundMouseMove: (e: MouseEvent) => void;
75
- private boundMouseUp: (e: MouseEvent) => void;
76
- private boundDblClick: (e: MouseEvent) => void;
77
- private boundTouchStart: (e: TouchEvent) => void;
78
- private boundTouchMove: (e: TouchEvent) => void;
79
- private boundTouchEnd: (e: TouchEvent) => void;
80
- private boundMouseLeave: (e: MouseEvent) => void;
81
-
82
- constructor(
83
- canvas: HTMLCanvasElement,
84
- spatialIndex: SpatialIndex,
85
- callbacks: InteractionCallbacks,
86
- ) {
87
- this.canvas = canvas;
88
- this.spatialIndex = spatialIndex;
89
- this.callbacks = callbacks;
90
-
91
- // Bind handlers
92
- this.boundWheel = this.onWheel.bind(this);
93
- this.boundMouseDown = this.onMouseDown.bind(this);
94
- this.boundMouseMove = this.onMouseMove.bind(this);
95
- this.boundMouseUp = this.onMouseUp.bind(this);
96
- this.boundMouseLeave = this.onMouseLeave.bind(this);
97
- this.boundDblClick = this.onDblClick.bind(this);
98
- this.boundTouchStart = this.onTouchStart.bind(this);
99
- this.boundTouchMove = this.onTouchMove.bind(this);
100
- this.boundTouchEnd = this.onTouchEnd.bind(this);
101
-
102
- // Attach event listeners
103
- canvas.addEventListener('wheel', this.boundWheel, { passive: false });
104
- canvas.addEventListener('mousedown', this.boundMouseDown);
105
- canvas.addEventListener('mousemove', this.boundMouseMove);
106
- canvas.addEventListener('mouseup', this.boundMouseUp);
107
- canvas.addEventListener('mouseleave', this.boundMouseLeave);
108
- canvas.addEventListener('dblclick', this.boundDblClick);
109
- canvas.addEventListener('touchstart', this.boundTouchStart, {
110
- passive: false,
111
- });
112
- canvas.addEventListener('touchmove', this.boundTouchMove, {
113
- passive: false,
114
- });
115
- canvas.addEventListener('touchend', this.boundTouchEnd);
116
- }
117
-
118
- setTransform(transform: ZoomTransform): void {
119
- this.transform = transform;
120
- }
121
-
122
- getTransform(): ZoomTransform {
123
- return this.transform;
124
- }
125
-
126
- destroy(): void {
127
- this.canvas.removeEventListener('wheel', this.boundWheel);
128
- this.canvas.removeEventListener('mousedown', this.boundMouseDown);
129
- this.canvas.removeEventListener('mousemove', this.boundMouseMove);
130
- this.canvas.removeEventListener('mouseup', this.boundMouseUp);
131
- this.canvas.removeEventListener('mouseleave', this.boundMouseLeave);
132
- this.canvas.removeEventListener('dblclick', this.boundDblClick);
133
- this.canvas.removeEventListener('touchstart', this.boundTouchStart);
134
- this.canvas.removeEventListener('touchmove', this.boundTouchMove);
135
- this.canvas.removeEventListener('touchend', this.boundTouchEnd);
136
- }
137
-
138
- // -------------------------------------------------------------------------
139
- // Mouse handlers
140
- // -------------------------------------------------------------------------
141
-
142
- private canvasXY(e: MouseEvent): { x: number; y: number } {
143
- const rect = this.canvas.getBoundingClientRect();
144
- return { x: e.clientX - rect.left, y: e.clientY - rect.top };
145
- }
146
-
147
- private hitTest(screenX: number, screenY: number): string | null {
148
- const graph = this.transform.screenToGraph(screenX, screenY);
149
- const node = this.spatialIndex.findNearest(graph.x, graph.y, HIT_DISTANCE / this.transform.k);
150
- return node?.id ?? null;
151
- }
152
-
153
- private onWheel(e: WheelEvent): void {
154
- e.preventDefault();
155
- const { x, y } = this.canvasXY(e);
156
- const factor = e.deltaY * ZOOM_STEP;
157
- const newK = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, this.transform.k * (1 + factor)));
158
- this.transform = this.transform.zoomAt(newK, x, y);
159
- this.callbacks.onTransformChange(this.transform);
160
- }
161
-
162
- private onMouseDown(e: MouseEvent): void {
163
- const { x, y } = this.canvasXY(e);
164
- const hitId = this.hitTest(x, y);
165
-
166
- if (hitId) {
167
- // Start potential node drag
168
- this.dragState = { nodeId: hitId, started: false };
169
- this.mousedownNodeId = hitId;
170
- } else {
171
- // Start pan
172
- this.panState = { startX: x, startY: y };
173
- this.mousedownNodeId = null;
174
- }
175
- }
176
-
177
- private onMouseMove(e: MouseEvent): void {
178
- const { x, y } = this.canvasXY(e);
179
-
180
- if (this.dragState) {
181
- const graph = this.transform.screenToGraph(x, y);
182
- if (!this.dragState.started) {
183
- this.dragState.started = true;
184
- this.callbacks.onNodeDragStart(this.dragState.nodeId);
185
- }
186
- this.callbacks.onNodeDrag(this.dragState.nodeId, graph.x, graph.y);
187
- return;
188
- }
189
-
190
- if (this.panState) {
191
- const dx = x - this.panState.startX;
192
- const dy = y - this.panState.startY;
193
- this.transform = this.transform.pan(dx, dy);
194
- this.panState = { startX: x, startY: y };
195
- this.callbacks.onTransformChange(this.transform);
196
- return;
197
- }
198
-
199
- // Hover detection
200
- const hitId = this.hitTest(x, y);
201
- this.callbacks.onHoverChange(hitId);
202
-
203
- // If no node hit, check edges via callback
204
- if (!hitId) {
205
- const graph = this.transform.screenToGraph(x, y);
206
- this.callbacks.onBackgroundHover?.(graph.x, graph.y, x, y);
207
- }
208
-
209
- // Update cursor
210
- this.canvas.style.cursor = hitId ? 'pointer' : 'default';
211
- }
212
-
213
- private onMouseUp(e: MouseEvent): void {
214
- const { x, y } = this.canvasXY(e);
215
-
216
- if (this.dragState) {
217
- if (this.dragState.started) {
218
- this.callbacks.onNodeDragEnd(this.dragState.nodeId);
219
- } else {
220
- // Was a click on a node (no drag movement)
221
- this.handleNodeClick(this.dragState.nodeId, e.shiftKey);
222
- }
223
- this.dragState = null;
224
- return;
225
- }
226
-
227
- if (this.panState) {
228
- this.panState = null;
229
-
230
- // If mouse up is on background (no node), treat as background click
231
- if (!this.mousedownNodeId) {
232
- const hitId = this.hitTest(x, y);
233
- if (!hitId) {
234
- // Background click: clear selection
235
- this.selectedIds.clear();
236
- this.callbacks.onSelectionChange([]);
237
- }
238
- }
239
- return;
240
- }
241
- }
242
-
243
- private onDblClick(e: MouseEvent): void {
244
- const { x, y } = this.canvasXY(e);
245
- const hitId = this.hitTest(x, y);
246
- if (hitId) {
247
- this.callbacks.onDoubleClick(hitId);
248
- }
249
- }
250
-
251
- private onMouseLeave(_e: MouseEvent): void {
252
- this.callbacks.onHoverChange(null);
253
- this.canvas.style.cursor = 'default';
254
-
255
- // Cancel any in-progress pan
256
- if (this.panState) {
257
- this.panState = null;
258
- }
259
- }
260
-
261
- private handleNodeClick(nodeId: string, shiftKey: boolean): void {
262
- if (shiftKey) {
263
- // Toggle node in multi-select
264
- if (this.selectedIds.has(nodeId)) {
265
- this.selectedIds.delete(nodeId);
266
- } else {
267
- this.selectedIds.add(nodeId);
268
- }
269
- } else {
270
- // Single select
271
- this.selectedIds.clear();
272
- this.selectedIds.add(nodeId);
273
- }
274
-
275
- this.callbacks.onSelectionChange([...this.selectedIds]);
276
- }
277
-
278
- // -------------------------------------------------------------------------
279
- // Touch handlers
280
- // -------------------------------------------------------------------------
281
-
282
- private onTouchStart(e: TouchEvent): void {
283
- e.preventDefault();
284
-
285
- if (e.touches.length === 2) {
286
- // Pinch-zoom start
287
- const [t0, t1] = [e.touches[0], e.touches[1]];
288
- this.lastTouchDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
289
- this.lastTouchCenter = {
290
- x: (t0.clientX + t1.clientX) / 2,
291
- y: (t0.clientY + t1.clientY) / 2,
292
- };
293
- } else if (e.touches.length === 1) {
294
- const touch = e.touches[0];
295
- const rect = this.canvas.getBoundingClientRect();
296
- const x = touch.clientX - rect.left;
297
- const y = touch.clientY - rect.top;
298
-
299
- const hitId = this.hitTest(x, y);
300
- if (hitId) {
301
- this.mousedownNodeId = hitId;
302
- } else {
303
- this.panState = { startX: x, startY: y };
304
- this.mousedownNodeId = null;
305
- }
306
- }
307
- }
308
-
309
- private onTouchMove(e: TouchEvent): void {
310
- e.preventDefault();
311
-
312
- if (e.touches.length === 2 && this.lastTouchDist !== null) {
313
- const [t0, t1] = [e.touches[0], e.touches[1]];
314
- const newDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
315
- const rect = this.canvas.getBoundingClientRect();
316
- const centerX = (t0.clientX + t1.clientX) / 2 - rect.left;
317
- const centerY = (t0.clientY + t1.clientY) / 2 - rect.top;
318
-
319
- const scale = newDist / this.lastTouchDist;
320
- const newK = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, this.transform.k * scale));
321
- this.transform = this.transform.zoomAt(newK, centerX, centerY);
322
-
323
- // Pan from center movement
324
- if (this.lastTouchCenter) {
325
- const dx = centerX - (this.lastTouchCenter.x - rect.left);
326
- const dy = centerY - (this.lastTouchCenter.y - rect.top);
327
- this.transform = this.transform.pan(dx, dy);
328
- }
329
-
330
- this.lastTouchDist = newDist;
331
- this.lastTouchCenter = {
332
- x: (t0.clientX + t1.clientX) / 2,
333
- y: (t0.clientY + t1.clientY) / 2,
334
- };
335
- this.callbacks.onTransformChange(this.transform);
336
- } else if (e.touches.length === 1 && this.panState) {
337
- const touch = e.touches[0];
338
- const rect = this.canvas.getBoundingClientRect();
339
- const x = touch.clientX - rect.left;
340
- const y = touch.clientY - rect.top;
341
-
342
- const dx = x - this.panState.startX;
343
- const dy = y - this.panState.startY;
344
- this.transform = this.transform.pan(dx, dy);
345
- this.panState = { startX: x, startY: y };
346
- this.callbacks.onTransformChange(this.transform);
347
- }
348
- }
349
-
350
- private onTouchEnd(e: TouchEvent): void {
351
- if (e.touches.length === 0) {
352
- // Tap-select
353
- if (this.mousedownNodeId && !this.panState) {
354
- this.handleNodeClick(this.mousedownNodeId, false);
355
- } else if (!this.mousedownNodeId && this.panState) {
356
- // Background tap: clear selection
357
- this.selectedIds.clear();
358
- this.callbacks.onSelectionChange([]);
359
- }
360
-
361
- this.panState = null;
362
- this.mousedownNodeId = null;
363
- this.lastTouchDist = null;
364
- this.lastTouchCenter = null;
365
- }
366
- }
367
- }
@@ -1,208 +0,0 @@
1
- /**
2
- * Keyboard navigation for the graph canvas.
3
- *
4
- * Provides accessible keyboard control: Tab to focus, arrow keys to
5
- * navigate between adjacent nodes (following edges), Enter to select,
6
- * Escape to clear, +/- to zoom, Home to fit all, / to focus search.
7
- */
8
-
9
- import type { PositionedNode } from './types';
10
-
11
- // ---------------------------------------------------------------------------
12
- // Options
13
- // ---------------------------------------------------------------------------
14
-
15
- export interface KeyboardNavOptions {
16
- canvas: HTMLCanvasElement;
17
- getNodes(): PositionedNode[];
18
- getSelectedIds(): string[];
19
- getAdjacency(): Map<string, Set<string>>;
20
- onSelect(nodeId: string): void;
21
- onDeselect(): void;
22
- onZoom(direction: 'in' | 'out'): void;
23
- onFitAll(): void;
24
- onFocusSearch?(): void;
25
- }
26
-
27
- // ---------------------------------------------------------------------------
28
- // Implementation
29
- // ---------------------------------------------------------------------------
30
-
31
- /**
32
- * Attach keyboard navigation to a graph canvas.
33
- * Returns a cleanup function that removes all listeners.
34
- */
35
- export function attachGraphKeyboardNav(options: KeyboardNavOptions): () => void {
36
- const {
37
- canvas,
38
- getNodes,
39
- getSelectedIds,
40
- getAdjacency,
41
- onSelect,
42
- onDeselect,
43
- onZoom,
44
- onFitAll,
45
- onFocusSearch,
46
- } = options;
47
-
48
- let focusedNodeId: string | null = null;
49
-
50
- // Make canvas focusable
51
- if (!canvas.hasAttribute('tabindex')) {
52
- canvas.setAttribute('tabindex', '0');
53
- }
54
-
55
- function findNodeById(id: string): PositionedNode | undefined {
56
- return getNodes().find((n) => n.id === id);
57
- }
58
-
59
- /**
60
- * Given a set of neighbor node ids, pick the one that best matches
61
- * the arrow key direction relative to the current focused node.
62
- */
63
- function pickDirectionalNeighbor(
64
- fromNode: PositionedNode,
65
- neighborIds: Set<string>,
66
- direction: 'up' | 'down' | 'left' | 'right',
67
- ): string | null {
68
- const nodes = getNodes();
69
- const candidates = nodes.filter((n) => neighborIds.has(n.id));
70
- if (candidates.length === 0) return null;
71
-
72
- // Score each candidate by how well it matches the desired direction
73
- let best: PositionedNode | null = null;
74
- let bestScore = -Infinity;
75
-
76
- for (const c of candidates) {
77
- const dx = c.x - fromNode.x;
78
- const dy = c.y - fromNode.y;
79
- let score: number;
80
-
81
- switch (direction) {
82
- case 'right':
83
- score = dx - Math.abs(dy) * 0.5;
84
- break;
85
- case 'left':
86
- score = -dx - Math.abs(dy) * 0.5;
87
- break;
88
- case 'down':
89
- score = dy - Math.abs(dx) * 0.5;
90
- break;
91
- case 'up':
92
- score = -dy - Math.abs(dx) * 0.5;
93
- break;
94
- }
95
-
96
- if (score > bestScore) {
97
- bestScore = score;
98
- best = c;
99
- }
100
- }
101
-
102
- return best?.id ?? null;
103
- }
104
-
105
- function onKeyDown(e: KeyboardEvent): void {
106
- switch (e.key) {
107
- case 'Tab': {
108
- // Focus first/selected node
109
- const selected = getSelectedIds();
110
- const nodes = getNodes();
111
- if (nodes.length === 0) return;
112
-
113
- if (selected.length > 0) {
114
- focusedNodeId = selected[0];
115
- } else if (!focusedNodeId || !findNodeById(focusedNodeId)) {
116
- focusedNodeId = nodes[0].id;
117
- }
118
-
119
- e.preventDefault();
120
- break;
121
- }
122
-
123
- case 'ArrowUp':
124
- case 'ArrowDown':
125
- case 'ArrowLeft':
126
- case 'ArrowRight': {
127
- if (!focusedNodeId) return;
128
- e.preventDefault();
129
-
130
- const focusedNode = findNodeById(focusedNodeId);
131
- if (!focusedNode) return;
132
-
133
- const adjacency = getAdjacency();
134
- const neighbors = adjacency.get(focusedNodeId);
135
- if (!neighbors || neighbors.size === 0) return;
136
-
137
- const dirMap: Record<string, 'up' | 'down' | 'left' | 'right'> = {
138
- ArrowUp: 'up',
139
- ArrowDown: 'down',
140
- ArrowLeft: 'left',
141
- ArrowRight: 'right',
142
- };
143
-
144
- const nextId = pickDirectionalNeighbor(focusedNode, neighbors, dirMap[e.key]);
145
- if (nextId) {
146
- focusedNodeId = nextId;
147
- onSelect(nextId);
148
- }
149
- break;
150
- }
151
-
152
- case 'Enter': {
153
- if (focusedNodeId) {
154
- e.preventDefault();
155
- const selected = getSelectedIds();
156
- if (selected.includes(focusedNodeId)) {
157
- onDeselect();
158
- } else {
159
- onSelect(focusedNodeId);
160
- }
161
- }
162
- break;
163
- }
164
-
165
- case 'Escape': {
166
- e.preventDefault();
167
- focusedNodeId = null;
168
- onDeselect();
169
- break;
170
- }
171
-
172
- case '+':
173
- case '=': {
174
- e.preventDefault();
175
- onZoom('in');
176
- break;
177
- }
178
-
179
- case '-':
180
- case '_': {
181
- e.preventDefault();
182
- onZoom('out');
183
- break;
184
- }
185
-
186
- case 'Home': {
187
- e.preventDefault();
188
- onFitAll();
189
- break;
190
- }
191
-
192
- case '/': {
193
- if (onFocusSearch) {
194
- e.preventDefault();
195
- onFocusSearch();
196
- }
197
- break;
198
- }
199
- }
200
- }
201
-
202
- canvas.addEventListener('keydown', onKeyDown);
203
-
204
- // Return cleanup function
205
- return () => {
206
- canvas.removeEventListener('keydown', onKeyDown);
207
- };
208
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Graph search manager.
3
- *
4
- * Provides case-insensitive substring matching against node labels/ids.
5
- * Returns a Set of matching node ids that the renderer uses to dim
6
- * non-matching nodes and edges.
7
- */
8
-
9
- export class GraphSearchManager {
10
- private matchedIds: Set<string> | null = null;
11
-
12
- /**
13
- * Search for nodes matching the query string.
14
- * Returns a Set of matching node ids, or an empty set if nothing matches.
15
- */
16
- search(query: string, nodes: Array<{ id: string; label?: string }>): Set<string> {
17
- const q = query.toLowerCase().trim();
18
-
19
- if (q === '') {
20
- this.matchedIds = null;
21
- return new Set();
22
- }
23
-
24
- const matches = new Set<string>();
25
- for (const node of nodes) {
26
- const label = (node.label ?? '').toLowerCase();
27
- const id = node.id.toLowerCase();
28
- if (label.includes(q) || id.includes(q)) {
29
- matches.add(node.id);
30
- }
31
- }
32
-
33
- this.matchedIds = matches;
34
- return matches;
35
- }
36
-
37
- /**
38
- * Clear the current search.
39
- * Returns null to indicate no active search.
40
- */
41
- clearSearch(): Set<string> | null {
42
- this.matchedIds = null;
43
- return null;
44
- }
45
-
46
- /** Get the current set of matched ids, or null if no search is active. */
47
- getMatches(): Set<string> | null {
48
- return this.matchedIds;
49
- }
50
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * Creates a Web Worker running the force simulation.
3
- *
4
- * References the built .js file via `new URL` + `import.meta.url`.
5
- * The consuming app's bundler resolves the worker path at build time.
6
- *
7
- * Note: SimulationManager handles .js/.ts fallback internally. This
8
- * helper is exported for consumers who want to manage the worker directly.
9
- */
10
- export function createSimulationWorker(): Worker {
11
- return new Worker(new URL('./simulation-worker.js', import.meta.url), { type: 'module' });
12
- }