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