@cosmos.gl/graph 2.3.1 → 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 (60) hide show
  1. package/.eslintrc +61 -0
  2. package/CHARTER.md +69 -0
  3. package/GOVERNANCE.md +21 -0
  4. package/dist/config.d.ts +69 -0
  5. package/dist/index.d.ts +62 -21
  6. package/dist/index.js +5672 -5188
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.min.js +272 -86
  9. package/dist/index.min.js.map +1 -1
  10. package/dist/modules/GraphData/index.d.ts +18 -2
  11. package/dist/modules/Lines/index.d.ts +8 -0
  12. package/dist/modules/Points/atlas-utils.d.ts +24 -0
  13. package/dist/modules/Points/index.d.ts +21 -2
  14. package/dist/modules/Store/index.d.ts +20 -3
  15. package/dist/modules/core-module.d.ts +1 -0
  16. package/dist/stories/beginners/link-hovering/data-generator.d.ts +19 -0
  17. package/dist/stories/beginners/link-hovering/index.d.ts +5 -0
  18. package/dist/stories/beginners.stories.d.ts +1 -0
  19. package/dist/stories/create-story.d.ts +5 -1
  20. package/dist/stories/shapes/image-example/index.d.ts +5 -0
  21. package/dist/stories/shapes.stories.d.ts +1 -0
  22. package/dist/variables.d.ts +5 -2
  23. package/package.json +4 -4
  24. package/src/config.ts +87 -2
  25. package/src/declaration.d.ts +5 -0
  26. package/src/index.ts +270 -98
  27. package/src/modules/GraphData/index.ts +68 -6
  28. package/src/modules/Lines/draw-curve-line.frag +12 -1
  29. package/src/modules/Lines/draw-curve-line.vert +29 -2
  30. package/src/modules/Lines/hovered-line-index.frag +27 -0
  31. package/src/modules/Lines/hovered-line-index.vert +8 -0
  32. package/src/modules/Lines/index.ts +112 -2
  33. package/src/modules/Points/atlas-utils.ts +137 -0
  34. package/src/modules/Points/draw-highlighted.vert +3 -3
  35. package/src/modules/Points/draw-points.frag +106 -14
  36. package/src/modules/Points/draw-points.vert +51 -25
  37. package/src/modules/Points/find-points-on-area-selection.frag +6 -5
  38. package/src/modules/Points/index.ts +121 -13
  39. package/src/modules/Store/index.ts +44 -5
  40. package/src/modules/core-module.ts +1 -0
  41. package/src/stories/1. welcome.mdx +2 -1
  42. package/src/stories/2. configuration.mdx +10 -1
  43. package/src/stories/3. api-reference.mdx +61 -5
  44. package/src/stories/beginners/basic-set-up/index.ts +20 -10
  45. package/src/stories/beginners/link-hovering/data-generator.ts +198 -0
  46. package/src/stories/beginners/link-hovering/index.ts +61 -0
  47. package/src/stories/beginners/link-hovering/style.css +73 -0
  48. package/src/stories/beginners/quick-start.ts +2 -1
  49. package/src/stories/beginners/remove-points/index.ts +28 -30
  50. package/src/stories/beginners.stories.ts +17 -0
  51. package/src/stories/clusters/polygon-selection/index.ts +2 -4
  52. package/src/stories/create-story.ts +32 -5
  53. package/src/stories/shapes/image-example/icons/box.png +0 -0
  54. package/src/stories/shapes/image-example/icons/lego.png +0 -0
  55. package/src/stories/shapes/image-example/icons/s.png +0 -0
  56. package/src/stories/shapes/image-example/icons/swift.png +0 -0
  57. package/src/stories/shapes/image-example/icons/toolbox.png +0 -0
  58. package/src/stories/shapes/image-example/index.ts +238 -0
  59. package/src/stories/shapes.stories.ts +12 -0
  60. package/src/variables.ts +5 -2
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
  /**
@@ -64,18 +64,20 @@ export class Graph {
64
64
  private _isFirstRenderAfterInit = true
65
65
  private _fitViewOnInitTimeoutID: number | undefined
66
66
 
67
- private _needsPointPositionsUpdate = false
68
- private _needsPointColorUpdate = false
69
- private _needsPointSizeUpdate = false
70
- private _needsPointShapeUpdate = false
71
- private _needsLinksUpdate = false
72
- private _needsLinkColorUpdate = false
73
- private _needsLinkWidthUpdate = false
74
- private _needsLinkArrowUpdate = false
75
- private _needsPointClusterUpdate = false
76
- private _needsForceManyBodyUpdate = false
77
- private _needsForceLinkUpdate = false
78
- private _needsForceCenterUpdate = false
67
+ private isPointPositionsUpdateNeeded = false
68
+ private isPointColorUpdateNeeded = false
69
+ private isPointSizeUpdateNeeded = false
70
+ private isPointShapeUpdateNeeded = false
71
+ private isPointImageIndicesUpdateNeeded = false
72
+ private isLinksUpdateNeeded = false
73
+ private isLinkColorUpdateNeeded = false
74
+ private isLinkWidthUpdateNeeded = false
75
+ private isLinkArrowUpdateNeeded = false
76
+ private isPointClusterUpdateNeeded = false
77
+ private isForceManyBodyUpdateNeeded = false
78
+ private isForceLinkUpdateNeeded = false
79
+ private isForceCenterUpdateNeeded = false
80
+ private isPointImageSizesUpdateNeeded = false
79
81
 
80
82
  private _isDestroyed = false
81
83
 
@@ -114,13 +116,37 @@ export class Graph {
114
116
  this.reglInstance = reglInstance
115
117
 
116
118
  this.store.adjustSpaceSize(this.config.spaceSize, this.reglInstance.limits.maxTextureSize)
119
+ this.store.setWebGLMaxTextureSize(this.reglInstance.limits.maxTextureSize)
117
120
  this.store.updateScreenSize(w, h)
118
121
 
119
122
  this.canvasD3Selection = select<HTMLCanvasElement, undefined>(this.canvas)
120
123
  this.canvasD3Selection
121
124
  .on('mouseenter.cosmos', () => { this._isMouseOnCanvas = true })
122
125
  .on('mousemove.cosmos', () => { this._isMouseOnCanvas = true })
123
- .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
+ })
124
150
  select(document)
125
151
  .on('keydown.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = true })
126
152
  .on('keyup.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = false })
@@ -173,18 +199,15 @@ export class Graph {
173
199
  this.clusters = new Clusters(this.reglInstance, this.config, this.store, this.graph, this.points)
174
200
 
175
201
  this.store.backgroundColor = getRgbaColor(this.config.backgroundColor)
176
- if (this.config.hoveredPointRingColor) {
177
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor)
178
- }
179
- if (this.config.focusedPointRingColor) {
180
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor)
181
- }
202
+ this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
203
+ this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
182
204
  if (this.config.focusedPointIndex !== undefined) {
183
205
  this.store.setFocusedPoint(this.config.focusedPointIndex)
184
206
  }
185
- if (this.config.pointGreyoutColor) {
186
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor)
187
- }
207
+ this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
208
+ this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
209
+
210
+ this.store.updateLinkHoveringEnabled(this.config)
188
211
 
189
212
  if (this.config.showFPSMonitor) this.fpsMonitor = new FPSMonitor(this.canvas)
190
213
 
@@ -248,15 +271,20 @@ export class Graph {
248
271
  prevConfig.curvedLinks !== this.config.curvedLinks) {
249
272
  this.lines.updateCurveLineGeometry()
250
273
  }
251
- 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
+ }
252
277
  if (prevConfig.hoveredPointRingColor !== this.config.hoveredPointRingColor) {
253
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor)
278
+ this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
254
279
  }
255
280
  if (prevConfig.focusedPointRingColor !== this.config.focusedPointRingColor) {
256
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor)
281
+ this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
257
282
  }
258
283
  if (prevConfig.pointGreyoutColor !== this.config.pointGreyoutColor) {
259
- 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)
260
288
  }
261
289
  if (prevConfig.focusedPointIndex !== this.config.focusedPointIndex) {
262
290
  this.store.setFocusedPoint(this.config.focusedPointIndex)
@@ -282,6 +310,12 @@ export class Graph {
282
310
  if (prevConfig.enableZoom !== this.config.enableZoom || prevConfig.enableDrag !== this.config.enableDrag) {
283
311
  this.updateZoomDragBehaviors()
284
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
+ }
285
319
  }
286
320
 
287
321
  /**
@@ -297,18 +331,19 @@ export class Graph {
297
331
  public setPointPositions (pointPositions: Float32Array, dontRescale?: boolean | undefined): void {
298
332
  if (this._isDestroyed || !this.points) return
299
333
  this.graph.inputPointPositions = pointPositions
300
- this.points.dontRescale = dontRescale
301
- this._needsPointPositionsUpdate = true
334
+ this.points.shouldSkipRescale = dontRescale
335
+ this.isPointPositionsUpdateNeeded = true
302
336
  // Links related texture depends on point positions, so we need to update it
303
- this._needsLinksUpdate = true
337
+ this.isLinksUpdateNeeded = true
304
338
  // Point related textures depend on point positions length, so we need to update them
305
- this._needsPointColorUpdate = true
306
- this._needsPointSizeUpdate = true
307
- this._needsPointShapeUpdate = true
308
- this._needsPointClusterUpdate = true
309
- this._needsForceManyBodyUpdate = true
310
- this._needsForceLinkUpdate = true
311
- this._needsForceCenterUpdate = true
339
+ this.isPointColorUpdateNeeded = true
340
+ this.isPointSizeUpdateNeeded = true
341
+ this.isPointShapeUpdateNeeded = true
342
+ this.isPointImageIndicesUpdateNeeded = true
343
+ this.isPointClusterUpdateNeeded = true
344
+ this.isForceManyBodyUpdateNeeded = true
345
+ this.isForceLinkUpdateNeeded = true
346
+ this.isForceCenterUpdateNeeded = true
312
347
  }
313
348
 
314
349
  /**
@@ -321,7 +356,7 @@ export class Graph {
321
356
  public setPointColors (pointColors: Float32Array): void {
322
357
  if (this._isDestroyed) return
323
358
  this.graph.inputPointColors = pointColors
324
- this._needsPointColorUpdate = true
359
+ this.isPointColorUpdateNeeded = true
325
360
  }
326
361
 
327
362
  /**
@@ -345,7 +380,7 @@ export class Graph {
345
380
  public setPointSizes (pointSizes: Float32Array): void {
346
381
  if (this._isDestroyed) return
347
382
  this.graph.inputPointSizes = pointSizes
348
- this._needsPointSizeUpdate = true
383
+ this.isPointSizeUpdateNeeded = true
349
384
  }
350
385
 
351
386
  /**
@@ -353,13 +388,55 @@ export class Graph {
353
388
  *
354
389
  * @param {Float32Array} pointShapes - A Float32Array representing the shapes of points in the format [shape1, shape2, ..., shapen],
355
390
  * where `n` is the index of the point and each shape value corresponds to a PointShape enum:
356
- * 0 = Circle, 1 = Square, 2 = Triangle, 3 = Diamond, 4 = Pentagon, 5 = Hexagon, 6 = Star, 7 = Cross.
391
+ * 0 = Circle, 1 = Square, 2 = Triangle, 3 = Diamond, 4 = Pentagon, 5 = Hexagon, 6 = Star, 7 = Cross, 8 = None.
357
392
  * Example: `new Float32Array([0, 1, 2])` sets the first point to Circle, the second point to Square, and the third point to Triangle.
393
+ * Images are rendered above shapes.
358
394
  */
359
395
  public setPointShapes (pointShapes: Float32Array): void {
360
396
  if (this._isDestroyed) return
361
397
  this.graph.inputPointShapes = pointShapes
362
- this._needsPointShapeUpdate = true
398
+ this.isPointShapeUpdateNeeded = true
399
+ }
400
+
401
+ /**
402
+ * Sets the images for the graph points using ImageData objects.
403
+ * Images are rendered above shapes.
404
+ * To use images, provide image indices via setPointImageIndices().
405
+ *
406
+ * @param {ImageData[]} imageDataArray - Array of ImageData objects to use as point images.
407
+ * Example: `setImageData([imageData1, imageData2, imageData3])`
408
+ */
409
+ public setImageData (imageDataArray: ImageData[]): void {
410
+ if (this._isDestroyed || !this.points) return
411
+ this.graph.inputImageData = imageDataArray
412
+ this.points.createAtlas()
413
+ }
414
+
415
+ /**
416
+ * Sets which image each point should use from the images array.
417
+ * Images are rendered above shapes.
418
+ *
419
+ * @param {Float32Array} imageIndices - A Float32Array representing which image each point uses in the format [index1, index2, ..., indexn],
420
+ * where `n` is the index of the point and each value is an index into the images array provided to `setImageData`.
421
+ * Example: `new Float32Array([0, 1, 0])` sets the first point to use image 0, second point to use image 1, third point to use image 0.
422
+ */
423
+ public setPointImageIndices (imageIndices: Float32Array): void {
424
+ if (this._isDestroyed) return
425
+ this.graph.inputPointImageIndices = imageIndices
426
+ this.isPointImageIndicesUpdateNeeded = true
427
+ }
428
+
429
+ /**
430
+ * Sets the sizes for the point images.
431
+ *
432
+ * @param {Float32Array} imageSizes - A Float32Array representing the sizes of point images in the format [size1, size2, ..., sizen],
433
+ * where `n` is the index of the point.
434
+ * Example: `new Float32Array([10, 20, 30])` sets the first image to size 10, the second image to size 20, and the third image to size 30.
435
+ */
436
+ public setPointImageSizes (imageSizes: Float32Array): void {
437
+ if (this._isDestroyed) return
438
+ this.graph.inputPointImageSizes = imageSizes
439
+ this.isPointImageSizesUpdateNeeded = true
363
440
  }
364
441
 
365
442
  /**
@@ -384,12 +461,12 @@ export class Graph {
384
461
  public setLinks (links: Float32Array): void {
385
462
  if (this._isDestroyed) return
386
463
  this.graph.inputLinks = links
387
- this._needsLinksUpdate = true
464
+ this.isLinksUpdateNeeded = true
388
465
  // Links related texture depends on links length, so we need to update it
389
- this._needsLinkColorUpdate = true
390
- this._needsLinkWidthUpdate = true
391
- this._needsLinkArrowUpdate = true
392
- this._needsForceLinkUpdate = true
466
+ this.isLinkColorUpdateNeeded = true
467
+ this.isLinkWidthUpdateNeeded = true
468
+ this.isLinkArrowUpdateNeeded = true
469
+ this.isForceLinkUpdateNeeded = true
393
470
  }
394
471
 
395
472
  /**
@@ -402,7 +479,7 @@ export class Graph {
402
479
  public setLinkColors (linkColors: Float32Array): void {
403
480
  if (this._isDestroyed) return
404
481
  this.graph.inputLinkColors = linkColors
405
- this._needsLinkColorUpdate = true
482
+ this.isLinkColorUpdateNeeded = true
406
483
  }
407
484
 
408
485
  /**
@@ -426,7 +503,7 @@ export class Graph {
426
503
  public setLinkWidths (linkWidths: Float32Array): void {
427
504
  if (this._isDestroyed) return
428
505
  this.graph.inputLinkWidths = linkWidths
429
- this._needsLinkWidthUpdate = true
506
+ this.isLinkWidthUpdateNeeded = true
430
507
  }
431
508
 
432
509
  /**
@@ -450,7 +527,7 @@ export class Graph {
450
527
  public setLinkArrows (linkArrows: boolean[]): void {
451
528
  if (this._isDestroyed) return
452
529
  this.graph.linkArrowsBoolean = linkArrows
453
- this._needsLinkArrowUpdate = true
530
+ this.isLinkArrowUpdateNeeded = true
454
531
  }
455
532
 
456
533
  /**
@@ -463,7 +540,7 @@ export class Graph {
463
540
  public setLinkStrength (linkStrength: Float32Array): void {
464
541
  if (this._isDestroyed) return
465
542
  this.graph.inputLinkStrength = linkStrength
466
- this._needsForceLinkUpdate = true
543
+ this.isForceLinkUpdateNeeded = true
467
544
  }
468
545
 
469
546
  /**
@@ -480,7 +557,7 @@ export class Graph {
480
557
  public setPointClusters (pointClusters: (number | undefined)[]): void {
481
558
  if (this._isDestroyed) return
482
559
  this.graph.inputPointClusters = pointClusters
483
- this._needsPointClusterUpdate = true
560
+ this.isPointClusterUpdateNeeded = true
484
561
  }
485
562
 
486
563
  /**
@@ -496,7 +573,7 @@ export class Graph {
496
573
  public setClusterPositions (clusterPositions: (number | undefined)[]): void {
497
574
  if (this._isDestroyed) return
498
575
  this.graph.inputClusterPositions = clusterPositions
499
- this._needsPointClusterUpdate = true
576
+ this.isPointClusterUpdateNeeded = true
500
577
  }
501
578
 
502
579
  /**
@@ -512,7 +589,7 @@ export class Graph {
512
589
  public setPointClusterStrength (clusterStrength: Float32Array): void {
513
590
  if (this._isDestroyed) return
514
591
  this.graph.inputClusterStrength = clusterStrength
515
- this._needsPointClusterUpdate = true
592
+ this.isPointClusterUpdateNeeded = true
516
593
  }
517
594
 
518
595
  /**
@@ -936,9 +1013,11 @@ export class Graph {
936
1013
 
937
1014
  /**
938
1015
  * Get current X and Y coordinates of the tracked points.
939
- * @returns A Map object where keys are the indices of the points and values are their corresponding X and Y coordinates in the [number, number] format.
1016
+ * Do not mutate the returned map - it may affect future calls.
1017
+ * @returns A ReadonlyMap where keys are point indices and values are their corresponding X and Y coordinates in the [number, number] format.
1018
+ * @see trackPointPositionsByIndices To set which points should be tracked
940
1019
  */
941
- public getTrackedPointPositionsMap (): Map<number, [number, number]> {
1020
+ public getTrackedPointPositionsMap (): ReadonlyMap<number, [number, number]> {
942
1021
  if (this._isDestroyed || !this.points) return new Map()
943
1022
  return this.points.getTrackedPositionsMap()
944
1023
  }
@@ -1017,7 +1096,8 @@ export class Graph {
1017
1096
  }
1018
1097
 
1019
1098
  /**
1020
- * Pause the simulation.
1099
+ * Pause the simulation. When paused, the simulation stops running
1100
+ * and can be resumed using the unpause method.
1021
1101
  */
1022
1102
  public pause (): void {
1023
1103
  if (this._isDestroyed) return
@@ -1026,7 +1106,19 @@ export class Graph {
1026
1106
  }
1027
1107
 
1028
1108
  /**
1029
- * 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.
1030
1122
  */
1031
1123
  public restart (): void {
1032
1124
  if (this._isDestroyed) return
@@ -1114,36 +1206,40 @@ export class Graph {
1114
1206
  */
1115
1207
  public create (): void {
1116
1208
  if (this._isDestroyed || !this.points || !this.lines) return
1117
- if (this._needsPointPositionsUpdate) this.points.updatePositions()
1118
- if (this._needsPointColorUpdate) this.points.updateColor()
1119
- if (this._needsPointSizeUpdate) this.points.updateSize()
1120
- if (this._needsPointShapeUpdate) this.points.updateShape()
1121
-
1122
- if (this._needsLinksUpdate) this.lines.updatePointsBuffer()
1123
- if (this._needsLinkColorUpdate) this.lines.updateColor()
1124
- if (this._needsLinkWidthUpdate) this.lines.updateWidth()
1125
- if (this._needsLinkArrowUpdate) this.lines.updateArrow()
1126
-
1127
- if (this._needsForceManyBodyUpdate) this.forceManyBody?.create()
1128
- if (this._needsForceLinkUpdate) {
1209
+ if (this.isPointPositionsUpdateNeeded) this.points.updatePositions()
1210
+ if (this.isPointColorUpdateNeeded) this.points.updateColor()
1211
+ if (this.isPointSizeUpdateNeeded) this.points.updateSize()
1212
+ if (this.isPointShapeUpdateNeeded) this.points.updateShape()
1213
+ if (this.isPointImageIndicesUpdateNeeded) this.points.updateImageIndices()
1214
+ if (this.isPointImageSizesUpdateNeeded) this.points.updateImageSizes()
1215
+
1216
+ if (this.isLinksUpdateNeeded) this.lines.updatePointsBuffer()
1217
+ if (this.isLinkColorUpdateNeeded) this.lines.updateColor()
1218
+ if (this.isLinkWidthUpdateNeeded) this.lines.updateWidth()
1219
+ if (this.isLinkArrowUpdateNeeded) this.lines.updateArrow()
1220
+
1221
+ if (this.isForceManyBodyUpdateNeeded) this.forceManyBody?.create()
1222
+ if (this.isForceLinkUpdateNeeded) {
1129
1223
  this.forceLinkIncoming?.create(LinkDirection.INCOMING)
1130
1224
  this.forceLinkOutgoing?.create(LinkDirection.OUTGOING)
1131
1225
  }
1132
- if (this._needsForceCenterUpdate) this.forceCenter?.create()
1133
- if (this._needsPointClusterUpdate) this.clusters?.create()
1134
-
1135
- this._needsPointPositionsUpdate = false
1136
- this._needsPointColorUpdate = false
1137
- this._needsPointSizeUpdate = false
1138
- this._needsPointShapeUpdate = false
1139
- this._needsLinksUpdate = false
1140
- this._needsLinkColorUpdate = false
1141
- this._needsLinkWidthUpdate = false
1142
- this._needsLinkArrowUpdate = false
1143
- this._needsPointClusterUpdate = false
1144
- this._needsForceManyBodyUpdate = false
1145
- this._needsForceLinkUpdate = false
1146
- this._needsForceCenterUpdate = false
1226
+ if (this.isForceCenterUpdateNeeded) this.forceCenter?.create()
1227
+ if (this.isPointClusterUpdateNeeded) this.clusters?.create()
1228
+
1229
+ this.isPointPositionsUpdateNeeded = false
1230
+ this.isPointColorUpdateNeeded = false
1231
+ this.isPointSizeUpdateNeeded = false
1232
+ this.isPointShapeUpdateNeeded = false
1233
+ this.isPointImageIndicesUpdateNeeded = false
1234
+ this.isPointImageSizesUpdateNeeded = false
1235
+ this.isLinksUpdateNeeded = false
1236
+ this.isLinkColorUpdateNeeded = false
1237
+ this.isLinkWidthUpdateNeeded = false
1238
+ this.isLinkArrowUpdateNeeded = false
1239
+ this.isPointClusterUpdateNeeded = false
1240
+ this.isForceManyBodyUpdateNeeded = false
1241
+ this.isForceLinkUpdateNeeded = false
1242
+ this.isForceCenterUpdateNeeded = false
1147
1243
  }
1148
1244
 
1149
1245
  /**
@@ -1193,6 +1289,7 @@ export class Graph {
1193
1289
  }
1194
1290
 
1195
1291
  private frame (): void {
1292
+ if (this._isDestroyed) return
1196
1293
  const { config: { simulationGravity, simulationCenter, renderLinks, enableSimulation }, store: { alpha, isSimulationRunning } } = this
1197
1294
  if (alpha < ALPHA_MIN && isSimulationRunning) this.end()
1198
1295
  if (!this.store.pointsTextureSize) return
@@ -1200,7 +1297,9 @@ export class Graph {
1200
1297
  this.requestAnimationFrameId = window.requestAnimationFrame((now) => {
1201
1298
  this.fpsMonitor?.begin()
1202
1299
  this.resizeCanvas()
1203
- if (!this.dragInstance.isActive) this.findHoveredPoint()
1300
+ if (!this.dragInstance.isActive) {
1301
+ this.findHoveredItem()
1302
+ }
1204
1303
 
1205
1304
  if (enableSimulation) {
1206
1305
  if (this.isRightClickMouse && this.config.enableRightClickRepulsion) {
@@ -1266,7 +1365,9 @@ export class Graph {
1266
1365
  this.fpsMonitor?.end(now)
1267
1366
 
1268
1367
  this.currentEvent = undefined
1269
- this.frame()
1368
+ if (!this._isDestroyed) {
1369
+ this.frame()
1370
+ }
1270
1371
  })
1271
1372
  }
1272
1373
 
@@ -1286,6 +1387,23 @@ export class Graph {
1286
1387
  this.store.hoveredPoint?.position,
1287
1388
  event
1288
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
+ }
1289
1407
  }
1290
1408
 
1291
1409
  private updateMousePosition (event: MouseEvent | D3DragEvent<HTMLCanvasElement, undefined, Hovered>): void {
@@ -1313,6 +1431,7 @@ export class Graph {
1313
1431
  }
1314
1432
 
1315
1433
  private resizeCanvas (forceResize = false): void {
1434
+ if (this._isDestroyed) return
1316
1435
  const prevWidth = this.canvas.width
1317
1436
  const prevHeight = this.canvas.height
1318
1437
  const w = this.canvas.clientWidth
@@ -1330,6 +1449,10 @@ export class Graph {
1330
1449
  this.canvasD3Selection
1331
1450
  ?.call(this.zoomInstance.behavior.transform, this.zoomInstance.getTransform([centerPosition], k))
1332
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
+ }
1333
1456
  }
1334
1457
  }
1335
1458
 
@@ -1361,13 +1484,31 @@ export class Graph {
1361
1484
  }
1362
1485
  }
1363
1486
 
1364
- private findHoveredPoint (): void {
1365
- if (!this._isMouseOnCanvas || !this.reglInstance || !this.points) return
1366
- if (this._findHoveredPointExecutionCount < 2) {
1367
- 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
1368
1491
  return
1369
1492
  }
1370
- 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
1371
1512
  this.points.findHoveredPoint()
1372
1513
  let isMouseover = false
1373
1514
  let isMouseout = false
@@ -1395,15 +1536,46 @@ export class Graph {
1395
1536
  )
1396
1537
  }
1397
1538
  if (isMouseout) this.config.onPointMouseOut?.(this.currentEvent)
1398
- 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)
1399
1569
  }
1400
1570
 
1401
1571
  private updateCanvasCursor (): void {
1402
- const { hoveredPointCursor } = this.config
1572
+ const { hoveredPointCursor, hoveredLinkCursor } = this.config
1403
1573
  if (this.dragInstance.isActive) select(this.canvas).style('cursor', 'grabbing')
1404
1574
  else if (this.store.hoveredPoint) {
1405
1575
  if (!this.config.enableDrag || this.store.isSpaceKeyPressed) select(this.canvas).style('cursor', hoveredPointCursor)
1406
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)
1407
1579
  } else select(this.canvas).style('cursor', null)
1408
1580
  }
1409
1581