@cosmos.gl/graph 2.4.0 → 2.5.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 (36) hide show
  1. package/dist/config.d.ts +69 -0
  2. package/dist/index.d.ts +16 -6
  3. package/dist/index.js +4328 -4129
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +113 -45
  6. package/dist/index.min.js.map +1 -1
  7. package/dist/modules/Lines/index.d.ts +8 -0
  8. package/dist/modules/Store/index.d.ts +14 -2
  9. package/dist/modules/core-module.d.ts +1 -0
  10. package/dist/stories/beginners/link-hovering/data-generator.d.ts +19 -0
  11. package/dist/stories/beginners/link-hovering/index.d.ts +5 -0
  12. package/dist/stories/beginners.stories.d.ts +1 -0
  13. package/dist/variables.d.ts +5 -2
  14. package/package.json +1 -1
  15. package/src/config.ts +86 -2
  16. package/src/index.ts +151 -31
  17. package/src/modules/Lines/draw-curve-line.frag +12 -1
  18. package/src/modules/Lines/draw-curve-line.vert +29 -2
  19. package/src/modules/Lines/hovered-line-index.frag +27 -0
  20. package/src/modules/Lines/hovered-line-index.vert +8 -0
  21. package/src/modules/Lines/index.ts +112 -2
  22. package/src/modules/Store/index.ts +33 -2
  23. package/src/modules/core-module.ts +1 -0
  24. package/src/stories/1. welcome.mdx +2 -1
  25. package/src/stories/2. configuration.mdx +10 -1
  26. package/src/stories/3. api-reference.mdx +13 -4
  27. package/src/stories/beginners/basic-set-up/index.ts +20 -10
  28. package/src/stories/beginners/link-hovering/data-generator.ts +198 -0
  29. package/src/stories/beginners/link-hovering/index.ts +61 -0
  30. package/src/stories/beginners/link-hovering/style.css +73 -0
  31. package/src/stories/beginners/quick-start.ts +2 -1
  32. package/src/stories/beginners/remove-points/index.ts +28 -30
  33. package/src/stories/beginners.stories.ts +17 -0
  34. package/src/stories/clusters/polygon-selection/index.ts +2 -4
  35. package/src/stories/shapes/image-example/index.ts +7 -8
  36. package/src/variables.ts +5 -2
@@ -1,17 +1,25 @@
1
+ import { default as regl } from 'regl';
1
2
  import { CoreModule } from '../core-module';
2
3
  export declare class Lines extends CoreModule {
4
+ linkIndexFbo: regl.Framebuffer2D | undefined;
5
+ hoveredLineIndexFbo: regl.Framebuffer2D | undefined;
3
6
  private drawCurveCommand;
7
+ private hoveredLineIndexCommand;
4
8
  private pointsBuffer;
5
9
  private colorBuffer;
6
10
  private widthBuffer;
7
11
  private arrowBuffer;
8
12
  private curveLineGeometry;
9
13
  private curveLineBuffer;
14
+ private linkIndexBuffer;
15
+ private quadBuffer;
10
16
  initPrograms(): void;
11
17
  draw(): void;
18
+ updateLinkIndexFbo(): void;
12
19
  updatePointsBuffer(): void;
13
20
  updateColor(): void;
14
21
  updateWidth(): void;
15
22
  updateArrow(): void;
16
23
  updateCurveLineGeometry(): void;
24
+ findHoveredLine(): void;
17
25
  }
@@ -1,6 +1,13 @@
1
1
  import { mat3 } from 'gl-matrix';
2
+ import { GraphConfigInterface } from '../../config';
2
3
  export declare const ALPHA_MIN = 0.001;
3
4
  export declare const MAX_POINT_SIZE = 64;
5
+ /**
6
+ * Maximum number of executions to delay before performing hover detection.
7
+ * This threshold prevents excessive hover detection calls for performance optimization.
8
+ * The `findHoveredItem` method will skip actual detection until this count is reached.
9
+ */
10
+ export declare const MAX_HOVER_DETECTION_DELAY = 4;
4
11
  export type Hovered = {
5
12
  index: number;
6
13
  position: [number, number];
@@ -24,14 +31,17 @@ export declare class Store {
24
31
  hoveredPoint: Hovered | undefined;
25
32
  focusedPoint: Focused | undefined;
26
33
  draggingPointIndex: number | undefined;
34
+ hoveredLinkIndex: number | undefined;
27
35
  adjustedSpaceSize: number;
28
36
  isSpaceKeyPressed: boolean;
29
37
  div: HTMLDivElement | undefined;
30
38
  webglMaxTextureSize: number;
31
39
  hoveredPointRingColor: number[];
32
40
  focusedPointRingColor: number[];
41
+ hoveredLinkColor: number[];
33
42
  greyoutPointColor: number[];
34
43
  isDarkenGreyout: boolean;
44
+ isLinkHoveringEnabled: boolean;
35
45
  private alphaTarget;
36
46
  private scalePointX;
37
47
  private scalePointY;
@@ -53,9 +63,11 @@ export declare class Store {
53
63
  updateScreenSize(width: number, height: number): void;
54
64
  scaleX(x: number): number;
55
65
  scaleY(y: number): number;
56
- setHoveredPointRingColor(color: string): void;
57
- setFocusedPointRingColor(color: string): void;
66
+ setHoveredPointRingColor(color: string | [number, number, number, number]): void;
67
+ setFocusedPointRingColor(color: string | [number, number, number, number]): void;
58
68
  setGreyoutPointColor(color: string | [number, number, number, number] | undefined): void;
69
+ updateLinkHoveringEnabled(config: Pick<GraphConfigInterface, 'onLinkClick' | 'onLinkMouseOver' | 'onLinkMouseOut'>): void;
70
+ setHoveredLinkColor(color?: string | [number, number, number, number]): void;
59
71
  setFocusedPoint(index?: number): void;
60
72
  addAlpha(decay: number): number;
61
73
  private alphaDecay;
@@ -9,5 +9,6 @@ export declare class CoreModule {
9
9
  readonly store: Store;
10
10
  readonly data: GraphData;
11
11
  readonly points: Points | undefined;
12
+ _debugRandomNumber: number;
12
13
  constructor(reglInstance: regl.Regl, config: GraphConfigInterface, store: Store, data: GraphData, points?: Points);
13
14
  }
@@ -0,0 +1,19 @@
1
+ interface Point {
2
+ id: number;
3
+ }
4
+ interface Link {
5
+ source: number;
6
+ target: number;
7
+ }
8
+ interface NetworkData {
9
+ pointPositions: Float32Array;
10
+ pointColors: Float32Array;
11
+ pointSizes: Float32Array;
12
+ links: Float32Array;
13
+ linkColors: Float32Array;
14
+ linkWidths: Float32Array;
15
+ points: Point[];
16
+ connections: Link[];
17
+ }
18
+ export declare function generateData(pointCount?: number): NetworkData;
19
+ export {};
@@ -0,0 +1,5 @@
1
+ import { Graph } from '../../..';
2
+ export declare const linkHovering: () => {
3
+ div: HTMLDivElement;
4
+ graph: Graph;
5
+ };
@@ -6,4 +6,5 @@ export declare const QuickStart: Story;
6
6
  export declare const BasicSetUp: Story;
7
7
  export declare const PointLabels: Story;
8
8
  export declare const RemovePoints: Story;
9
+ export declare const LinkHovering: Story;
9
10
  export default meta;
@@ -13,18 +13,21 @@ export declare const defaultConfigValues: {
13
13
  spaceSize: number;
14
14
  pointSizeScale: number;
15
15
  linkWidthScale: number;
16
- arrowSizeScale: number;
16
+ linkArrowsSizeScale: number;
17
17
  renderLinks: boolean;
18
18
  curvedLinks: boolean;
19
19
  curvedLinkSegments: number;
20
20
  curvedLinkWeight: number;
21
21
  curvedLinkControlPointDistance: number;
22
- arrowLinks: boolean;
22
+ linkArrows: boolean;
23
23
  linkVisibilityDistanceRange: number[];
24
24
  linkVisibilityMinTransparency: number;
25
25
  hoveredPointCursor: string;
26
+ hoveredLinkCursor: string;
26
27
  renderHoveredPointRing: boolean;
27
28
  hoveredPointRingColor: string;
29
+ hoveredLinkColor: undefined;
30
+ hoveredLinkWidthIncrease: number;
28
31
  focusedPointRingColor: string;
29
32
  focusedPointIndex: undefined;
30
33
  useClassicQuadtree: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmos.gl/graph",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "GPU-based force graph layout and rendering",
5
5
  "jsdelivr": "dist/index.min.js",
6
6
  "main": "dist/index.js",
package/src/config.ts CHANGED
@@ -98,6 +98,12 @@ export interface GraphConfigInterface {
98
98
  */
99
99
  hoveredPointCursor?: string;
100
100
 
101
+ /**
102
+ * Cursor style to use when hovering over a link
103
+ * Default value: `auto`
104
+ */
105
+ hoveredLinkCursor?: string;
106
+
101
107
  /**
102
108
  * Turns ring rendering around a point on hover on / off
103
109
  * Default value: `false`
@@ -158,6 +164,19 @@ export interface GraphConfigInterface {
158
164
  * Default value: `1`
159
165
  */
160
166
  linkWidth?: number;
167
+ /**
168
+ * The color to use for links when they are hovered.
169
+ * This can be either a hex color string (e.g., '#ff3333') or an array of RGBA values
170
+ * in the format `[red, green, blue, alpha]` where each value is a number between 0 and 255.
171
+ * Default value: `undefined`
172
+ */
173
+ hoveredLinkColor?: string | [number, number, number, number];
174
+ /**
175
+ * Number of pixels to add to the link width when hovered.
176
+ * The hovered width is calculated as: originalWidth + hoveredLinkWidthIncrease
177
+ * Default value: `5`
178
+ */
179
+ hoveredLinkWidthIncrease?: number;
161
180
  /**
162
181
  * Scale factor for the link width.
163
182
  * Default value: `1`
@@ -324,9 +343,15 @@ export interface GraphConfigInterface {
324
343
  onSimulationPause?: () => void;
325
344
  /**
326
345
  * Callback function that will be called when the simulation is restarted.
346
+ * @deprecated Use `onSimulationUnpause` instead. This callback will be removed in a future version.
327
347
  * Default value: `undefined`
328
348
  */
329
349
  onSimulationRestart?: () => void;
350
+ /**
351
+ * Callback function that will be called when the simulation is unpaused.
352
+ * Default value: `undefined`
353
+ */
354
+ onSimulationUnpause?: () => void;
330
355
 
331
356
  /**
332
357
  * Callback function that will be called on every canvas click.
@@ -339,6 +364,40 @@ export interface GraphConfigInterface {
339
364
  index: number | undefined, pointPosition: [number, number] | undefined, event: MouseEvent
340
365
  ) => void;
341
366
 
367
+ /**
368
+ * Callback function that will be called when a point is clicked.
369
+ * The point index will be passed as the first argument,
370
+ * position as the second argument and the corresponding mouse event as the third argument:
371
+ * `(index: number, pointPosition: [number, number], event: MouseEvent) => void`.
372
+ * Default value: `undefined`
373
+ */
374
+ onPointClick?: (
375
+ index: number,
376
+ pointPosition: [number, number],
377
+ event: MouseEvent
378
+ ) => void;
379
+
380
+ /**
381
+ * Callback function that will be called when a link is clicked.
382
+ * The link index will be passed as the first argument and the corresponding mouse event as the second argument:
383
+ * `(linkIndex: number, event: MouseEvent) => void`.
384
+ * Default value: `undefined`
385
+ */
386
+ onLinkClick?: (
387
+ linkIndex: number,
388
+ event: MouseEvent
389
+ ) => void;
390
+
391
+ /**
392
+ * Callback function that will be called when the background (empty space) is clicked.
393
+ * The mouse event will be passed as the first argument:
394
+ * `(event: MouseEvent) => void`.
395
+ * Default value: `undefined`
396
+ */
397
+ onBackgroundClick?: (
398
+ event: MouseEvent
399
+ ) => void;
400
+
342
401
  /**
343
402
  * Callback function that will be called when mouse movement happens.
344
403
  * If the mouse moves over a point, its index will be passed as the first argument,
@@ -374,6 +433,22 @@ export interface GraphConfigInterface {
374
433
  */
375
434
  onPointMouseOut?: (event: MouseEvent | D3ZoomEvent<HTMLCanvasElement, undefined> | D3DragEvent<HTMLCanvasElement, undefined, Hovered> | undefined) => void;
376
435
 
436
+ /**
437
+ * Callback function that will be called when the mouse moves over a link.
438
+ * The link index will be passed as the first argument:
439
+ * `(linkIndex: number) => void`.
440
+ * Default value: `undefined`
441
+ */
442
+ onLinkMouseOver?: (linkIndex: number) => void;
443
+
444
+ /**
445
+ * Callback function that will be called when the mouse moves out of a link.
446
+ * The event will be passed as the first argument:
447
+ * `(event: MouseEvent | D3ZoomEvent<HTMLCanvasElement, undefined> | D3DragEvent<HTMLCanvasElement, undefined, Hovered> | undefined) => void`.
448
+ * Default value: `undefined`
449
+ */
450
+ onLinkMouseOut?: (event: MouseEvent | D3ZoomEvent<HTMLCanvasElement, undefined> | D3DragEvent<HTMLCanvasElement, undefined, Hovered> | undefined) => void;
451
+
377
452
  /**
378
453
  * Callback function that will be called when zooming or panning starts.
379
454
  * First argument is a D3 Zoom Event and second indicates whether
@@ -546,6 +621,7 @@ export class GraphConfig implements GraphConfigInterface {
546
621
  public pointOpacity = defaultPointOpacity
547
622
  public pointSizeScale = defaultConfigValues.pointSizeScale
548
623
  public hoveredPointCursor = defaultConfigValues.hoveredPointCursor
624
+ public hoveredLinkCursor = defaultConfigValues.hoveredLinkCursor
549
625
  public renderHoveredPointRing = defaultConfigValues.renderHoveredPointRing
550
626
  public hoveredPointRingColor = defaultConfigValues.hoveredPointRingColor
551
627
  public focusedPointRingColor = defaultConfigValues.focusedPointRingColor
@@ -555,13 +631,15 @@ export class GraphConfig implements GraphConfigInterface {
555
631
  public linkGreyoutOpacity = defaultGreyoutLinkOpacity
556
632
  public linkWidth = defaultLinkWidth
557
633
  public linkWidthScale = defaultConfigValues.linkWidthScale
634
+ public hoveredLinkColor = defaultConfigValues.hoveredLinkColor
635
+ public hoveredLinkWidthIncrease = defaultConfigValues.hoveredLinkWidthIncrease
558
636
  public renderLinks = defaultConfigValues.renderLinks
559
637
  public curvedLinks = defaultConfigValues.curvedLinks
560
638
  public curvedLinkSegments = defaultConfigValues.curvedLinkSegments
561
639
  public curvedLinkWeight = defaultConfigValues.curvedLinkWeight
562
640
  public curvedLinkControlPointDistance = defaultConfigValues.curvedLinkControlPointDistance
563
- public linkArrows = defaultConfigValues.arrowLinks
564
- public linkArrowsSizeScale = defaultConfigValues.arrowSizeScale
641
+ public linkArrows = defaultConfigValues.linkArrows
642
+ public linkArrowsSizeScale = defaultConfigValues.linkArrowsSizeScale
565
643
  public scaleLinksOnZoom = defaultConfigValues.scaleLinksOnZoom
566
644
  public linkVisibilityDistanceRange = defaultConfigValues.linkVisibilityDistanceRange
567
645
  public linkVisibilityMinTransparency = defaultConfigValues.linkVisibilityMinTransparency
@@ -586,11 +664,17 @@ export class GraphConfig implements GraphConfigInterface {
586
664
  public onSimulationEnd: GraphConfigInterface['onSimulationEnd'] = undefined
587
665
  public onSimulationPause: GraphConfigInterface['onSimulationPause'] = undefined
588
666
  public onSimulationRestart: GraphConfigInterface['onSimulationRestart'] = undefined
667
+ public onSimulationUnpause: GraphConfigInterface['onSimulationUnpause'] = undefined
589
668
 
590
669
  public onClick: GraphConfigInterface['onClick'] = undefined
670
+ public onPointClick: GraphConfigInterface['onPointClick'] = undefined
671
+ public onLinkClick: GraphConfigInterface['onLinkClick'] = undefined
672
+ public onBackgroundClick: GraphConfigInterface['onBackgroundClick'] = undefined
591
673
  public onMouseMove: GraphConfigInterface['onMouseMove'] = undefined
592
674
  public onPointMouseOver: GraphConfigInterface['onPointMouseOver'] = undefined
593
675
  public onPointMouseOut: GraphConfigInterface['onPointMouseOut'] = undefined
676
+ public onLinkMouseOver: GraphConfigInterface['onLinkMouseOver'] = undefined
677
+ public onLinkMouseOut: GraphConfigInterface['onLinkMouseOut'] = undefined
594
678
  public onZoomStart: GraphConfigInterface['onZoomStart'] = undefined
595
679
  public onZoom: GraphConfigInterface['onZoom'] = undefined
596
680
  public onZoomEnd: GraphConfigInterface['onZoomEnd'] = undefined
package/src/index.ts CHANGED
@@ -17,10 +17,10 @@ import { FPSMonitor } from '@/graph/modules/FPSMonitor'
17
17
  import { GraphData } from '@/graph/modules/GraphData'
18
18
  import { Lines } from '@/graph/modules/Lines'
19
19
  import { Points } from '@/graph/modules/Points'
20
- import { Store, ALPHA_MIN, MAX_POINT_SIZE, type Hovered } from '@/graph/modules/Store'
20
+ import { Store, ALPHA_MIN, MAX_POINT_SIZE, MAX_HOVER_DETECTION_DELAY, type Hovered } from '@/graph/modules/Store'
21
21
  import { Zoom } from '@/graph/modules/Zoom'
22
22
  import { Drag } from '@/graph/modules/Drag'
23
- import { defaultConfigValues, defaultScaleToZoom } from '@/graph/variables'
23
+ import { defaultConfigValues, defaultScaleToZoom, defaultGreyoutPointColor, defaultBackgroundColor } from '@/graph/variables'
24
24
  import { createWebGLErrorMessage } from './graph/utils/error-message'
25
25
 
26
26
  export class Graph {
@@ -50,12 +50,12 @@ export class Graph {
50
50
 
51
51
  private currentEvent: D3ZoomEvent<HTMLCanvasElement, undefined> | D3DragEvent<HTMLCanvasElement, undefined, Hovered> | MouseEvent | undefined
52
52
  /**
53
- * The value of `_findHoveredPointExecutionCount` is incremented by 1 on each animation frame.
54
- * When the counter reaches 2 (or more), it is reset to 0 and the `findHoveredPoint` method is executed.
53
+ * The value of `_findHoveredItemExecutionCount` is incremented by 1 on each animation frame.
54
+ * When the counter reaches MAX_HOVER_DETECTION_DELAY (default 4), it is reset to 0 and the `findHoveredPoint` or `findHoveredLine` method is executed.
55
55
  */
56
- private _findHoveredPointExecutionCount = 0
56
+ private _findHoveredItemExecutionCount = 0
57
57
  /**
58
- * If the mouse is not on the Canvas, the `findHoveredPoint` method will not be executed.
58
+ * If the mouse is not on the Canvas, the `findHoveredPoint` or `findHoveredLine` method will not be executed.
59
59
  */
60
60
  private _isMouseOnCanvas = false
61
61
  /**
@@ -123,7 +123,30 @@ export class Graph {
123
123
  this.canvasD3Selection
124
124
  .on('mouseenter.cosmos', () => { this._isMouseOnCanvas = true })
125
125
  .on('mousemove.cosmos', () => { this._isMouseOnCanvas = true })
126
- .on('mouseleave.cosmos', () => { this._isMouseOnCanvas = false })
126
+ .on('mouseleave.cosmos', (event) => {
127
+ this._isMouseOnCanvas = false
128
+ this.currentEvent = event
129
+
130
+ // Clear point hover state and trigger callback if needed
131
+ if (this.store.hoveredPoint !== undefined && this.config.onPointMouseOut) {
132
+ this.config.onPointMouseOut(event)
133
+ }
134
+
135
+ // Clear link hover state and trigger callback if needed
136
+ if (this.store.hoveredLinkIndex !== undefined && this.config.onLinkMouseOut) {
137
+ this.config.onLinkMouseOut(event)
138
+ }
139
+
140
+ // Reset right-click flag
141
+ this.isRightClickMouse = false
142
+
143
+ // Clear hover states
144
+ this.store.hoveredPoint = undefined
145
+ this.store.hoveredLinkIndex = undefined
146
+
147
+ // Update cursor style after clearing hover states
148
+ this.updateCanvasCursor()
149
+ })
127
150
  select(document)
128
151
  .on('keydown.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = true })
129
152
  .on('keyup.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = false })
@@ -176,18 +199,15 @@ export class Graph {
176
199
  this.clusters = new Clusters(this.reglInstance, this.config, this.store, this.graph, this.points)
177
200
 
178
201
  this.store.backgroundColor = getRgbaColor(this.config.backgroundColor)
179
- if (this.config.hoveredPointRingColor) {
180
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor)
181
- }
182
- if (this.config.focusedPointRingColor) {
183
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor)
184
- }
202
+ this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
203
+ this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
185
204
  if (this.config.focusedPointIndex !== undefined) {
186
205
  this.store.setFocusedPoint(this.config.focusedPointIndex)
187
206
  }
188
- if (this.config.pointGreyoutColor) {
189
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor)
190
- }
207
+ this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
208
+ this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
209
+
210
+ this.store.updateLinkHoveringEnabled(this.config)
191
211
 
192
212
  if (this.config.showFPSMonitor) this.fpsMonitor = new FPSMonitor(this.canvas)
193
213
 
@@ -251,15 +271,20 @@ export class Graph {
251
271
  prevConfig.curvedLinks !== this.config.curvedLinks) {
252
272
  this.lines.updateCurveLineGeometry()
253
273
  }
254
- if (prevConfig.backgroundColor !== this.config.backgroundColor) this.store.backgroundColor = getRgbaColor(this.config.backgroundColor)
274
+ if (prevConfig.backgroundColor !== this.config.backgroundColor) {
275
+ this.store.backgroundColor = getRgbaColor(this.config.backgroundColor ?? defaultBackgroundColor)
276
+ }
255
277
  if (prevConfig.hoveredPointRingColor !== this.config.hoveredPointRingColor) {
256
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor)
278
+ this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
257
279
  }
258
280
  if (prevConfig.focusedPointRingColor !== this.config.focusedPointRingColor) {
259
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor)
281
+ this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
260
282
  }
261
283
  if (prevConfig.pointGreyoutColor !== this.config.pointGreyoutColor) {
262
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor)
284
+ this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
285
+ }
286
+ if (prevConfig.hoveredLinkColor !== this.config.hoveredLinkColor) {
287
+ this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
263
288
  }
264
289
  if (prevConfig.focusedPointIndex !== this.config.focusedPointIndex) {
265
290
  this.store.setFocusedPoint(this.config.focusedPointIndex)
@@ -285,6 +310,12 @@ export class Graph {
285
310
  if (prevConfig.enableZoom !== this.config.enableZoom || prevConfig.enableDrag !== this.config.enableDrag) {
286
311
  this.updateZoomDragBehaviors()
287
312
  }
313
+
314
+ if (prevConfig.onLinkClick !== this.config.onLinkClick ||
315
+ prevConfig.onLinkMouseOver !== this.config.onLinkMouseOver ||
316
+ prevConfig.onLinkMouseOut !== this.config.onLinkMouseOut) {
317
+ this.store.updateLinkHoveringEnabled(this.config)
318
+ }
288
319
  }
289
320
 
290
321
  /**
@@ -1065,7 +1096,8 @@ export class Graph {
1065
1096
  }
1066
1097
 
1067
1098
  /**
1068
- * Pause the simulation.
1099
+ * Pause the simulation. When paused, the simulation stops running
1100
+ * and can be resumed using the unpause method.
1069
1101
  */
1070
1102
  public pause (): void {
1071
1103
  if (this._isDestroyed) return
@@ -1074,7 +1106,19 @@ export class Graph {
1074
1106
  }
1075
1107
 
1076
1108
  /**
1077
- * Restart the simulation.
1109
+ * Unpause the simulation. This method resumes a paused
1110
+ * simulation and continues its execution.
1111
+ */
1112
+ public unpause (): void {
1113
+ if (this._isDestroyed) return
1114
+ this.store.isSimulationRunning = true
1115
+ this.config.onSimulationUnpause?.()
1116
+ }
1117
+
1118
+ /**
1119
+ * Restart/Resume the simulation. This method unpauses a paused
1120
+ * simulation and resumes its execution.
1121
+ * @deprecated Use `unpause()` instead. This method will be removed in a future version.
1078
1122
  */
1079
1123
  public restart (): void {
1080
1124
  if (this._isDestroyed) return
@@ -1245,6 +1289,7 @@ export class Graph {
1245
1289
  }
1246
1290
 
1247
1291
  private frame (): void {
1292
+ if (this._isDestroyed) return
1248
1293
  const { config: { simulationGravity, simulationCenter, renderLinks, enableSimulation }, store: { alpha, isSimulationRunning } } = this
1249
1294
  if (alpha < ALPHA_MIN && isSimulationRunning) this.end()
1250
1295
  if (!this.store.pointsTextureSize) return
@@ -1252,7 +1297,9 @@ export class Graph {
1252
1297
  this.requestAnimationFrameId = window.requestAnimationFrame((now) => {
1253
1298
  this.fpsMonitor?.begin()
1254
1299
  this.resizeCanvas()
1255
- if (!this.dragInstance.isActive) this.findHoveredPoint()
1300
+ if (!this.dragInstance.isActive) {
1301
+ this.findHoveredItem()
1302
+ }
1256
1303
 
1257
1304
  if (enableSimulation) {
1258
1305
  if (this.isRightClickMouse && this.config.enableRightClickRepulsion) {
@@ -1318,7 +1365,9 @@ export class Graph {
1318
1365
  this.fpsMonitor?.end(now)
1319
1366
 
1320
1367
  this.currentEvent = undefined
1321
- this.frame()
1368
+ if (!this._isDestroyed) {
1369
+ this.frame()
1370
+ }
1322
1371
  })
1323
1372
  }
1324
1373
 
@@ -1338,6 +1387,23 @@ export class Graph {
1338
1387
  this.store.hoveredPoint?.position,
1339
1388
  event
1340
1389
  )
1390
+
1391
+ if (this.store.hoveredPoint) {
1392
+ this.config.onPointClick?.(
1393
+ this.store.hoveredPoint.index,
1394
+ this.store.hoveredPoint.position,
1395
+ event
1396
+ )
1397
+ } else if (this.store.hoveredLinkIndex !== undefined) {
1398
+ this.config.onLinkClick?.(
1399
+ this.store.hoveredLinkIndex,
1400
+ event
1401
+ )
1402
+ } else {
1403
+ this.config.onBackgroundClick?.(
1404
+ event
1405
+ )
1406
+ }
1341
1407
  }
1342
1408
 
1343
1409
  private updateMousePosition (event: MouseEvent | D3DragEvent<HTMLCanvasElement, undefined, Hovered>): void {
@@ -1365,6 +1431,7 @@ export class Graph {
1365
1431
  }
1366
1432
 
1367
1433
  private resizeCanvas (forceResize = false): void {
1434
+ if (this._isDestroyed) return
1368
1435
  const prevWidth = this.canvas.width
1369
1436
  const prevHeight = this.canvas.height
1370
1437
  const w = this.canvas.clientWidth
@@ -1382,6 +1449,10 @@ export class Graph {
1382
1449
  this.canvasD3Selection
1383
1450
  ?.call(this.zoomInstance.behavior.transform, this.zoomInstance.getTransform([centerPosition], k))
1384
1451
  this.points?.updateSampledPointsGrid()
1452
+ // Only update link index FBO if link hovering is enabled
1453
+ if (this.store.isLinkHoveringEnabled) {
1454
+ this.lines?.updateLinkIndexFbo()
1455
+ }
1385
1456
  }
1386
1457
  }
1387
1458
 
@@ -1413,13 +1484,31 @@ export class Graph {
1413
1484
  }
1414
1485
  }
1415
1486
 
1416
- private findHoveredPoint (): void {
1417
- if (!this._isMouseOnCanvas || !this.reglInstance || !this.points) return
1418
- if (this._findHoveredPointExecutionCount < 2) {
1419
- this._findHoveredPointExecutionCount += 1
1487
+ private findHoveredItem (): void {
1488
+ if (this._isDestroyed || !this._isMouseOnCanvas || !this.reglInstance) return
1489
+ if (this._findHoveredItemExecutionCount < MAX_HOVER_DETECTION_DELAY) {
1490
+ this._findHoveredItemExecutionCount += 1
1420
1491
  return
1421
1492
  }
1422
- this._findHoveredPointExecutionCount = 0
1493
+ this._findHoveredItemExecutionCount = 0
1494
+ this.findHoveredPoint()
1495
+
1496
+ if (this.graph.linksNumber && this.store.isLinkHoveringEnabled) {
1497
+ this.findHoveredLine()
1498
+ } else if (this.store.hoveredLinkIndex !== undefined) {
1499
+ // Clear stale hoveredLinkIndex when there are no links
1500
+ const wasHovered = this.store.hoveredLinkIndex !== undefined
1501
+ this.store.hoveredLinkIndex = undefined
1502
+ if (wasHovered && this.config.onLinkMouseOut) {
1503
+ this.config.onLinkMouseOut(this.currentEvent)
1504
+ }
1505
+ }
1506
+
1507
+ this.updateCanvasCursor()
1508
+ }
1509
+
1510
+ private findHoveredPoint (): void {
1511
+ if (this._isDestroyed || !this.reglInstance || !this.points) return
1423
1512
  this.points.findHoveredPoint()
1424
1513
  let isMouseover = false
1425
1514
  let isMouseout = false
@@ -1447,15 +1536,46 @@ export class Graph {
1447
1536
  )
1448
1537
  }
1449
1538
  if (isMouseout) this.config.onPointMouseOut?.(this.currentEvent)
1450
- this.updateCanvasCursor()
1539
+ }
1540
+
1541
+ private findHoveredLine (): void {
1542
+ if (this._isDestroyed || !this.reglInstance || !this.lines) return
1543
+ if (this.store.hoveredPoint) {
1544
+ if (this.store.hoveredLinkIndex !== undefined) {
1545
+ this.store.hoveredLinkIndex = undefined
1546
+ this.config.onLinkMouseOut?.(this.currentEvent)
1547
+ }
1548
+ return
1549
+ }
1550
+ this.lines.findHoveredLine()
1551
+ let isMouseover = false
1552
+ let isMouseout = false
1553
+
1554
+ const pixels = readPixels(this.reglInstance, this.lines.hoveredLineIndexFbo as regl.Framebuffer2D)
1555
+ const hoveredLineIndex = pixels[0] as number
1556
+
1557
+ if (hoveredLineIndex >= 0) {
1558
+ if (this.store.hoveredLinkIndex !== hoveredLineIndex) isMouseover = true
1559
+ this.store.hoveredLinkIndex = hoveredLineIndex
1560
+ } else {
1561
+ if (this.store.hoveredLinkIndex !== undefined) isMouseout = true
1562
+ this.store.hoveredLinkIndex = undefined
1563
+ }
1564
+
1565
+ if (isMouseover && this.store.hoveredLinkIndex !== undefined) {
1566
+ this.config.onLinkMouseOver?.(this.store.hoveredLinkIndex)
1567
+ }
1568
+ if (isMouseout) this.config.onLinkMouseOut?.(this.currentEvent)
1451
1569
  }
1452
1570
 
1453
1571
  private updateCanvasCursor (): void {
1454
- const { hoveredPointCursor } = this.config
1572
+ const { hoveredPointCursor, hoveredLinkCursor } = this.config
1455
1573
  if (this.dragInstance.isActive) select(this.canvas).style('cursor', 'grabbing')
1456
1574
  else if (this.store.hoveredPoint) {
1457
1575
  if (!this.config.enableDrag || this.store.isSpaceKeyPressed) select(this.canvas).style('cursor', hoveredPointCursor)
1458
1576
  else select(this.canvas).style('cursor', 'grab')
1577
+ } else if (this.store.isLinkHoveringEnabled && this.store.hoveredLinkIndex !== undefined) {
1578
+ select(this.canvas).style('cursor', hoveredLinkCursor)
1459
1579
  } else select(this.canvas).style('cursor', null)
1460
1580
  }
1461
1581
 
@@ -6,6 +6,10 @@ varying float arrowLength;
6
6
  varying float useArrow;
7
7
  varying float smoothing;
8
8
  varying float arrowWidthFactor;
9
+ varying float linkIndex;
10
+
11
+ // renderMode: 0.0 = normal rendering, 1.0 = index buffer rendering for picking
12
+ uniform float renderMode;
9
13
 
10
14
  float map(float value, float min1, float max1, float min2, float max2) {
11
15
  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
@@ -31,5 +35,12 @@ void main() {
31
35
  opacity = linkOpacity;
32
36
  } else opacity = rgbaColor.a * smoothstep(0.5, 0.5 - smoothing, abs(pos.y));
33
37
 
34
- gl_FragColor = vec4(color, opacity);
38
+ if (renderMode > 0.0) {
39
+ if (opacity > 0.0) {
40
+ gl_FragColor = vec4(linkIndex, 0.0, 0.0, 1.0);
41
+ } else {
42
+ gl_FragColor = vec4(-1.0, 0.0, 0.0, 0.0);
43
+ }
44
+ } else gl_FragColor = vec4(color, opacity);
45
+
35
46
  }