@cosmos.gl/graph 2.6.0 → 2.6.2-rc.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 (169) hide show
  1. package/dist/config.d.ts +3 -0
  2. package/dist/index.d.ts +48 -6
  3. package/dist/index.js +1346 -1289
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +15 -15
  6. package/dist/index.min.js.map +1 -1
  7. package/package.json +5 -1
  8. package/.eslintrc +0 -147
  9. package/.github/SECURITY.md +0 -19
  10. package/.github/dco.yml +0 -4
  11. package/.github/workflows/github_pages.yml +0 -54
  12. package/.storybook/main.ts +0 -26
  13. package/.storybook/manager-head.html +0 -1
  14. package/.storybook/manager.ts +0 -14
  15. package/.storybook/preview.ts +0 -29
  16. package/.storybook/style.css +0 -3
  17. package/CHARTER.md +0 -69
  18. package/CODE_OF_CONDUCT.md +0 -178
  19. package/CONTRIBUTING.md +0 -22
  20. package/GOVERNANCE.md +0 -21
  21. package/cosmos-2-0-migration-notes.md +0 -98
  22. package/cosmos_awesome.md +0 -96
  23. package/dist/stories/beginners/basic-set-up/data-gen.d.ts +0 -4
  24. package/dist/stories/beginners/basic-set-up/index.d.ts +0 -5
  25. package/dist/stories/beginners/link-hovering/data-generator.d.ts +0 -19
  26. package/dist/stories/beginners/link-hovering/index.d.ts +0 -5
  27. package/dist/stories/beginners/pinned-points/data-gen.d.ts +0 -5
  28. package/dist/stories/beginners/pinned-points/index.d.ts +0 -5
  29. package/dist/stories/beginners/point-labels/data.d.ts +0 -13
  30. package/dist/stories/beginners/point-labels/index.d.ts +0 -9
  31. package/dist/stories/beginners/point-labels/labels.d.ts +0 -8
  32. package/dist/stories/beginners/quick-start.d.ts +0 -5
  33. package/dist/stories/beginners/remove-points/config.d.ts +0 -2
  34. package/dist/stories/beginners/remove-points/data-gen.d.ts +0 -4
  35. package/dist/stories/beginners/remove-points/index.d.ts +0 -5
  36. package/dist/stories/beginners.stories.d.ts +0 -11
  37. package/dist/stories/clusters/polygon-selection/index.d.ts +0 -6
  38. package/dist/stories/clusters/polygon-selection/polygon.d.ts +0 -20
  39. package/dist/stories/clusters/radial.d.ts +0 -5
  40. package/dist/stories/clusters/with-labels.d.ts +0 -6
  41. package/dist/stories/clusters/worm.d.ts +0 -5
  42. package/dist/stories/clusters.stories.d.ts +0 -9
  43. package/dist/stories/create-cluster-labels.d.ts +0 -4
  44. package/dist/stories/create-cosmos.d.ts +0 -16
  45. package/dist/stories/create-story.d.ts +0 -16
  46. package/dist/stories/experiments/full-mesh.d.ts +0 -5
  47. package/dist/stories/experiments/mesh-with-holes.d.ts +0 -5
  48. package/dist/stories/experiments.stories.d.ts +0 -7
  49. package/dist/stories/generate-mesh-data.d.ts +0 -12
  50. package/dist/stories/geospatial/moscow-metro-stations/index.d.ts +0 -15
  51. package/dist/stories/geospatial/moscow-metro-stations/moscow-metro-coords.d.ts +0 -1
  52. package/dist/stories/geospatial/moscow-metro-stations/point-colors.d.ts +0 -1
  53. package/dist/stories/geospatial.stories.d.ts +0 -6
  54. package/dist/stories/shapes/all-shapes/index.d.ts +0 -5
  55. package/dist/stories/shapes/image-example/index.d.ts +0 -5
  56. package/dist/stories/shapes.stories.d.ts +0 -7
  57. package/logo.svg +0 -3
  58. package/rollup.config.js +0 -70
  59. package/src/config.ts +0 -734
  60. package/src/declaration.d.ts +0 -12
  61. package/src/graph/utils/error-message.ts +0 -23
  62. package/src/helper.ts +0 -74
  63. package/src/index.ts +0 -1635
  64. package/src/modules/Clusters/calculate-centermass.frag +0 -9
  65. package/src/modules/Clusters/calculate-centermass.vert +0 -26
  66. package/src/modules/Clusters/force-cluster.frag +0 -39
  67. package/src/modules/Clusters/index.ts +0 -200
  68. package/src/modules/Drag/index.ts +0 -33
  69. package/src/modules/FPSMonitor/css.ts +0 -53
  70. package/src/modules/FPSMonitor/index.ts +0 -28
  71. package/src/modules/ForceCenter/calculate-centermass.frag +0 -9
  72. package/src/modules/ForceCenter/calculate-centermass.vert +0 -18
  73. package/src/modules/ForceCenter/force-center.frag +0 -27
  74. package/src/modules/ForceCenter/index.ts +0 -104
  75. package/src/modules/ForceGravity/force-gravity.frag +0 -27
  76. package/src/modules/ForceGravity/index.ts +0 -33
  77. package/src/modules/ForceLink/force-spring.ts +0 -73
  78. package/src/modules/ForceLink/index.ts +0 -149
  79. package/src/modules/ForceManyBody/calculate-level.frag +0 -9
  80. package/src/modules/ForceManyBody/calculate-level.vert +0 -25
  81. package/src/modules/ForceManyBody/force-centermass.frag +0 -52
  82. package/src/modules/ForceManyBody/force-level.frag +0 -121
  83. package/src/modules/ForceManyBody/index.ts +0 -223
  84. package/src/modules/ForceManyBody/quadtree-frag-shader.ts +0 -90
  85. package/src/modules/ForceManyBodyQuadtree/calculate-level.frag +0 -9
  86. package/src/modules/ForceManyBodyQuadtree/calculate-level.vert +0 -25
  87. package/src/modules/ForceManyBodyQuadtree/index.ts +0 -157
  88. package/src/modules/ForceManyBodyQuadtree/quadtree-frag-shader.ts +0 -93
  89. package/src/modules/ForceMouse/force-mouse.frag +0 -24
  90. package/src/modules/ForceMouse/index.ts +0 -32
  91. package/src/modules/GraphData/index.ts +0 -384
  92. package/src/modules/Lines/draw-curve-line.frag +0 -46
  93. package/src/modules/Lines/draw-curve-line.vert +0 -194
  94. package/src/modules/Lines/geometry.ts +0 -18
  95. package/src/modules/Lines/hovered-line-index.frag +0 -27
  96. package/src/modules/Lines/hovered-line-index.vert +0 -8
  97. package/src/modules/Lines/index.ts +0 -273
  98. package/src/modules/Points/atlas-utils.ts +0 -137
  99. package/src/modules/Points/drag-point.frag +0 -20
  100. package/src/modules/Points/draw-highlighted.frag +0 -16
  101. package/src/modules/Points/draw-highlighted.vert +0 -86
  102. package/src/modules/Points/draw-points.frag +0 -243
  103. package/src/modules/Points/draw-points.vert +0 -127
  104. package/src/modules/Points/fill-sampled-points.frag +0 -9
  105. package/src/modules/Points/fill-sampled-points.vert +0 -29
  106. package/src/modules/Points/find-hovered-point.frag +0 -9
  107. package/src/modules/Points/find-hovered-point.vert +0 -57
  108. package/src/modules/Points/find-points-on-area-selection.frag +0 -48
  109. package/src/modules/Points/find-points-on-polygon-selection.frag +0 -65
  110. package/src/modules/Points/index.ts +0 -968
  111. package/src/modules/Points/track-positions.frag +0 -18
  112. package/src/modules/Points/update-position.frag +0 -37
  113. package/src/modules/Shared/buffer.ts +0 -37
  114. package/src/modules/Shared/clear.frag +0 -7
  115. package/src/modules/Shared/quad.vert +0 -12
  116. package/src/modules/Store/index.ts +0 -173
  117. package/src/modules/Zoom/index.ts +0 -148
  118. package/src/modules/core-module.ts +0 -28
  119. package/src/stories/1. welcome.mdx +0 -81
  120. package/src/stories/2. configuration.mdx +0 -113
  121. package/src/stories/3. api-reference.mdx +0 -591
  122. package/src/stories/beginners/basic-set-up/data-gen.ts +0 -33
  123. package/src/stories/beginners/basic-set-up/index.ts +0 -163
  124. package/src/stories/beginners/basic-set-up/style.css +0 -35
  125. package/src/stories/beginners/link-hovering/data-generator.ts +0 -198
  126. package/src/stories/beginners/link-hovering/index.ts +0 -61
  127. package/src/stories/beginners/link-hovering/style.css +0 -73
  128. package/src/stories/beginners/pinned-points/data-gen.ts +0 -153
  129. package/src/stories/beginners/pinned-points/index.ts +0 -61
  130. package/src/stories/beginners/point-labels/data.ts +0 -73
  131. package/src/stories/beginners/point-labels/index.ts +0 -65
  132. package/src/stories/beginners/point-labels/labels.ts +0 -46
  133. package/src/stories/beginners/point-labels/style.css +0 -16
  134. package/src/stories/beginners/quick-start.ts +0 -50
  135. package/src/stories/beginners/remove-points/config.ts +0 -25
  136. package/src/stories/beginners/remove-points/data-gen.ts +0 -30
  137. package/src/stories/beginners/remove-points/index.ts +0 -92
  138. package/src/stories/beginners/remove-points/style.css +0 -31
  139. package/src/stories/beginners.stories.ts +0 -131
  140. package/src/stories/clusters/polygon-selection/index.ts +0 -51
  141. package/src/stories/clusters/polygon-selection/polygon.ts +0 -143
  142. package/src/stories/clusters/polygon-selection/style.css +0 -8
  143. package/src/stories/clusters/radial.ts +0 -24
  144. package/src/stories/clusters/with-labels.ts +0 -53
  145. package/src/stories/clusters/worm.ts +0 -40
  146. package/src/stories/clusters.stories.ts +0 -77
  147. package/src/stories/create-cluster-labels.ts +0 -50
  148. package/src/stories/create-cosmos.ts +0 -68
  149. package/src/stories/create-story.ts +0 -51
  150. package/src/stories/experiments/full-mesh.ts +0 -13
  151. package/src/stories/experiments/mesh-with-holes.ts +0 -13
  152. package/src/stories/experiments.stories.ts +0 -43
  153. package/src/stories/generate-mesh-data.ts +0 -125
  154. package/src/stories/geospatial/moscow-metro-stations/index.ts +0 -62
  155. package/src/stories/geospatial/moscow-metro-stations/moscow-metro-coords.ts +0 -1
  156. package/src/stories/geospatial/moscow-metro-stations/point-colors.ts +0 -46
  157. package/src/stories/geospatial/moscow-metro-stations/style.css +0 -30
  158. package/src/stories/geospatial.stories.ts +0 -30
  159. package/src/stories/shapes/all-shapes/index.ts +0 -69
  160. package/src/stories/shapes/image-example/icons/box.png +0 -0
  161. package/src/stories/shapes/image-example/icons/lego.png +0 -0
  162. package/src/stories/shapes/image-example/icons/s.png +0 -0
  163. package/src/stories/shapes/image-example/icons/swift.png +0 -0
  164. package/src/stories/shapes/image-example/icons/toolbox.png +0 -0
  165. package/src/stories/shapes/image-example/index.ts +0 -238
  166. package/src/stories/shapes.stories.ts +0 -37
  167. package/src/variables.ts +0 -68
  168. package/tsconfig.json +0 -41
  169. package/vite.config.ts +0 -54
package/src/index.ts DELETED
@@ -1,1635 +0,0 @@
1
- import { select, Selection } from 'd3-selection'
2
- import 'd3-transition'
3
- import { easeQuadInOut, easeQuadIn, easeQuadOut } from 'd3-ease'
4
- import { D3ZoomEvent } from 'd3-zoom'
5
- import { D3DragEvent } from 'd3-drag'
6
- import regl from 'regl'
7
- import { GraphConfig, GraphConfigInterface } from '@/graph/config'
8
- import { getRgbaColor, readPixels, sanitizeHtml } from '@/graph/helper'
9
- import { ForceCenter } from '@/graph/modules/ForceCenter'
10
- import { ForceGravity } from '@/graph/modules/ForceGravity'
11
- import { ForceLink, LinkDirection } from '@/graph/modules/ForceLink'
12
- import { ForceManyBody } from '@/graph/modules/ForceManyBody'
13
- import { ForceManyBodyQuadtree } from '@/graph/modules/ForceManyBodyQuadtree'
14
- import { ForceMouse } from '@/graph/modules/ForceMouse'
15
- import { Clusters } from '@/graph/modules/Clusters'
16
- import { FPSMonitor } from '@/graph/modules/FPSMonitor'
17
- import { GraphData } from '@/graph/modules/GraphData'
18
- import { Lines } from '@/graph/modules/Lines'
19
- import { Points } from '@/graph/modules/Points'
20
- import { Store, ALPHA_MIN, MAX_POINT_SIZE, MAX_HOVER_DETECTION_DELAY, type Hovered } from '@/graph/modules/Store'
21
- import { Zoom } from '@/graph/modules/Zoom'
22
- import { Drag } from '@/graph/modules/Drag'
23
- import { defaultConfigValues, defaultScaleToZoom, defaultGreyoutPointColor, defaultBackgroundColor } from '@/graph/variables'
24
- import { createWebGLErrorMessage } from './graph/utils/error-message'
25
-
26
- export class Graph {
27
- public config = new GraphConfig()
28
- public graph = new GraphData(this.config)
29
- private canvas: HTMLCanvasElement
30
- private attributionDivElement: HTMLElement | undefined
31
- private canvasD3Selection: Selection<HTMLCanvasElement, undefined, null, undefined> | undefined
32
- private reglInstance: regl.Regl | undefined
33
- private requestAnimationFrameId = 0
34
- private isRightClickMouse = false
35
-
36
- private store = new Store()
37
- private points: Points | undefined
38
- private lines: Lines | undefined
39
- private forceGravity: ForceGravity | undefined
40
- private forceCenter: ForceCenter | undefined
41
- private forceManyBody: ForceManyBody | ForceManyBodyQuadtree | undefined
42
- private forceLinkIncoming: ForceLink | undefined
43
- private forceLinkOutgoing: ForceLink | undefined
44
- private forceMouse: ForceMouse | undefined
45
- private clusters: Clusters | undefined
46
- private zoomInstance = new Zoom(this.store, this.config)
47
- private dragInstance = new Drag(this.store, this.config)
48
-
49
- private fpsMonitor: FPSMonitor | undefined
50
-
51
- private currentEvent: D3ZoomEvent<HTMLCanvasElement, undefined> | D3DragEvent<HTMLCanvasElement, undefined, Hovered> | MouseEvent | undefined
52
- /**
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
- */
56
- private _findHoveredItemExecutionCount = 0
57
- /**
58
- * If the mouse is not on the Canvas, the `findHoveredPoint` or `findHoveredLine` method will not be executed.
59
- */
60
- private _isMouseOnCanvas = false
61
- /**
62
- * After setting data and render graph at a first time, the fit logic will run
63
- * */
64
- private _isFirstRenderAfterInit = true
65
- private _fitViewOnInitTimeoutID: number | undefined
66
-
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
81
-
82
- private _isDestroyed = false
83
-
84
- public constructor (div: HTMLDivElement, config?: GraphConfigInterface) {
85
- if (config) this.config.init(config)
86
-
87
- this.store.div = div
88
- const canvas = document.createElement('canvas')
89
- canvas.style.width = '100%'
90
- canvas.style.height = '100%'
91
- this.store.div.appendChild(canvas)
92
- this.addAttribution()
93
- const w = canvas.clientWidth
94
- const h = canvas.clientHeight
95
-
96
- canvas.width = w * this.config.pixelRatio
97
- canvas.height = h * this.config.pixelRatio
98
-
99
- this.canvas = canvas
100
-
101
- let reglInstance: regl.Regl | undefined
102
- try {
103
- reglInstance = regl({
104
- canvas: this.canvas,
105
- attributes: {
106
- antialias: false,
107
- preserveDrawingBuffer: true,
108
- },
109
- extensions: ['OES_texture_float', 'ANGLE_instanced_arrays'],
110
- })
111
- } catch (e) {
112
- createWebGLErrorMessage(this.store.div)
113
- this._isDestroyed = true
114
- return
115
- }
116
- this.reglInstance = reglInstance
117
-
118
- this.store.adjustSpaceSize(this.config.spaceSize, this.reglInstance.limits.maxTextureSize)
119
- this.store.setWebGLMaxTextureSize(this.reglInstance.limits.maxTextureSize)
120
- this.store.updateScreenSize(w, h)
121
-
122
- this.canvasD3Selection = select<HTMLCanvasElement, undefined>(this.canvas)
123
- this.canvasD3Selection
124
- .on('mouseenter.cosmos', () => { this._isMouseOnCanvas = true })
125
- .on('mousemove.cosmos', () => { this._isMouseOnCanvas = true })
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
- })
150
- select(document)
151
- .on('keydown.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = true })
152
- .on('keyup.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = false })
153
- this.zoomInstance.behavior
154
- .on('start.detect', (e: D3ZoomEvent<HTMLCanvasElement, undefined>) => { this.currentEvent = e })
155
- .on('zoom.detect', (e: D3ZoomEvent<HTMLCanvasElement, undefined>) => {
156
- const userDriven = !!e.sourceEvent
157
- if (userDriven) this.updateMousePosition(e.sourceEvent)
158
- this.currentEvent = e
159
- })
160
- .on('end.detect', (e: D3ZoomEvent<HTMLCanvasElement, undefined>) => { this.currentEvent = e })
161
- this.dragInstance.behavior
162
- .on('start.detect', (e: D3DragEvent<HTMLCanvasElement, undefined, Hovered>) => {
163
- this.currentEvent = e
164
- this.updateCanvasCursor()
165
- })
166
- .on('drag.detect', (e: D3DragEvent<HTMLCanvasElement, undefined, Hovered>) => {
167
- if (this.dragInstance.isActive) {
168
- this.updateMousePosition(e)
169
- }
170
- this.currentEvent = e
171
- })
172
- .on('end.detect', (e: D3DragEvent<HTMLCanvasElement, undefined, Hovered>) => {
173
- this.currentEvent = e
174
- this.updateCanvasCursor()
175
- })
176
- this.canvasD3Selection
177
- .call(this.dragInstance.behavior)
178
- .call(this.zoomInstance.behavior)
179
- .on('click', this.onClick.bind(this))
180
- .on('mousemove', this.onMouseMove.bind(this))
181
- .on('contextmenu', this.onRightClickMouse.bind(this))
182
- if (!this.config.enableZoom || !this.config.enableDrag) this.updateZoomDragBehaviors()
183
- this.setZoomLevel(this.config.initialZoomLevel ?? 1)
184
-
185
- this.store.maxPointSize = (this.reglInstance.limits.pointSizeDims[1] ?? MAX_POINT_SIZE) / this.config.pixelRatio
186
-
187
- this.points = new Points(this.reglInstance, this.config, this.store, this.graph)
188
- this.lines = new Lines(this.reglInstance, this.config, this.store, this.graph, this.points)
189
- if (this.config.enableSimulation) {
190
- this.forceGravity = new ForceGravity(this.reglInstance, this.config, this.store, this.graph, this.points)
191
- this.forceCenter = new ForceCenter(this.reglInstance, this.config, this.store, this.graph, this.points)
192
- this.forceManyBody = this.config.useClassicQuadtree
193
- ? new ForceManyBodyQuadtree(this.reglInstance, this.config, this.store, this.graph, this.points)
194
- : new ForceManyBody(this.reglInstance, this.config, this.store, this.graph, this.points)
195
- this.forceLinkIncoming = new ForceLink(this.reglInstance, this.config, this.store, this.graph, this.points)
196
- this.forceLinkOutgoing = new ForceLink(this.reglInstance, this.config, this.store, this.graph, this.points)
197
- this.forceMouse = new ForceMouse(this.reglInstance, this.config, this.store, this.graph, this.points)
198
- }
199
- this.clusters = new Clusters(this.reglInstance, this.config, this.store, this.graph, this.points)
200
-
201
- this.store.backgroundColor = getRgbaColor(this.config.backgroundColor)
202
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
203
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
204
- if (this.config.focusedPointIndex !== undefined) {
205
- this.store.setFocusedPoint(this.config.focusedPointIndex)
206
- }
207
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
208
- this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
209
-
210
- this.store.updateLinkHoveringEnabled(this.config)
211
-
212
- if (this.config.showFPSMonitor) this.fpsMonitor = new FPSMonitor(this.canvas)
213
-
214
- if (this.config.randomSeed !== undefined) this.store.addRandomSeed(this.config.randomSeed)
215
- }
216
-
217
- /**
218
- * Returns the current simulation progress
219
- */
220
- public get progress (): number {
221
- if (this._isDestroyed) return 0
222
- return this.store.simulationProgress
223
- }
224
-
225
- /**
226
- * A value that gives information about the running simulation status.
227
- */
228
- public get isSimulationRunning (): boolean {
229
- if (this._isDestroyed) return false
230
- return this.store.isSimulationRunning
231
- }
232
-
233
- /**
234
- * The maximum point size.
235
- * This value is the maximum size of the `gl.POINTS` primitive that WebGL can render on the user's hardware.
236
- */
237
- public get maxPointSize (): number {
238
- if (this._isDestroyed) return 0
239
- return this.store.maxPointSize
240
- }
241
-
242
- /**
243
- * Set or update Cosmos configuration. The changes will be applied in real time.
244
- * @param config Cosmos configuration object.
245
- */
246
- public setConfig (config: Partial<GraphConfigInterface>): void {
247
- if (this._isDestroyed || !this.reglInstance || !this.points || !this.lines || !this.clusters) return
248
- const prevConfig = { ...this.config }
249
- this.config.init(config)
250
- if ((prevConfig.pointDefaultColor !== this.config.pointDefaultColor) ||
251
- (prevConfig.pointColor !== this.config.pointColor)) {
252
- this.graph.updatePointColor()
253
- this.points.updateColor()
254
- }
255
- if (prevConfig.pointSize !== this.config.pointSize) {
256
- this.graph.updatePointSize()
257
- this.points.updateSize()
258
- }
259
- if (prevConfig.linkColor !== this.config.linkColor) {
260
- this.graph.updateLinkColor()
261
- this.lines.updateColor()
262
- }
263
- if (prevConfig.linkWidth !== this.config.linkWidth) {
264
- this.graph.updateLinkWidth()
265
- this.lines.updateWidth()
266
- }
267
- if (prevConfig.linkArrows !== this.config.linkArrows) {
268
- this.graph.updateArrows()
269
- this.lines.updateArrow()
270
- }
271
- if (prevConfig.curvedLinkSegments !== this.config.curvedLinkSegments ||
272
- prevConfig.curvedLinks !== this.config.curvedLinks) {
273
- this.lines.updateCurveLineGeometry()
274
- }
275
- if (prevConfig.backgroundColor !== this.config.backgroundColor) {
276
- this.store.backgroundColor = getRgbaColor(this.config.backgroundColor ?? defaultBackgroundColor)
277
- }
278
- if (prevConfig.hoveredPointRingColor !== this.config.hoveredPointRingColor) {
279
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
280
- }
281
- if (prevConfig.focusedPointRingColor !== this.config.focusedPointRingColor) {
282
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
283
- }
284
- if (prevConfig.pointGreyoutColor !== this.config.pointGreyoutColor) {
285
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
286
- }
287
- if (prevConfig.hoveredLinkColor !== this.config.hoveredLinkColor) {
288
- this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
289
- }
290
- if (prevConfig.focusedPointIndex !== this.config.focusedPointIndex) {
291
- this.store.setFocusedPoint(this.config.focusedPointIndex)
292
- }
293
- if (prevConfig.spaceSize !== this.config.spaceSize ||
294
- prevConfig.simulationRepulsionQuadtreeLevels !== this.config.simulationRepulsionQuadtreeLevels) {
295
- this.store.adjustSpaceSize(this.config.spaceSize, this.reglInstance.limits.maxTextureSize)
296
- this.resizeCanvas(true)
297
- this.update(this.store.isSimulationRunning ? this.store.alpha : 0)
298
- }
299
- if (prevConfig.showFPSMonitor !== this.config.showFPSMonitor) {
300
- if (this.config.showFPSMonitor) {
301
- this.fpsMonitor = new FPSMonitor(this.canvas)
302
- } else {
303
- this.fpsMonitor?.destroy()
304
- this.fpsMonitor = undefined
305
- }
306
- }
307
- if (prevConfig.pixelRatio !== this.config.pixelRatio) {
308
- this.store.maxPointSize = (this.reglInstance.limits.pointSizeDims[1] ?? MAX_POINT_SIZE) / this.config.pixelRatio
309
- }
310
-
311
- if (prevConfig.enableZoom !== this.config.enableZoom || prevConfig.enableDrag !== this.config.enableDrag) {
312
- this.updateZoomDragBehaviors()
313
- }
314
-
315
- if (prevConfig.onLinkClick !== this.config.onLinkClick ||
316
- prevConfig.onLinkMouseOver !== this.config.onLinkMouseOver ||
317
- prevConfig.onLinkMouseOut !== this.config.onLinkMouseOut) {
318
- this.store.updateLinkHoveringEnabled(this.config)
319
- }
320
- }
321
-
322
- /**
323
- * Sets the positions for the graph points.
324
- *
325
- * @param {Float32Array} pointPositions - A Float32Array representing the positions of points in the format [x1, y1, x2, y2, ..., xn, yn],
326
- * where `n` is the index of the point.
327
- * Example: `new Float32Array([1, 2, 3, 4, 5, 6])` sets the first point to (1, 2), the second point to (3, 4), and so on.
328
- * @param {boolean | undefined} dontRescale - For this call only, don't rescale the points.
329
- * - `true`: Don't rescale.
330
- * - `false` or `undefined` (default): Use the behavior defined by `config.rescalePositions`.
331
- */
332
- public setPointPositions (pointPositions: Float32Array, dontRescale?: boolean | undefined): void {
333
- if (this._isDestroyed || !this.points) return
334
- this.graph.inputPointPositions = pointPositions
335
- this.points.shouldSkipRescale = dontRescale
336
- this.isPointPositionsUpdateNeeded = true
337
- // Links related texture depends on point positions, so we need to update it
338
- this.isLinksUpdateNeeded = true
339
- // Point related textures depend on point positions length, so we need to update them
340
- this.isPointColorUpdateNeeded = true
341
- this.isPointSizeUpdateNeeded = true
342
- this.isPointShapeUpdateNeeded = true
343
- this.isPointImageIndicesUpdateNeeded = true
344
- this.isPointImageSizesUpdateNeeded = true
345
- this.isPointClusterUpdateNeeded = true
346
- this.isForceManyBodyUpdateNeeded = true
347
- this.isForceLinkUpdateNeeded = true
348
- this.isForceCenterUpdateNeeded = true
349
- }
350
-
351
- /**
352
- * Sets the colors for the graph points.
353
- *
354
- * @param {Float32Array} pointColors - A Float32Array representing the colors of points in the format [r1, g1, b1, a1, r2, g2, b2, a2, ..., rn, gn, bn, an],
355
- * where each color is represented in RGBA format.
356
- * Example: `new Float32Array([255, 0, 0, 1, 0, 255, 0, 1])` sets the first point to red and the second point to green.
357
- */
358
- public setPointColors (pointColors: Float32Array): void {
359
- if (this._isDestroyed) return
360
- this.graph.inputPointColors = pointColors
361
- this.isPointColorUpdateNeeded = true
362
- }
363
-
364
- /**
365
- * Gets the current colors of the graph points.
366
- *
367
- * @returns {Float32Array} A Float32Array representing the colors of points in the format [r1, g1, b1, a1, r2, g2, b2, a2, ..., rn, gn, bn, an],
368
- * where each color is in RGBA format. Returns an empty Float32Array if no point colors are set.
369
- */
370
- public getPointColors (): Float32Array {
371
- if (this._isDestroyed) return new Float32Array()
372
- return this.graph.pointColors ?? new Float32Array()
373
- }
374
-
375
- /**
376
- * Sets the sizes for the graph points.
377
- *
378
- * @param {Float32Array} pointSizes - A Float32Array representing the sizes of points in the format [size1, size2, ..., sizen],
379
- * where `n` is the index of the point.
380
- * Example: `new Float32Array([10, 20, 30])` sets the first point to size 10, the second point to size 20, and the third point to size 30.
381
- */
382
- public setPointSizes (pointSizes: Float32Array): void {
383
- if (this._isDestroyed) return
384
- this.graph.inputPointSizes = pointSizes
385
- this.isPointSizeUpdateNeeded = true
386
- }
387
-
388
- /**
389
- * Sets the shapes for the graph points.
390
- *
391
- * @param {Float32Array} pointShapes - A Float32Array representing the shapes of points in the format [shape1, shape2, ..., shapen],
392
- * where `n` is the index of the point and each shape value corresponds to a PointShape enum:
393
- * 0 = Circle, 1 = Square, 2 = Triangle, 3 = Diamond, 4 = Pentagon, 5 = Hexagon, 6 = Star, 7 = Cross, 8 = None.
394
- * Example: `new Float32Array([0, 1, 2])` sets the first point to Circle, the second point to Square, and the third point to Triangle.
395
- * Images are rendered above shapes.
396
- */
397
- public setPointShapes (pointShapes: Float32Array): void {
398
- if (this._isDestroyed) return
399
- this.graph.inputPointShapes = pointShapes
400
- this.isPointShapeUpdateNeeded = true
401
- }
402
-
403
- /**
404
- * Sets the images for the graph points using ImageData objects.
405
- * Images are rendered above shapes.
406
- * To use images, provide image indices via setPointImageIndices().
407
- *
408
- * @param {ImageData[]} imageDataArray - Array of ImageData objects to use as point images.
409
- * Example: `setImageData([imageData1, imageData2, imageData3])`
410
- */
411
- public setImageData (imageDataArray: ImageData[]): void {
412
- if (this._isDestroyed || !this.points) return
413
- this.graph.inputImageData = imageDataArray
414
- this.points.createAtlas()
415
- }
416
-
417
- /**
418
- * Sets which image each point should use from the images array.
419
- * Images are rendered above shapes.
420
- *
421
- * @param {Float32Array} imageIndices - A Float32Array representing which image each point uses in the format [index1, index2, ..., indexn],
422
- * where `n` is the index of the point and each value is an index into the images array provided to `setImageData`.
423
- * 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.
424
- */
425
- public setPointImageIndices (imageIndices: Float32Array): void {
426
- if (this._isDestroyed) return
427
- this.graph.inputPointImageIndices = imageIndices
428
- this.isPointImageIndicesUpdateNeeded = true
429
- }
430
-
431
- /**
432
- * Sets the sizes for the point images.
433
- *
434
- * @param {Float32Array} imageSizes - A Float32Array representing the sizes of point images in the format [size1, size2, ..., sizen],
435
- * where `n` is the index of the point.
436
- * 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.
437
- */
438
- public setPointImageSizes (imageSizes: Float32Array): void {
439
- if (this._isDestroyed) return
440
- this.graph.inputPointImageSizes = imageSizes
441
- this.isPointImageSizesUpdateNeeded = true
442
- }
443
-
444
- /**
445
- * Gets the current sizes of the graph points.
446
- *
447
- * @returns {Float32Array} A Float32Array representing the sizes of points in the format [size1, size2, ..., sizen],
448
- * where `n` is the index of the point. Returns an empty Float32Array if no point sizes are set.
449
- */
450
- public getPointSizes (): Float32Array {
451
- if (this._isDestroyed) return new Float32Array()
452
- return this.graph.pointSizes ?? new Float32Array()
453
- }
454
-
455
- /**
456
- * Sets the links for the graph.
457
- *
458
- * @param {Float32Array} links - A Float32Array representing the links between points
459
- * in the format [source1, target1, source2, target2, ..., sourcen, targetn],
460
- * where `source` and `target` are the indices of the points being linked.
461
- * Example: `new Float32Array([0, 1, 1, 2])` creates a link from point 0 to point 1 and another link from point 1 to point 2.
462
- */
463
- public setLinks (links: Float32Array): void {
464
- if (this._isDestroyed) return
465
- this.graph.inputLinks = links
466
- this.isLinksUpdateNeeded = true
467
- // Links related texture depends on links length, so we need to update it
468
- this.isLinkColorUpdateNeeded = true
469
- this.isLinkWidthUpdateNeeded = true
470
- this.isLinkArrowUpdateNeeded = true
471
- this.isForceLinkUpdateNeeded = true
472
- }
473
-
474
- /**
475
- * Sets the colors for the graph links.
476
- *
477
- * @param {Float32Array} linkColors - A Float32Array representing the colors of links in the format [r1, g1, b1, a1, r2, g2, b2, a2, ..., rn, gn, bn, an],
478
- * where each color is in RGBA format.
479
- * Example: `new Float32Array([255, 0, 0, 1, 0, 255, 0, 1])` sets the first link to red and the second link to green.
480
- */
481
- public setLinkColors (linkColors: Float32Array): void {
482
- if (this._isDestroyed) return
483
- this.graph.inputLinkColors = linkColors
484
- this.isLinkColorUpdateNeeded = true
485
- }
486
-
487
- /**
488
- * Gets the current colors of the graph links.
489
- *
490
- * @returns {Float32Array} A Float32Array representing the colors of links in the format [r1, g1, b1, a1, r2, g2, b2, a2, ..., rn, gn, bn, an],
491
- * where each color is in RGBA format. Returns an empty Float32Array if no link colors are set.
492
- */
493
- public getLinkColors (): Float32Array {
494
- if (this._isDestroyed) return new Float32Array()
495
- return this.graph.linkColors ?? new Float32Array()
496
- }
497
-
498
- /**
499
- * Sets the widths for the graph links.
500
- *
501
- * @param {Float32Array} linkWidths - A Float32Array representing the widths of links in the format [width1, width2, ..., widthn],
502
- * where `n` is the index of the link.
503
- * Example: `new Float32Array([1, 2, 3])` sets the first link to width 1, the second link to width 2, and the third link to width 3.
504
- */
505
- public setLinkWidths (linkWidths: Float32Array): void {
506
- if (this._isDestroyed) return
507
- this.graph.inputLinkWidths = linkWidths
508
- this.isLinkWidthUpdateNeeded = true
509
- }
510
-
511
- /**
512
- * Gets the current widths of the graph links.
513
- *
514
- * @returns {Float32Array} A Float32Array representing the widths of links in the format [width1, width2, ..., widthn],
515
- * where `n` is the index of the link. Returns an empty Float32Array if no link widths are set.
516
- */
517
- public getLinkWidths (): Float32Array {
518
- if (this._isDestroyed) return new Float32Array()
519
- return this.graph.linkWidths ?? new Float32Array()
520
- }
521
-
522
- /**
523
- * Sets the arrows for the graph links.
524
- *
525
- * @param {boolean[]} linkArrows - An array of booleans indicating whether each link should have an arrow,
526
- * in the format [arrow1, arrow2, ..., arrown], where `n` is the index of the link.
527
- * Example: `[true, false, true]` sets arrows on the first and third links, but not on the second link.
528
- */
529
- public setLinkArrows (linkArrows: boolean[]): void {
530
- if (this._isDestroyed) return
531
- this.graph.linkArrowsBoolean = linkArrows
532
- this.isLinkArrowUpdateNeeded = true
533
- }
534
-
535
- /**
536
- * Sets the strength for the graph links.
537
- *
538
- * @param {Float32Array} linkStrength - A Float32Array representing the strength of each link in the format [strength1, strength2, ..., strengthn],
539
- * where `n` is the index of the link.
540
- * Example: `new Float32Array([1, 2, 3])` sets the first link to strength 1, the second link to strength 2, and the third link to strength 3.
541
- */
542
- public setLinkStrength (linkStrength: Float32Array): void {
543
- if (this._isDestroyed) return
544
- this.graph.inputLinkStrength = linkStrength
545
- this.isForceLinkUpdateNeeded = true
546
- }
547
-
548
- /**
549
- * Sets the point clusters for the graph.
550
- *
551
- * @param {(number | undefined)[]} pointClusters - Array of cluster indices for each point in the graph.
552
- * - Index: Each index corresponds to a point.
553
- * - Values: Integers starting from 0; `undefined` indicates that a point does not belong to any cluster and will not be affected by cluster forces.
554
- * @example
555
- * `[0, 1, 0, 2, undefined, 1]` maps points to clusters: point 0 and 2 to cluster 0, point 1 to cluster 1, and point 3 to cluster 2.
556
- * Points 4 is unclustered.
557
- * @note Clusters without specified positions via `setClusterPositions` will be positioned at their centermass by default.
558
- */
559
- public setPointClusters (pointClusters: (number | undefined)[]): void {
560
- if (this._isDestroyed) return
561
- this.graph.inputPointClusters = pointClusters
562
- this.isPointClusterUpdateNeeded = true
563
- }
564
-
565
- /**
566
- * Sets the positions of the point clusters for the graph.
567
- *
568
- * @param {(number | undefined)[]} clusterPositions - Array of cluster positions.
569
- * - Every two elements represent the x and y coordinates for a cluster position.
570
- * - `undefined` means the cluster's position is not defined and will use centermass positioning instead.
571
- * @example
572
- * `[10, 20, 30, 40, undefined, undefined]` places the first cluster at (10, 20) and the second at (30, 40);
573
- * the third cluster will be positioned at its centermass automatically.
574
- */
575
- public setClusterPositions (clusterPositions: (number | undefined)[]): void {
576
- if (this._isDestroyed) return
577
- this.graph.inputClusterPositions = clusterPositions
578
- this.isPointClusterUpdateNeeded = true
579
- }
580
-
581
- /**
582
- * Sets the force strength coefficients for clustering points in the graph.
583
- *
584
- * This method allows you to customize the forces acting on individual points during the clustering process.
585
- * The force coefficients determine the strength of the forces applied to each point.
586
- *
587
- * @param {Float32Array} clusterStrength - A Float32Array of force strength coefficients for each point in the format [coeff1, coeff2, ..., coeffn],
588
- * where `n` is the index of the point.
589
- * Example: `new Float32Array([1, 0.4, 0.3])` sets the force coefficient for point 0 to 1, point 1 to 0.4, and point 2 to 0.3.
590
- */
591
- public setPointClusterStrength (clusterStrength: Float32Array): void {
592
- if (this._isDestroyed) return
593
- this.graph.inputClusterStrength = clusterStrength
594
- this.isPointClusterUpdateNeeded = true
595
- }
596
-
597
- /**
598
- * Sets which points are pinned (fixed) in position.
599
- *
600
- * Pinned points:
601
- * - Do not move due to physics forces (gravity, repulsion, link forces, etc.)
602
- * - Still participate in force calculations (other nodes are attracted to/repelled by them)
603
- * - Can still be dragged by the user if `enableDrag` is true
604
- *
605
- * @param {number[] | null} pinnedIndices - Array of point indices to pin. Set to `[]` or `null` to unpin all points.
606
- * @example
607
- * // Pin points 0 and 5
608
- * graph.setPinnedPoints([0, 5])
609
- *
610
- * // Unpin all points
611
- * graph.setPinnedPoints([])
612
- * graph.setPinnedPoints(null)
613
- */
614
- public setPinnedPoints (pinnedIndices: number[] | null): void {
615
- if (this._isDestroyed || !this.points) return
616
- this.graph.inputPinnedPoints = pinnedIndices && pinnedIndices.length > 0 ? pinnedIndices : undefined
617
- this.points.updatePinnedStatus()
618
- }
619
-
620
- /**
621
- * Renders the graph.
622
- *
623
- * @param {number} [simulationAlpha] - Optional value between 0 and 1
624
- * that controls the initial energy of the simulation.The higher the value,
625
- * the more initial energy the simulation will get. Zero value stops the simulation.
626
- */
627
- public render (simulationAlpha?: number): void {
628
- if (this._isDestroyed || !this.reglInstance) return
629
- this.graph.update()
630
- const { fitViewOnInit, fitViewDelay, fitViewPadding, fitViewDuration, fitViewByPointsInRect, fitViewByPointIndices, initialZoomLevel } = this.config
631
- if (!this.graph.pointsNumber && !this.graph.linksNumber) {
632
- this.stopFrames()
633
- select(this.canvas).style('cursor', null)
634
- this.reglInstance.clear({
635
- color: this.store.backgroundColor,
636
- depth: 1,
637
- stencil: 0,
638
- })
639
- return
640
- }
641
-
642
- // If `initialZoomLevel` is set, we don't need to fit the view
643
- if (this._isFirstRenderAfterInit && fitViewOnInit && initialZoomLevel === undefined) {
644
- this._fitViewOnInitTimeoutID = window.setTimeout(() => {
645
- if (fitViewByPointIndices) this.fitViewByPointIndices(fitViewByPointIndices, fitViewDuration, fitViewPadding)
646
- else if (fitViewByPointsInRect) this.setZoomTransformByPointPositions(fitViewByPointsInRect, fitViewDuration, undefined, fitViewPadding)
647
- else this.fitView(fitViewDuration, fitViewPadding)
648
- }, fitViewDelay)
649
- }
650
- this._isFirstRenderAfterInit = false
651
-
652
- this.update(simulationAlpha)
653
- }
654
-
655
- /**
656
- * Center the view on a point and zoom in, by point index.
657
- * @param index The index of the point in the array of points.
658
- * @param duration Duration of the animation transition in milliseconds (`700` by default).
659
- * @param scale Scale value to zoom in or out (`3` by default).
660
- * @param canZoomOut Set to `false` to prevent zooming out from the point (`true` by default).
661
- */
662
- public zoomToPointByIndex (index: number, duration = 700, scale = defaultScaleToZoom, canZoomOut = true): void {
663
- if (this._isDestroyed || !this.reglInstance || !this.points || !this.canvasD3Selection) return
664
- const { store: { screenSize } } = this
665
- const positionPixels = readPixels(this.reglInstance, this.points.currentPositionFbo as regl.Framebuffer2D)
666
- if (index === undefined) return
667
- const posX = positionPixels[index * 4 + 0]
668
- const posY = positionPixels[index * 4 + 1]
669
- if (posX === undefined || posY === undefined) return
670
- const distance = this.zoomInstance.getDistanceToPoint([posX, posY])
671
- const zoomLevel = canZoomOut ? scale : Math.max(this.getZoomLevel(), scale)
672
- if (distance < Math.min(screenSize[0], screenSize[1])) {
673
- this.setZoomTransformByPointPositions([posX, posY], duration, zoomLevel)
674
- } else {
675
- const transform = this.zoomInstance.getTransform([[posX, posY]], zoomLevel)
676
- const middle = this.zoomInstance.getMiddlePointTransform([posX, posY])
677
- this.canvasD3Selection
678
- .transition()
679
- .ease(easeQuadIn)
680
- .duration(duration / 2)
681
- .call(this.zoomInstance.behavior.transform, middle)
682
- .transition()
683
- .ease(easeQuadOut)
684
- .duration(duration / 2)
685
- .call(this.zoomInstance.behavior.transform, transform)
686
- }
687
- }
688
-
689
- /**
690
- * Zoom the view in or out to the specified zoom level.
691
- * @param value Zoom level
692
- * @param duration Duration of the zoom in/out transition.
693
- */
694
-
695
- public zoom (value: number, duration = 0): void {
696
- if (this._isDestroyed) return
697
- this.setZoomLevel(value, duration)
698
- }
699
-
700
- /**
701
- * Zoom the view in or out to the specified zoom level.
702
- * @param value Zoom level
703
- * @param duration Duration of the zoom in/out transition.
704
- */
705
- public setZoomLevel (value: number, duration = 0): void {
706
- if (this._isDestroyed || !this.canvasD3Selection) return
707
- if (duration === 0) {
708
- this.canvasD3Selection
709
- .call(this.zoomInstance.behavior.scaleTo, value)
710
- } else {
711
- this.canvasD3Selection
712
- .transition()
713
- .duration(duration)
714
- .call(this.zoomInstance.behavior.scaleTo, value)
715
- }
716
- }
717
-
718
- /**
719
- * Get zoom level.
720
- * @returns Zoom level value of the view.
721
- */
722
- public getZoomLevel (): number {
723
- if (this._isDestroyed) return 0
724
- return this.zoomInstance.eventTransform.k
725
- }
726
-
727
- /**
728
- * Get current X and Y coordinates of the points.
729
- * @returns Array of point positions.
730
- */
731
- public getPointPositions (): number[] {
732
- if (this._isDestroyed || !this.reglInstance || !this.points) return []
733
- if (this.graph.pointsNumber === undefined) return []
734
- const positions: number[] = []
735
- const pointPositionsPixels = readPixels(this.reglInstance, this.points.currentPositionFbo as regl.Framebuffer2D)
736
- positions.length = this.graph.pointsNumber * 2
737
- for (let i = 0; i < this.graph.pointsNumber; i += 1) {
738
- const posX = pointPositionsPixels[i * 4 + 0]
739
- const posY = pointPositionsPixels[i * 4 + 1]
740
- if (posX !== undefined && posY !== undefined) {
741
- positions[i * 2] = posX
742
- positions[i * 2 + 1] = posY
743
- }
744
- }
745
- return positions
746
- }
747
-
748
- /**
749
- * Get current X and Y coordinates of the clusters.
750
- * @returns Array of point cluster.
751
- */
752
- public getClusterPositions (): number[] {
753
- if (this._isDestroyed || !this.reglInstance || !this.clusters) return []
754
- if (this.graph.pointClusters === undefined || this.clusters.clusterCount === undefined) return []
755
- this.clusters.calculateCentermass()
756
- const positions: number[] = []
757
- const clusterPositionsPixels = readPixels(this.reglInstance, this.clusters.centermassFbo as regl.Framebuffer2D)
758
- positions.length = this.clusters.clusterCount * 2
759
- for (let i = 0; i < positions.length / 2; i += 1) {
760
- const sumX = clusterPositionsPixels[i * 4 + 0]
761
- const sumY = clusterPositionsPixels[i * 4 + 1]
762
- const sumN = clusterPositionsPixels[i * 4 + 2]
763
- if (sumX !== undefined && sumY !== undefined && sumN !== undefined) {
764
- positions[i * 2] = sumX / sumN
765
- positions[i * 2 + 1] = sumY / sumN
766
- }
767
- }
768
- return positions
769
- }
770
-
771
- /**
772
- * Center and zoom in/out the view to fit all points in the scene.
773
- * @param duration Duration of the center and zoom in/out animation in milliseconds (`250` by default).
774
- * @param padding Padding around the viewport in percentage (`0.1` by default).
775
- */
776
- public fitView (duration = 250, padding = 0.1): void {
777
- if (this._isDestroyed) return
778
- this.setZoomTransformByPointPositions(this.getPointPositions(), duration, undefined, padding)
779
- }
780
-
781
- /**
782
- * Center and zoom in/out the view to fit points by their indices in the scene.
783
- * @param duration Duration of the center and zoom in/out animation in milliseconds (`250` by default).
784
- * @param padding Padding around the viewport in percentage
785
- */
786
- public fitViewByPointIndices (indices: number[], duration = 250, padding = 0.1): void {
787
- if (this._isDestroyed) return
788
- const positionsArray = this.getPointPositions()
789
- const positions = new Array(indices.length * 2)
790
- for (const [i, index] of indices.entries()) {
791
- positions[i * 2] = positionsArray[index * 2]
792
- positions[i * 2 + 1] = positionsArray[index * 2 + 1]
793
- }
794
- this.setZoomTransformByPointPositions(positions, duration, undefined, padding)
795
- }
796
-
797
- /**
798
- * Center and zoom in/out the view to fit points by their positions in the scene.
799
- * @param duration Duration of the center and zoom in/out animation in milliseconds (`250` by default).
800
- * @param padding Padding around the viewport in percentage
801
- */
802
- public fitViewByPointPositions (positions: number[], duration = 250, padding = 0.1): void {
803
- if (this._isDestroyed) return
804
- this.setZoomTransformByPointPositions(positions, duration, undefined, padding)
805
- }
806
-
807
- /**
808
- * Get points indices inside a rectangular area.
809
- * @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
810
- * The `left` and `right` coordinates should be from 0 to the width of the canvas.
811
- * The `top` and `bottom` coordinates should be from 0 to the height of the canvas.
812
- * @returns A Float32Array containing the indices of points inside a rectangular area.
813
- */
814
- public getPointsInRect (selection: [[number, number], [number, number]]): Float32Array {
815
- if (this._isDestroyed || !this.reglInstance || !this.points) return new Float32Array()
816
- const h = this.store.screenSize[1]
817
- this.store.selectedArea = [[selection[0][0], (h - selection[1][1])], [selection[1][0], (h - selection[0][1])]]
818
- this.points.findPointsOnAreaSelection()
819
- const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
820
-
821
- return pixels
822
- .map((pixel, i) => {
823
- if (i % 4 === 0 && pixel !== 0) return i / 4
824
- else return -1
825
- })
826
- .filter(d => d !== -1)
827
- }
828
-
829
- /**
830
- * Get points indices inside a rectangular area.
831
- * @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
832
- * The `left` and `right` coordinates should be from 0 to the width of the canvas.
833
- * The `top` and `bottom` coordinates should be from 0 to the height of the canvas.
834
- * @returns A Float32Array containing the indices of points inside a rectangular area.
835
- * @deprecated Use `getPointsInRect` instead. This method will be removed in a future version.
836
- */
837
- public getPointsInRange (selection: [[number, number], [number, number]]): Float32Array {
838
- return this.getPointsInRect(selection)
839
- }
840
-
841
- /**
842
- * Get points indices inside a polygon area.
843
- * @param polygonPath - Array of points `[[x1, y1], [x2, y2], ..., [xn, yn]]` that defines the polygon.
844
- * The coordinates should be from 0 to the width/height of the canvas.
845
- * @returns A Float32Array containing the indices of points inside the polygon area.
846
- */
847
- public getPointsInPolygon (polygonPath: [number, number][]): Float32Array {
848
- if (this._isDestroyed || !this.reglInstance || !this.points) return new Float32Array()
849
- if (polygonPath.length < 3) return new Float32Array() // Need at least 3 points for a polygon
850
-
851
- const h = this.store.screenSize[1]
852
- // Convert coordinates to WebGL coordinate system (flip Y)
853
- const convertedPath = polygonPath.map(([x, y]) => [x, h - y] as [number, number])
854
- this.points.updatePolygonPath(convertedPath)
855
- this.points.findPointsOnPolygonSelection()
856
- const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
857
-
858
- return pixels
859
- .map((pixel, i) => {
860
- if (i % 4 === 0 && pixel !== 0) return i / 4
861
- else return -1
862
- })
863
- .filter(d => d !== -1)
864
- }
865
-
866
- /** Select points inside a rectangular area.
867
- * @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
868
- * The `left` and `right` coordinates should be from 0 to the width of the canvas.
869
- * The `top` and `bottom` coordinates should be from 0 to the height of the canvas. */
870
- public selectPointsInRect (selection: [[number, number], [number, number]] | null): void {
871
- if (this._isDestroyed || !this.reglInstance || !this.points) return
872
- if (selection) {
873
- const h = this.store.screenSize[1]
874
- this.store.selectedArea = [[selection[0][0], (h - selection[1][1])], [selection[1][0], (h - selection[0][1])]]
875
- this.points.findPointsOnAreaSelection()
876
- const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
877
- this.store.selectedIndices = pixels
878
- .map((pixel, i) => {
879
- if (i % 4 === 0 && pixel !== 0) return i / 4
880
- else return -1
881
- })
882
- .filter(d => d !== -1)
883
- } else {
884
- this.store.selectedIndices = null
885
- }
886
- this.points.updateGreyoutStatus()
887
- }
888
-
889
- /** Select points inside a rectangular area.
890
- * @param selection - Array of two corner points `[[left, top], [right, bottom]]`.
891
- * The `left` and `right` coordinates should be from 0 to the width of the canvas.
892
- * The `top` and `bottom` coordinates should be from 0 to the height of the canvas.
893
- * @deprecated Use `selectPointsInRect` instead. This method will be removed in a future version.
894
- */
895
- public selectPointsInRange (selection: [[number, number], [number, number]] | null): void {
896
- return this.selectPointsInRect(selection)
897
- }
898
-
899
- /** Select points inside a polygon area.
900
- * @param polygonPath - Array of points `[[x1, y1], [x2, y2], ..., [xn, yn]]` that defines the polygon.
901
- * The coordinates should be from 0 to the width/height of the canvas.
902
- * Set to null to clear selection. */
903
- public selectPointsInPolygon (polygonPath: [number, number][] | null): void {
904
- if (this._isDestroyed || !this.reglInstance || !this.points) return
905
- if (polygonPath) {
906
- if (polygonPath.length < 3) {
907
- console.warn('Polygon path requires at least 3 points to form a polygon.')
908
- return
909
- }
910
-
911
- const h = this.store.screenSize[1]
912
- // Convert coordinates to WebGL coordinate system (flip Y)
913
- const convertedPath = polygonPath.map(([x, y]) => [x, h - y] as [number, number])
914
- this.points.updatePolygonPath(convertedPath)
915
- this.points.findPointsOnPolygonSelection()
916
- const pixels = readPixels(this.reglInstance, this.points.selectedFbo as regl.Framebuffer2D)
917
- this.store.selectedIndices = pixels
918
- .map((pixel, i) => {
919
- if (i % 4 === 0 && pixel !== 0) return i / 4
920
- else return -1
921
- })
922
- .filter(d => d !== -1)
923
- } else {
924
- this.store.selectedIndices = null
925
- }
926
- this.points.updateGreyoutStatus()
927
- }
928
-
929
- /**
930
- * Select a point by index. If you want the adjacent points to get selected too, provide `true` as the second argument.
931
- * @param index The index of the point in the array of points.
932
- * @param selectAdjacentPoints When set to `true`, selects adjacent points (`false` by default).
933
- */
934
- public selectPointByIndex (index: number, selectAdjacentPoints = false): void {
935
- if (this._isDestroyed) return
936
- if (selectAdjacentPoints) {
937
- const adjacentIndices = this.graph.getAdjacentIndices(index) ?? []
938
- this.selectPointsByIndices([index, ...adjacentIndices])
939
- } else this.selectPointsByIndices([index])
940
- }
941
-
942
- /**
943
- * Select multiples points by their indices.
944
- * @param indices Array of points indices.
945
- */
946
- public selectPointsByIndices (indices?: (number | undefined)[] | null): void {
947
- if (this._isDestroyed || !this.points) return
948
- if (!indices) {
949
- this.store.selectedIndices = null
950
- } else if (indices.length === 0) {
951
- this.store.selectedIndices = new Float32Array()
952
- } else {
953
- this.store.selectedIndices = new Float32Array(indices.filter(d => d !== undefined))
954
- }
955
-
956
- this.points.updateGreyoutStatus()
957
- }
958
-
959
- /**
960
- * Unselect all points.
961
- */
962
- public unselectPoints (): void {
963
- if (this._isDestroyed || !this.points) return
964
- this.store.selectedIndices = null
965
- this.points.updateGreyoutStatus()
966
- }
967
-
968
- /**
969
- * Get indices of points that are currently selected.
970
- * @returns Array of selected indices of points.
971
- */
972
- public getSelectedIndices (): number[] | null {
973
- if (this._isDestroyed) return null
974
- const { selectedIndices } = this.store
975
- if (!selectedIndices) return null
976
- return Array.from(selectedIndices)
977
- }
978
-
979
- /**
980
- * Get indices that are adjacent to a specific point by its index.
981
- * @param index Index of the point.
982
- * @returns Array of adjacent indices.
983
- */
984
-
985
- public getAdjacentIndices (index: number): number[] | undefined {
986
- if (this._isDestroyed) return undefined
987
- return this.graph.getAdjacentIndices(index)
988
- }
989
-
990
- /**
991
- * Converts the X and Y point coordinates from the space coordinate system to the screen coordinate system.
992
- * @param spacePosition Array of x and y coordinates in the space coordinate system.
993
- * @returns Array of x and y coordinates in the screen coordinate system.
994
- */
995
- public spaceToScreenPosition (spacePosition: [number, number]): [number, number] {
996
- if (this._isDestroyed) return [0, 0]
997
- return this.zoomInstance.convertSpaceToScreenPosition(spacePosition)
998
- }
999
-
1000
- /**
1001
- * Converts the X and Y point coordinates from the screen coordinate system to the space coordinate system.
1002
- * @param screenPosition Array of x and y coordinates in the screen coordinate system.
1003
- * @returns Array of x and y coordinates in the space coordinate system.
1004
- */
1005
- public screenToSpacePosition (screenPosition: [number, number]): [number, number] {
1006
- if (this._isDestroyed) return [0, 0]
1007
- return this.zoomInstance.convertScreenToSpacePosition(screenPosition)
1008
- }
1009
-
1010
- /**
1011
- * Converts the point radius value from the space coordinate system to the screen coordinate system.
1012
- * @param spaceRadius Radius of point in the space coordinate system.
1013
- * @returns Radius of point in the screen coordinate system.
1014
- */
1015
- public spaceToScreenRadius (spaceRadius: number): number {
1016
- if (this._isDestroyed) return 0
1017
- return this.zoomInstance.convertSpaceToScreenRadius(spaceRadius)
1018
- }
1019
-
1020
- /**
1021
- * Get point radius by its index.
1022
- * @param index Index of the point.
1023
- * @returns Radius of the point.
1024
- */
1025
- public getPointRadiusByIndex (index: number): number | undefined {
1026
- if (this._isDestroyed) return undefined
1027
- return this.graph.pointSizes?.[index]
1028
- }
1029
-
1030
- /**
1031
- * Track multiple point positions by their indices on each Cosmos tick.
1032
- * @param indices Array of points indices.
1033
- */
1034
- public trackPointPositionsByIndices (indices: number[]): void {
1035
- if (this._isDestroyed || !this.points) return
1036
- this.points.trackPointsByIndices(indices)
1037
- }
1038
-
1039
- /**
1040
- * Get current X and Y coordinates of the tracked points.
1041
- * Do not mutate the returned map - it may affect future calls.
1042
- * @returns A ReadonlyMap where keys are point indices and values are their corresponding X and Y coordinates in the [number, number] format.
1043
- * @see trackPointPositionsByIndices To set which points should be tracked
1044
- */
1045
- public getTrackedPointPositionsMap (): ReadonlyMap<number, [number, number]> {
1046
- if (this._isDestroyed || !this.points) return new Map()
1047
- return this.points.getTrackedPositionsMap()
1048
- }
1049
-
1050
- /**
1051
- * Get current X and Y coordinates of the tracked points as an array.
1052
- * @returns Array of point positions in the format [x1, y1, x2, y2, ..., xn, yn] for tracked points only.
1053
- * The positions are ordered by the tracking indices (same order as provided to trackPointPositionsByIndices).
1054
- * Returns an empty array if no points are being tracked.
1055
- */
1056
- public getTrackedPointPositionsArray (): number[] {
1057
- if (this._isDestroyed || !this.points) return []
1058
- return this.points.getTrackedPositionsArray()
1059
- }
1060
-
1061
- /**
1062
- * For the points that are currently visible on the screen, get a sample of point indices with their coordinates.
1063
- * The resulting number of points will depend on the `pointSamplingDistance` configuration property,
1064
- * and the sampled points will be evenly distributed.
1065
- * @returns A Map object where keys are the index of the points and values are their corresponding X and Y coordinates in the [number, number] format.
1066
- */
1067
- public getSampledPointPositionsMap (): Map<number, [number, number]> {
1068
- if (this._isDestroyed || !this.points) return new Map()
1069
- return this.points.getSampledPointPositionsMap()
1070
- }
1071
-
1072
- /**
1073
- * For the points that are currently visible on the screen, get a sample of point indices and positions.
1074
- * The resulting number of points will depend on the `pointSamplingDistance` configuration property,
1075
- * and the sampled points will be evenly distributed.
1076
- * @returns An object containing arrays of point indices and positions.
1077
- */
1078
- public getSampledPoints (): { indices: number[]; positions: number[] } {
1079
- if (this._isDestroyed || !this.points) return { indices: [], positions: [] }
1080
- return this.points.getSampledPoints()
1081
- }
1082
-
1083
- /**
1084
- * Gets the X-axis of rescaling function.
1085
- *
1086
- * This scale is automatically created when position rescaling is enabled.
1087
- */
1088
- public getScaleX (): ((x: number) => number) | undefined {
1089
- if (this._isDestroyed || !this.points) return undefined
1090
- return this.points.scaleX
1091
- }
1092
-
1093
- /**
1094
- * Gets the Y-axis of rescaling function.
1095
- *
1096
- * This scale is automatically created when position rescaling is enabled.
1097
- */
1098
- public getScaleY (): ((y: number) => number) | undefined {
1099
- if (this._isDestroyed || !this.points) return undefined
1100
- return this.points.scaleY
1101
- }
1102
-
1103
- /**
1104
- * Start the simulation.
1105
- * @param alpha Value from 0 to 1. The higher the value, the more initial energy the simulation will get.
1106
- */
1107
- public start (alpha = 1): void {
1108
- if (this._isDestroyed) return
1109
- if (!this.graph.pointsNumber) return
1110
-
1111
- // Only start the simulation if alpha > 0
1112
- if (alpha > 0) {
1113
- this.store.isSimulationRunning = true
1114
- this.store.simulationProgress = 0
1115
- this.config.onSimulationStart?.()
1116
- }
1117
-
1118
- this.store.alpha = alpha
1119
- this.stopFrames()
1120
- this.frame()
1121
- }
1122
-
1123
- /**
1124
- * Pause the simulation. When paused, the simulation stops running
1125
- * and can be resumed using the unpause method.
1126
- */
1127
- public pause (): void {
1128
- if (this._isDestroyed) return
1129
- this.store.isSimulationRunning = false
1130
- this.config.onSimulationPause?.()
1131
- }
1132
-
1133
- /**
1134
- * Unpause the simulation. This method resumes a paused
1135
- * simulation and continues its execution.
1136
- */
1137
- public unpause (): void {
1138
- if (this._isDestroyed) return
1139
- this.store.isSimulationRunning = true
1140
- this.config.onSimulationUnpause?.()
1141
- }
1142
-
1143
- /**
1144
- * Restart/Resume the simulation. This method unpauses a paused
1145
- * simulation and resumes its execution.
1146
- * @deprecated Use `unpause()` instead. This method will be removed in a future version.
1147
- */
1148
- public restart (): void {
1149
- if (this._isDestroyed) return
1150
- this.store.isSimulationRunning = true
1151
- this.config.onSimulationRestart?.()
1152
- }
1153
-
1154
- /**
1155
- * Render only one frame of the simulation (stops the simulation if it was running).
1156
- */
1157
- public step (): void {
1158
- if (this._isDestroyed) return
1159
- this.store.isSimulationRunning = false
1160
- this.stopFrames()
1161
- this.frame()
1162
- }
1163
-
1164
- /**
1165
- * Destroy this Cosmos instance.
1166
- */
1167
- public destroy (): void {
1168
- if (this._isDestroyed || !this.reglInstance) return
1169
- window.clearTimeout(this._fitViewOnInitTimeoutID)
1170
- this.stopFrames()
1171
-
1172
- // Remove all event listeners
1173
- if (this.canvasD3Selection) {
1174
- this.canvasD3Selection
1175
- .on('mouseenter.cosmos', null)
1176
- .on('mousemove.cosmos', null)
1177
- .on('mouseleave.cosmos', null)
1178
- .on('click', null)
1179
- .on('mousemove', null)
1180
- .on('contextmenu', null)
1181
- .on('.drag', null)
1182
- .on('.zoom', null)
1183
- }
1184
-
1185
- select(document)
1186
- .on('keydown.cosmos', null)
1187
- .on('keyup.cosmos', null)
1188
-
1189
- if (this.zoomInstance?.behavior) {
1190
- this.zoomInstance.behavior
1191
- .on('start.detect', null)
1192
- .on('zoom.detect', null)
1193
- .on('end.detect', null)
1194
- }
1195
-
1196
- if (this.dragInstance?.behavior) {
1197
- this.dragInstance.behavior
1198
- .on('start.detect', null)
1199
- .on('drag.detect', null)
1200
- .on('end.detect', null)
1201
- }
1202
-
1203
- this.fpsMonitor?.destroy()
1204
- this.reglInstance.destroy()
1205
- // Clears the canvas after particle system is destroyed
1206
- this.reglInstance.clear({
1207
- color: this.store.backgroundColor,
1208
- depth: 1,
1209
- stencil: 0,
1210
- })
1211
-
1212
- if (this.canvas && this.canvas.parentNode) {
1213
- this.canvas.parentNode.removeChild(this.canvas)
1214
- }
1215
-
1216
- if (this.attributionDivElement && this.attributionDivElement.parentNode) {
1217
- this.attributionDivElement.parentNode.removeChild(this.attributionDivElement)
1218
- }
1219
-
1220
- document.getElementById('gl-bench-style')?.remove()
1221
-
1222
- this.canvasD3Selection = undefined
1223
- this.reglInstance = undefined
1224
- this.attributionDivElement = undefined
1225
-
1226
- this._isDestroyed = true
1227
- }
1228
-
1229
- /**
1230
- * Updates and recreates the graph visualization based on pending changes.
1231
- */
1232
- public create (): void {
1233
- if (this._isDestroyed || !this.points || !this.lines) return
1234
- if (this.isPointPositionsUpdateNeeded) this.points.updatePositions()
1235
- if (this.isPointColorUpdateNeeded) this.points.updateColor()
1236
- if (this.isPointSizeUpdateNeeded) this.points.updateSize()
1237
- if (this.isPointShapeUpdateNeeded) this.points.updateShape()
1238
- if (this.isPointImageIndicesUpdateNeeded) this.points.updateImageIndices()
1239
- if (this.isPointImageSizesUpdateNeeded) this.points.updateImageSizes()
1240
-
1241
- if (this.isLinksUpdateNeeded) this.lines.updatePointsBuffer()
1242
- if (this.isLinkColorUpdateNeeded) this.lines.updateColor()
1243
- if (this.isLinkWidthUpdateNeeded) this.lines.updateWidth()
1244
- if (this.isLinkArrowUpdateNeeded) this.lines.updateArrow()
1245
-
1246
- if (this.isForceManyBodyUpdateNeeded) this.forceManyBody?.create()
1247
- if (this.isForceLinkUpdateNeeded) {
1248
- this.forceLinkIncoming?.create(LinkDirection.INCOMING)
1249
- this.forceLinkOutgoing?.create(LinkDirection.OUTGOING)
1250
- }
1251
- if (this.isForceCenterUpdateNeeded) this.forceCenter?.create()
1252
- if (this.isPointClusterUpdateNeeded) this.clusters?.create()
1253
-
1254
- this.isPointPositionsUpdateNeeded = false
1255
- this.isPointColorUpdateNeeded = false
1256
- this.isPointSizeUpdateNeeded = false
1257
- this.isPointShapeUpdateNeeded = false
1258
- this.isPointImageIndicesUpdateNeeded = false
1259
- this.isPointImageSizesUpdateNeeded = false
1260
- this.isLinksUpdateNeeded = false
1261
- this.isLinkColorUpdateNeeded = false
1262
- this.isLinkWidthUpdateNeeded = false
1263
- this.isLinkArrowUpdateNeeded = false
1264
- this.isPointClusterUpdateNeeded = false
1265
- this.isForceManyBodyUpdateNeeded = false
1266
- this.isForceLinkUpdateNeeded = false
1267
- this.isForceCenterUpdateNeeded = false
1268
- }
1269
-
1270
- /**
1271
- * Converts an array of tuple positions to a single array containing all coordinates sequentially
1272
- * @param pointPositions An array of tuple positions
1273
- * @returns A flatten array of coordinates
1274
- */
1275
- public flatten (pointPositions: [number, number][]): number[] {
1276
- return pointPositions.flat()
1277
- }
1278
-
1279
- /**
1280
- * Converts a flat array of point positions to a tuple pairs representing coordinates
1281
- * @param pointPositions A flattened array of coordinates
1282
- * @returns An array of tuple positions
1283
- */
1284
- public pair (pointPositions: number[]): [number, number][] {
1285
- const arr = new Array(pointPositions.length / 2) as [number, number][]
1286
- for (let i = 0; i < pointPositions.length / 2; i++) {
1287
- arr[i] = [pointPositions[i * 2] as number, pointPositions[i * 2 + 1] as number]
1288
- }
1289
-
1290
- return arr
1291
- }
1292
-
1293
- private update (simulationAlpha = this.store.alpha): void {
1294
- const { graph } = this
1295
- this.store.pointsTextureSize = Math.ceil(Math.sqrt(graph.pointsNumber ?? 0))
1296
- this.store.linksTextureSize = Math.ceil(Math.sqrt((graph.linksNumber ?? 0) * 2))
1297
- this.create()
1298
- this.initPrograms()
1299
- this.store.hoveredPoint = undefined
1300
- this.start(simulationAlpha)
1301
- }
1302
-
1303
- private initPrograms (): void {
1304
- if (this._isDestroyed || !this.points || !this.lines || !this.clusters) return
1305
- this.points.initPrograms()
1306
- this.lines.initPrograms()
1307
- this.forceGravity?.initPrograms()
1308
- this.forceLinkIncoming?.initPrograms()
1309
- this.forceLinkOutgoing?.initPrograms()
1310
- this.forceMouse?.initPrograms()
1311
- this.forceManyBody?.initPrograms()
1312
- this.forceCenter?.initPrograms()
1313
- this.clusters.initPrograms()
1314
- }
1315
-
1316
- private frame (): void {
1317
- if (this._isDestroyed) return
1318
- const { config: { simulationGravity, simulationCenter, renderLinks, enableSimulation }, store: { alpha, isSimulationRunning } } = this
1319
- if (alpha < ALPHA_MIN && isSimulationRunning) this.end()
1320
- if (!this.store.pointsTextureSize) return
1321
-
1322
- this.requestAnimationFrameId = window.requestAnimationFrame((now) => {
1323
- this.fpsMonitor?.begin()
1324
- this.resizeCanvas()
1325
- if (!this.dragInstance.isActive) {
1326
- this.findHoveredItem()
1327
- }
1328
-
1329
- if (enableSimulation) {
1330
- if (this.isRightClickMouse && this.config.enableRightClickRepulsion) {
1331
- this.forceMouse?.run()
1332
- this.points?.updatePosition()
1333
- }
1334
- if ((isSimulationRunning && !(this.zoomInstance.isRunning && !this.config.enableSimulationDuringZoom))) {
1335
- if (simulationGravity) {
1336
- this.forceGravity?.run()
1337
- this.points?.updatePosition()
1338
- }
1339
-
1340
- if (simulationCenter) {
1341
- this.forceCenter?.run()
1342
- this.points?.updatePosition()
1343
- }
1344
-
1345
- this.forceManyBody?.run()
1346
- this.points?.updatePosition()
1347
-
1348
- if (this.store.linksTextureSize) {
1349
- this.forceLinkIncoming?.run()
1350
- this.points?.updatePosition()
1351
- this.forceLinkOutgoing?.run()
1352
- this.points?.updatePosition()
1353
- }
1354
-
1355
- if (this.graph.pointClusters || this.graph.clusterPositions) {
1356
- this.clusters?.run()
1357
- this.points?.updatePosition()
1358
- }
1359
-
1360
- this.store.alpha += this.store.addAlpha(this.config.simulationDecay ?? defaultConfigValues.simulation.decay)
1361
- if (this.isRightClickMouse && this.config.enableRightClickRepulsion) this.store.alpha = Math.max(this.store.alpha, 0.1)
1362
- this.store.simulationProgress = Math.sqrt(Math.min(1, ALPHA_MIN / this.store.alpha))
1363
- this.config.onSimulationTick?.(
1364
- this.store.alpha,
1365
- this.store.hoveredPoint?.index,
1366
- this.store.hoveredPoint?.position
1367
- )
1368
- }
1369
-
1370
- this.points?.trackPoints()
1371
- }
1372
-
1373
- // Clear canvas
1374
- this.reglInstance?.clear({
1375
- color: this.store.backgroundColor,
1376
- depth: 1,
1377
- stencil: 0,
1378
- })
1379
-
1380
- if (renderLinks && this.store.linksTextureSize) {
1381
- this.lines?.draw()
1382
- }
1383
-
1384
- this.points?.draw()
1385
- if (this.dragInstance.isActive) {
1386
- // To prevent the dragged point from suddenly jumping, run the drag function twice
1387
- this.points?.drag()
1388
- this.points?.drag()
1389
- // Update tracked positions after drag, even when simulation is disabled
1390
- this.points?.trackPoints()
1391
- }
1392
- this.fpsMonitor?.end(now)
1393
-
1394
- this.currentEvent = undefined
1395
- if (!this._isDestroyed) {
1396
- this.frame()
1397
- }
1398
- })
1399
- }
1400
-
1401
- private stopFrames (): void {
1402
- if (this.requestAnimationFrameId) window.cancelAnimationFrame(this.requestAnimationFrameId)
1403
- }
1404
-
1405
- private end (): void {
1406
- this.store.isSimulationRunning = false
1407
- this.store.simulationProgress = 1
1408
- this.config.onSimulationEnd?.()
1409
- }
1410
-
1411
- private onClick (event: MouseEvent): void {
1412
- this.config.onClick?.(
1413
- this.store.hoveredPoint?.index,
1414
- this.store.hoveredPoint?.position,
1415
- event
1416
- )
1417
-
1418
- if (this.store.hoveredPoint) {
1419
- this.config.onPointClick?.(
1420
- this.store.hoveredPoint.index,
1421
- this.store.hoveredPoint.position,
1422
- event
1423
- )
1424
- } else if (this.store.hoveredLinkIndex !== undefined) {
1425
- this.config.onLinkClick?.(
1426
- this.store.hoveredLinkIndex,
1427
- event
1428
- )
1429
- } else {
1430
- this.config.onBackgroundClick?.(
1431
- event
1432
- )
1433
- }
1434
- }
1435
-
1436
- private updateMousePosition (event: MouseEvent | D3DragEvent<HTMLCanvasElement, undefined, Hovered>): void {
1437
- if (!event) return
1438
- const mouseX = (event as MouseEvent).offsetX ?? (event as D3DragEvent<HTMLCanvasElement, undefined, Hovered>).x
1439
- const mouseY = (event as MouseEvent).offsetY ?? (event as D3DragEvent<HTMLCanvasElement, undefined, Hovered>).y
1440
- if (mouseX === undefined || mouseY === undefined) return
1441
- this.store.mousePosition = this.zoomInstance.convertScreenToSpacePosition([mouseX, mouseY])
1442
- this.store.screenMousePosition = [mouseX, (this.store.screenSize[1] - mouseY)]
1443
- }
1444
-
1445
- private onMouseMove (event: MouseEvent): void {
1446
- this.currentEvent = event
1447
- this.updateMousePosition(event)
1448
- this.isRightClickMouse = event.which === 3
1449
- this.config.onMouseMove?.(
1450
- this.store.hoveredPoint?.index,
1451
- this.store.hoveredPoint?.position,
1452
- this.currentEvent
1453
- )
1454
- }
1455
-
1456
- private onRightClickMouse (event: MouseEvent): void {
1457
- event.preventDefault()
1458
- }
1459
-
1460
- private resizeCanvas (forceResize = false): void {
1461
- if (this._isDestroyed) return
1462
- const prevWidth = this.canvas.width
1463
- const prevHeight = this.canvas.height
1464
- const w = this.canvas.clientWidth
1465
- const h = this.canvas.clientHeight
1466
-
1467
- if (forceResize || prevWidth !== w * this.config.pixelRatio || prevHeight !== h * this.config.pixelRatio) {
1468
- const [prevW, prevH] = this.store.screenSize
1469
- const { k } = this.zoomInstance.eventTransform
1470
- const centerPosition = this.zoomInstance.convertScreenToSpacePosition([prevW / 2, prevH / 2])
1471
-
1472
- this.store.updateScreenSize(w, h)
1473
- this.canvas.width = w * this.config.pixelRatio
1474
- this.canvas.height = h * this.config.pixelRatio
1475
- this.reglInstance?.poll()
1476
- this.canvasD3Selection
1477
- ?.call(this.zoomInstance.behavior.transform, this.zoomInstance.getTransform([centerPosition], k))
1478
- this.points?.updateSampledPointsGrid()
1479
- // Only update link index FBO if link hovering is enabled
1480
- if (this.store.isLinkHoveringEnabled) {
1481
- this.lines?.updateLinkIndexFbo()
1482
- }
1483
- }
1484
- }
1485
-
1486
- private setZoomTransformByPointPositions (positions: number[], duration = 250, scale?: number, padding?: number): void {
1487
- this.resizeCanvas()
1488
- const transform = this.zoomInstance.getTransform(this.pair(positions), scale, padding)
1489
- this.canvasD3Selection
1490
- ?.transition()
1491
- .ease(easeQuadInOut)
1492
- .duration(duration)
1493
- .call(this.zoomInstance.behavior.transform, transform)
1494
- }
1495
-
1496
- private updateZoomDragBehaviors (): void {
1497
- if (this.config.enableDrag) {
1498
- this.canvasD3Selection?.call(this.dragInstance.behavior)
1499
- } else {
1500
- this.canvasD3Selection
1501
- ?.call(this.dragInstance.behavior)
1502
- .on('.drag', null)
1503
- }
1504
-
1505
- if (this.config.enableZoom) {
1506
- this.canvasD3Selection?.call(this.zoomInstance.behavior)
1507
- } else {
1508
- this.canvasD3Selection
1509
- ?.call(this.zoomInstance.behavior)
1510
- .on('wheel.zoom', null)
1511
- }
1512
- }
1513
-
1514
- private findHoveredItem (): void {
1515
- if (this._isDestroyed || !this._isMouseOnCanvas || !this.reglInstance) return
1516
- if (this._findHoveredItemExecutionCount < MAX_HOVER_DETECTION_DELAY) {
1517
- this._findHoveredItemExecutionCount += 1
1518
- return
1519
- }
1520
- this._findHoveredItemExecutionCount = 0
1521
- this.findHoveredPoint()
1522
-
1523
- if (this.graph.linksNumber && this.store.isLinkHoveringEnabled) {
1524
- this.findHoveredLine()
1525
- } else if (this.store.hoveredLinkIndex !== undefined) {
1526
- // Clear stale hoveredLinkIndex when there are no links
1527
- const wasHovered = this.store.hoveredLinkIndex !== undefined
1528
- this.store.hoveredLinkIndex = undefined
1529
- if (wasHovered && this.config.onLinkMouseOut) {
1530
- this.config.onLinkMouseOut(this.currentEvent)
1531
- }
1532
- }
1533
-
1534
- this.updateCanvasCursor()
1535
- }
1536
-
1537
- private findHoveredPoint (): void {
1538
- if (this._isDestroyed || !this.reglInstance || !this.points) return
1539
- this.points.findHoveredPoint()
1540
- let isMouseover = false
1541
- let isMouseout = false
1542
- const pixels = readPixels(this.reglInstance, this.points.hoveredFbo as regl.Framebuffer2D)
1543
- const pointSize = pixels[1] as number
1544
- if (pointSize) {
1545
- const hoveredIndex = pixels[0] as number
1546
- if (this.store.hoveredPoint?.index !== hoveredIndex) isMouseover = true
1547
- const pointX = pixels[2] as number
1548
- const pointY = pixels[3] as number
1549
- this.store.hoveredPoint = {
1550
- index: hoveredIndex,
1551
- position: [pointX, pointY],
1552
- }
1553
- } else {
1554
- if (this.store.hoveredPoint) isMouseout = true
1555
- this.store.hoveredPoint = undefined
1556
- }
1557
-
1558
- if (isMouseover && this.store.hoveredPoint) {
1559
- this.config.onPointMouseOver?.(
1560
- this.store.hoveredPoint.index,
1561
- this.store.hoveredPoint.position,
1562
- this.currentEvent
1563
- )
1564
- }
1565
- if (isMouseout) this.config.onPointMouseOut?.(this.currentEvent)
1566
- }
1567
-
1568
- private findHoveredLine (): void {
1569
- if (this._isDestroyed || !this.reglInstance || !this.lines) return
1570
- if (this.store.hoveredPoint) {
1571
- if (this.store.hoveredLinkIndex !== undefined) {
1572
- this.store.hoveredLinkIndex = undefined
1573
- this.config.onLinkMouseOut?.(this.currentEvent)
1574
- }
1575
- return
1576
- }
1577
- this.lines.findHoveredLine()
1578
- let isMouseover = false
1579
- let isMouseout = false
1580
-
1581
- const pixels = readPixels(this.reglInstance, this.lines.hoveredLineIndexFbo as regl.Framebuffer2D)
1582
- const hoveredLineIndex = pixels[0] as number
1583
-
1584
- if (hoveredLineIndex >= 0) {
1585
- if (this.store.hoveredLinkIndex !== hoveredLineIndex) isMouseover = true
1586
- this.store.hoveredLinkIndex = hoveredLineIndex
1587
- } else {
1588
- if (this.store.hoveredLinkIndex !== undefined) isMouseout = true
1589
- this.store.hoveredLinkIndex = undefined
1590
- }
1591
-
1592
- if (isMouseover && this.store.hoveredLinkIndex !== undefined) {
1593
- this.config.onLinkMouseOver?.(this.store.hoveredLinkIndex)
1594
- }
1595
- if (isMouseout) this.config.onLinkMouseOut?.(this.currentEvent)
1596
- }
1597
-
1598
- private updateCanvasCursor (): void {
1599
- const { hoveredPointCursor, hoveredLinkCursor } = this.config
1600
- if (this.dragInstance.isActive) select(this.canvas).style('cursor', 'grabbing')
1601
- else if (this.store.hoveredPoint) {
1602
- if (!this.config.enableDrag || this.store.isSpaceKeyPressed) select(this.canvas).style('cursor', hoveredPointCursor)
1603
- else select(this.canvas).style('cursor', 'grab')
1604
- } else if (this.store.isLinkHoveringEnabled && this.store.hoveredLinkIndex !== undefined) {
1605
- select(this.canvas).style('cursor', hoveredLinkCursor)
1606
- } else select(this.canvas).style('cursor', null)
1607
- }
1608
-
1609
- private addAttribution (): void {
1610
- if (!this.config.attribution) return
1611
- this.attributionDivElement = document.createElement('div')
1612
- this.attributionDivElement.style.cssText = `
1613
- user-select: none;
1614
- position: absolute;
1615
- bottom: 0;
1616
- right: 0;
1617
- color: var(--cosmosgl-attribution-color);
1618
- margin: 0 0.6rem 0.6rem 0;
1619
- font-size: 0.7rem;
1620
- font-family: inherit;
1621
- `
1622
- // Sanitize the attribution HTML content to prevent XSS attacks
1623
- // Use more permissive settings for attribution since it's controlled by the library user
1624
- this.attributionDivElement.innerHTML = sanitizeHtml(this.config.attribution, {
1625
- ALLOWED_TAGS: ['a', 'b', 'i', 'em', 'strong', 'span', 'div', 'p', 'br', 'img'],
1626
- ALLOWED_ATTR: ['href', 'target', 'class', 'id', 'style', 'src', 'alt', 'title'],
1627
- })
1628
- this.store.div?.appendChild(this.attributionDivElement)
1629
- }
1630
- }
1631
-
1632
- export type { GraphConfigInterface } from './config'
1633
- export { PointShape } from './modules/GraphData'
1634
-
1635
- export * from './helper'