force-graph 1.42.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/dist/force-graph.common.js +1754 -0
  4. package/dist/force-graph.d.ts +195 -0
  5. package/dist/force-graph.js +12168 -0
  6. package/dist/force-graph.js.map +1 -0
  7. package/dist/force-graph.min.js +5 -0
  8. package/dist/force-graph.module.js +1743 -0
  9. package/example/auto-colored/index.html +34 -0
  10. package/example/basic/index.html +29 -0
  11. package/example/build-a-graph/index.html +108 -0
  12. package/example/click-to-focus/index.html +28 -0
  13. package/example/collision-detection/index.html +50 -0
  14. package/example/curved-links/index.html +37 -0
  15. package/example/curved-links-computed-curvature/index.html +76 -0
  16. package/example/custom-node-shape/index.html +44 -0
  17. package/example/dag-yarn/index.html +96 -0
  18. package/example/dagre/index.html +119 -0
  19. package/example/dash-odd-links/index.html +47 -0
  20. package/example/datasets/blocks.json +1 -0
  21. package/example/datasets/d3-dependencies.csv +464 -0
  22. package/example/datasets/miserables.json +337 -0
  23. package/example/datasets/mplate.mtx +74090 -0
  24. package/example/directional-links-arrows/index.html +29 -0
  25. package/example/directional-links-particles/index.html +22 -0
  26. package/example/dynamic/index.html +42 -0
  27. package/example/emit-particles/index.html +50 -0
  28. package/example/expandable-nodes/index.html +66 -0
  29. package/example/expandable-tree/index.html +85 -0
  30. package/example/fit-to-canvas/index.html +34 -0
  31. package/example/fix-dragged-nodes/index.html +24 -0
  32. package/example/highlight/index.html +84 -0
  33. package/example/huge-1M/index.html +37 -0
  34. package/example/img-nodes/imgs/cat.jpg +0 -0
  35. package/example/img-nodes/imgs/dog.jpg +0 -0
  36. package/example/img-nodes/imgs/eagle.jpg +0 -0
  37. package/example/img-nodes/imgs/elephant.jpg +0 -0
  38. package/example/img-nodes/imgs/grasshopper.jpg +0 -0
  39. package/example/img-nodes/imgs/octopus.jpg +0 -0
  40. package/example/img-nodes/imgs/owl.jpg +0 -0
  41. package/example/img-nodes/imgs/panda.jpg +0 -0
  42. package/example/img-nodes/imgs/squirrel.jpg +0 -0
  43. package/example/img-nodes/imgs/tiger.jpg +0 -0
  44. package/example/img-nodes/imgs/whale.jpg +0 -0
  45. package/example/img-nodes/index.html +43 -0
  46. package/example/large-graph/index.html +41 -0
  47. package/example/load-json/index.html +24 -0
  48. package/example/medium-graph/index.html +26 -0
  49. package/example/medium-graph/preview.png +0 -0
  50. package/example/move-viewport/index.html +42 -0
  51. package/example/multi-selection/index.html +57 -0
  52. package/example/responsive/index.html +37 -0
  53. package/example/text-links/index.html +69 -0
  54. package/example/text-nodes/index.html +42 -0
  55. package/example/tree/index.html +71 -0
  56. package/package.json +72 -0
  57. package/src/canvas-force-graph.js +544 -0
  58. package/src/color-utils.js +17 -0
  59. package/src/dagDepths.js +51 -0
  60. package/src/force-graph.css +35 -0
  61. package/src/force-graph.js +644 -0
  62. package/src/index.d.ts +195 -0
  63. package/src/index.js +3 -0
  64. package/src/kapsule-link.js +34 -0
@@ -0,0 +1,644 @@
1
+ import { select as d3Select } from 'd3-selection';
2
+ import { zoom as d3Zoom, zoomTransform as d3ZoomTransform } from 'd3-zoom';
3
+ import { drag as d3Drag } from 'd3-drag';
4
+ import { max as d3Max, min as d3Min } from 'd3-array';
5
+ import throttle from 'lodash.throttle';
6
+ import TWEEN from '@tweenjs/tween.js';
7
+ import Kapsule from 'kapsule';
8
+ import accessorFn from 'accessor-fn';
9
+ import ColorTracker from 'canvas-color-tracker';
10
+
11
+ import CanvasForceGraph from './canvas-force-graph';
12
+ import linkKapsule from './kapsule-link.js';
13
+
14
+ const HOVER_CANVAS_THROTTLE_DELAY = 800; // ms to throttle shadow canvas updates for perf improvement
15
+ const ZOOM2NODES_FACTOR = 4;
16
+
17
+ // Expose config from forceGraph
18
+ const bindFG = linkKapsule('forceGraph', CanvasForceGraph);
19
+ const bindBoth = linkKapsule(['forceGraph', 'shadowGraph'], CanvasForceGraph);
20
+ const linkedProps = Object.assign(
21
+ ...[
22
+ 'nodeColor',
23
+ 'nodeAutoColorBy',
24
+ 'nodeCanvasObject',
25
+ 'nodeCanvasObjectMode',
26
+ 'linkColor',
27
+ 'linkAutoColorBy',
28
+ 'linkLineDash',
29
+ 'linkWidth',
30
+ 'linkCanvasObject',
31
+ 'linkCanvasObjectMode',
32
+ 'linkDirectionalArrowLength',
33
+ 'linkDirectionalArrowColor',
34
+ 'linkDirectionalArrowRelPos',
35
+ 'linkDirectionalParticles',
36
+ 'linkDirectionalParticleSpeed',
37
+ 'linkDirectionalParticleWidth',
38
+ 'linkDirectionalParticleColor',
39
+ 'dagMode',
40
+ 'dagLevelDistance',
41
+ 'dagNodeFilter',
42
+ 'onDagError',
43
+ 'd3AlphaMin',
44
+ 'd3AlphaDecay',
45
+ 'd3VelocityDecay',
46
+ 'warmupTicks',
47
+ 'cooldownTicks',
48
+ 'cooldownTime',
49
+ 'onEngineTick',
50
+ 'onEngineStop'
51
+ ].map(p => ({ [p]: bindFG.linkProp(p)})),
52
+ ...[
53
+ 'nodeRelSize',
54
+ 'nodeId',
55
+ 'nodeVal',
56
+ 'nodeVisibility',
57
+ 'linkSource',
58
+ 'linkTarget',
59
+ 'linkVisibility',
60
+ 'linkCurvature'
61
+ ].map(p => ({ [p]: bindBoth.linkProp(p)}))
62
+ );
63
+ const linkedMethods = Object.assign(...[
64
+ 'd3Force',
65
+ 'd3ReheatSimulation',
66
+ 'emitParticle'
67
+ ].map(p => ({ [p]: bindFG.linkMethod(p)})));
68
+
69
+ function adjustCanvasSize(state) {
70
+ if (state.canvas) {
71
+ let curWidth = state.canvas.width;
72
+ let curHeight = state.canvas.height;
73
+ if (curWidth === 300 && curHeight === 150) { // Default canvas dimensions
74
+ curWidth = curHeight = 0;
75
+ }
76
+
77
+ const pxScale = window.devicePixelRatio; // 2 on retina displays
78
+ curWidth /= pxScale;
79
+ curHeight /= pxScale;
80
+
81
+ // Resize canvases
82
+ [state.canvas, state.shadowCanvas].forEach(canvas => {
83
+ // Element size
84
+ canvas.style.width = `${state.width}px`;
85
+ canvas.style.height = `${state.height}px`;
86
+
87
+ // Memory size (scaled to avoid blurriness)
88
+ canvas.width = state.width * pxScale;
89
+ canvas.height = state.height * pxScale;
90
+
91
+ // Normalize coordinate system to use css pixels (on init only)
92
+ if (!curWidth && !curHeight) {
93
+ canvas.getContext('2d').scale(pxScale, pxScale);
94
+ }
95
+ });
96
+
97
+ // Relative center panning based on 0,0
98
+ const k = d3ZoomTransform(state.canvas).k;
99
+ state.zoom.translateBy(state.zoom.__baseElem,
100
+ (state.width - curWidth) / 2 / k,
101
+ (state.height - curHeight) / 2 / k
102
+ );
103
+ state.needsRedraw = true;
104
+ }
105
+ }
106
+
107
+ function resetTransform(ctx) {
108
+ const pxRatio = window.devicePixelRatio;
109
+ ctx.setTransform(pxRatio, 0, 0, pxRatio, 0, 0);
110
+ }
111
+
112
+ function clearCanvas(ctx, width, height) {
113
+ ctx.save();
114
+ resetTransform(ctx); // reset transform
115
+ ctx.clearRect(0, 0, width, height);
116
+ ctx.restore(); //restore transforms
117
+ }
118
+
119
+ //
120
+
121
+ export default Kapsule({
122
+ props:{
123
+ width: { default: window.innerWidth, onChange: (_, state) => adjustCanvasSize(state), triggerUpdate: false } ,
124
+ height: { default: window.innerHeight, onChange: (_, state) => adjustCanvasSize(state), triggerUpdate: false },
125
+ graphData: {
126
+ default: { nodes: [], links: [] },
127
+ onChange: ((d, state) => {
128
+ [{ type: 'Node', objs: d.nodes }, { type: 'Link', objs: d.links }].forEach(hexIndex);
129
+ state.forceGraph.graphData(d);
130
+ state.shadowGraph.graphData(d);
131
+
132
+ function hexIndex({ type, objs }) {
133
+ objs
134
+ .filter(d => {
135
+ if (!d.hasOwnProperty('__indexColor')) return true;
136
+ const cur = state.colorTracker.lookup(d.__indexColor);
137
+ return (!cur || !cur.hasOwnProperty('d') || cur.d !== d);
138
+ })
139
+ .forEach(d => {
140
+ // store object lookup color
141
+ d.__indexColor = state.colorTracker.register({ type, d });
142
+ });
143
+ }
144
+ }),
145
+ triggerUpdate: false
146
+ },
147
+ backgroundColor: { onChange(color, state) { state.canvas && color && (state.canvas.style.background = color) }, triggerUpdate: false },
148
+ nodeLabel: { default: 'name', triggerUpdate: false },
149
+ nodePointerAreaPaint: { onChange(paintFn, state) {
150
+ state.shadowGraph.nodeCanvasObject(!paintFn ? null :
151
+ (node, ctx, globalScale) => paintFn(node, node.__indexColor, ctx, globalScale)
152
+ );
153
+ state.flushShadowCanvas && state.flushShadowCanvas();
154
+ }, triggerUpdate: false },
155
+ linkPointerAreaPaint: { onChange(paintFn, state) {
156
+ state.shadowGraph.linkCanvasObject(!paintFn ? null :
157
+ (link, ctx, globalScale) => paintFn(link, link.__indexColor, ctx, globalScale)
158
+ );
159
+ state.flushShadowCanvas && state.flushShadowCanvas();
160
+ }, triggerUpdate: false },
161
+ linkLabel: { default: 'name', triggerUpdate: false },
162
+ linkHoverPrecision: { default: 4, triggerUpdate: false },
163
+ minZoom: { default: 0.01, onChange(minZoom, state) { state.zoom.scaleExtent([minZoom, state.zoom.scaleExtent()[1]]); }, triggerUpdate: false },
164
+ maxZoom: { default: 1000, onChange(maxZoom, state) { state.zoom.scaleExtent([state.zoom.scaleExtent()[0], maxZoom]) }, triggerUpdate: false },
165
+ enableNodeDrag: { default: true, triggerUpdate: false },
166
+ enableZoomInteraction: { default: true, triggerUpdate: false },
167
+ enablePanInteraction: { default: true, triggerUpdate: false },
168
+ enableZoomPanInteraction: { default: true, triggerUpdate: false }, // to be deprecated
169
+ enablePointerInteraction: { default: true, onChange(_, state) { state.hoverObj = null; }, triggerUpdate: false },
170
+ autoPauseRedraw: { default: true, triggerUpdate: false },
171
+ onNodeDrag: { default: () => {}, triggerUpdate: false },
172
+ onNodeDragEnd: { default: () => {}, triggerUpdate: false },
173
+ onNodeClick: { triggerUpdate: false },
174
+ onNodeRightClick: { triggerUpdate: false },
175
+ onNodeHover: { triggerUpdate: false },
176
+ onLinkClick: { triggerUpdate: false },
177
+ onLinkRightClick: { triggerUpdate: false },
178
+ onLinkHover: { triggerUpdate: false },
179
+ onBackgroundClick: { triggerUpdate: false },
180
+ onBackgroundRightClick: { triggerUpdate: false },
181
+ onZoom: { default: () => {}, triggerUpdate: false },
182
+ onZoomEnd: { default: () => {}, triggerUpdate: false },
183
+ onRenderFramePre: { triggerUpdate: false },
184
+ onRenderFramePost: { triggerUpdate: false },
185
+ ...linkedProps
186
+ },
187
+
188
+ aliases: { // Prop names supported for backwards compatibility
189
+ stopAnimation: 'pauseAnimation'
190
+ },
191
+
192
+ methods: {
193
+ graph2ScreenCoords: function(state, x, y) {
194
+ const t = d3ZoomTransform(state.canvas);
195
+ return { x: x * t.k + t.x, y: y * t.k + t.y };
196
+ },
197
+ screen2GraphCoords: function(state, x, y) {
198
+ const t = d3ZoomTransform(state.canvas);
199
+ return { x: (x - t.x) / t.k, y: (y - t.y) / t.k };
200
+ },
201
+ centerAt: function(state, x, y, transitionDuration) {
202
+ if (!state.canvas) return null; // no canvas yet
203
+
204
+ // setter
205
+ if (x !== undefined || y !== undefined) {
206
+ const finalPos = Object.assign({},
207
+ x !== undefined ? { x } : {},
208
+ y !== undefined ? { y } : {}
209
+ );
210
+ if (!transitionDuration) { // no animation
211
+ setCenter(finalPos);
212
+ } else {
213
+ new TWEEN.Tween(getCenter())
214
+ .to(finalPos, transitionDuration)
215
+ .easing(TWEEN.Easing.Quadratic.Out)
216
+ .onUpdate(setCenter)
217
+ .start();
218
+ }
219
+ return this;
220
+ }
221
+
222
+ // getter
223
+ return getCenter();
224
+
225
+ //
226
+
227
+ function getCenter() {
228
+ const t = d3ZoomTransform(state.canvas);
229
+ return { x: (state.width / 2 - t.x) / t.k, y: (state.height / 2 - t.y) / t.k };
230
+ }
231
+
232
+ function setCenter({ x, y }) {
233
+ state.zoom.translateTo(
234
+ state.zoom.__baseElem,
235
+ x === undefined ? getCenter().x : x,
236
+ y === undefined ? getCenter().y : y
237
+ );
238
+ state.needsRedraw = true;
239
+ }
240
+ },
241
+ zoom: function(state, k, transitionDuration) {
242
+ if (!state.canvas) return null; // no canvas yet
243
+
244
+ // setter
245
+ if (k !== undefined) {
246
+ if (!transitionDuration) { // no animation
247
+ setZoom(k);
248
+ } else {
249
+ new TWEEN.Tween({ k: getZoom() })
250
+ .to({ k }, transitionDuration)
251
+ .easing(TWEEN.Easing.Quadratic.Out)
252
+ .onUpdate(({ k }) => setZoom(k))
253
+ .start();
254
+ }
255
+ return this;
256
+ }
257
+
258
+ // getter
259
+ return getZoom();
260
+
261
+ //
262
+
263
+ function getZoom() {
264
+ return d3ZoomTransform(state.canvas).k;
265
+ }
266
+
267
+ function setZoom(k) {
268
+ state.zoom.scaleTo(state.zoom.__baseElem, k);
269
+ state.needsRedraw = true;
270
+ }
271
+ },
272
+ zoomToFit: function(state, transitionDuration = 0, padding = 10, ...bboxArgs) {
273
+ const bbox = this.getGraphBbox(...bboxArgs);
274
+
275
+ if (bbox) {
276
+ const center = {
277
+ x: (bbox.x[0] + bbox.x[1]) / 2,
278
+ y: (bbox.y[0] + bbox.y[1]) / 2,
279
+ };
280
+
281
+ const zoomK = Math.max(1e-12, Math.min(1e12,
282
+ (state.width - padding * 2) / (bbox.x[1] - bbox.x[0]),
283
+ (state.height - padding * 2) / (bbox.y[1] - bbox.y[0]))
284
+ );
285
+
286
+ this.centerAt(center.x, center.y, transitionDuration);
287
+ this.zoom(zoomK, transitionDuration);
288
+ }
289
+
290
+ return this;
291
+ },
292
+ getGraphBbox: function(state, nodeFilter = () => true) {
293
+ const getVal = accessorFn(state.nodeVal);
294
+ const getR = node => Math.sqrt(Math.max(0, getVal(node) || 1)) * state.nodeRelSize;
295
+
296
+ const nodesPos = state.graphData.nodes.filter(nodeFilter).map(node => ({
297
+ x: node.x,
298
+ y: node.y,
299
+ r: getR(node)
300
+ }));
301
+
302
+ return !nodesPos.length ? null : {
303
+ x: [
304
+ d3Min(nodesPos, node => node.x - node.r),
305
+ d3Max(nodesPos, node => node.x + node.r)
306
+ ],
307
+ y: [
308
+ d3Min(nodesPos, node => node.y - node.r),
309
+ d3Max(nodesPos, node => node.y + node.r)
310
+ ]
311
+ };
312
+ },
313
+ pauseAnimation: function(state) {
314
+ if (state.animationFrameRequestId) {
315
+ cancelAnimationFrame(state.animationFrameRequestId);
316
+ state.animationFrameRequestId = null;
317
+ }
318
+ return this;
319
+ },
320
+ resumeAnimation: function(state) {
321
+ if (!state.animationFrameRequestId) {
322
+ this._animationCycle();
323
+ }
324
+ return this;
325
+ },
326
+ _destructor: function() {
327
+ this.pauseAnimation();
328
+ this.graphData({ nodes: [], links: []});
329
+ },
330
+ ...linkedMethods
331
+ },
332
+
333
+ stateInit: () => ({
334
+ lastSetZoom: 1,
335
+ zoom: d3Zoom(),
336
+ forceGraph: new CanvasForceGraph(),
337
+ shadowGraph: new CanvasForceGraph()
338
+ .cooldownTicks(0)
339
+ .nodeColor('__indexColor')
340
+ .linkColor('__indexColor')
341
+ .isShadow(true),
342
+ colorTracker: new ColorTracker() // indexed objects for rgb lookup
343
+ }),
344
+
345
+ init: function(domNode, state) {
346
+ // Wipe DOM
347
+ domNode.innerHTML = '';
348
+
349
+ // Container anchor for canvas and tooltip
350
+ const container = document.createElement('div');
351
+ container.classList.add('force-graph-container');
352
+ container.style.position = 'relative';
353
+ domNode.appendChild(container);
354
+
355
+ state.canvas = document.createElement('canvas');
356
+ if (state.backgroundColor) state.canvas.style.background = state.backgroundColor;
357
+ container.appendChild(state.canvas);
358
+
359
+ state.shadowCanvas = document.createElement('canvas');
360
+
361
+ // Show shadow canvas
362
+ //state.shadowCanvas.style.position = 'absolute';
363
+ //state.shadowCanvas.style.top = '0';
364
+ //state.shadowCanvas.style.left = '0';
365
+ //container.appendChild(state.shadowCanvas);
366
+
367
+ const ctx = state.canvas.getContext('2d');
368
+ const shadowCtx = state.shadowCanvas.getContext('2d');
369
+
370
+ const pointerPos = { x: -1e12, y: -1e12 };
371
+ const getObjUnderPointer = () => {
372
+ let obj = null;
373
+ const pxScale = window.devicePixelRatio;
374
+ const px = (pointerPos.x > 0 && pointerPos.y > 0)
375
+ ? shadowCtx.getImageData(pointerPos.x * pxScale, pointerPos.y * pxScale, 1, 1)
376
+ : null;
377
+ // Lookup object per pixel color
378
+ px && (obj = state.colorTracker.lookup(px.data));
379
+ return obj;
380
+ };
381
+
382
+ // Setup node drag interaction
383
+ d3Select(state.canvas).call(
384
+ d3Drag()
385
+ .subject(() => {
386
+ if (!state.enableNodeDrag) { return null; }
387
+ const obj = getObjUnderPointer();
388
+ return (obj && obj.type === 'Node') ? obj.d : null; // Only drag nodes
389
+ })
390
+ .on('start', ev => {
391
+ const obj = ev.subject;
392
+ obj.__initialDragPos = { x: obj.x, y: obj.y, fx: obj.fx, fy: obj.fy };
393
+
394
+ // keep engine running at low intensity throughout drag
395
+ if (!ev.active) {
396
+ obj.fx = obj.x; obj.fy = obj.y; // Fix points
397
+ }
398
+
399
+ // drag cursor
400
+ state.canvas.classList.add('grabbable');
401
+ })
402
+ .on('drag', ev => {
403
+ const obj = ev.subject;
404
+ const initPos = obj.__initialDragPos;
405
+ const dragPos = ev;
406
+
407
+ const k = d3ZoomTransform(state.canvas).k;
408
+ const translate = {
409
+ x: (initPos.x + (dragPos.x - initPos.x) / k) - obj.x,
410
+ y: (initPos.y + (dragPos.y - initPos.y) / k) - obj.y
411
+ };
412
+
413
+ // Move fx/fy (and x/y) of nodes based on the scaled drag distance since the drag start
414
+ ['x', 'y'].forEach(c => obj[`f${c}`] = obj[c] = initPos[c] + (dragPos[c] - initPos[c]) / k);
415
+
416
+ // prevent freeze while dragging
417
+ state.forceGraph
418
+ .d3AlphaTarget(0.3) // keep engine running at low intensity throughout drag
419
+ .resetCountdown(); // prevent freeze while dragging
420
+
421
+ state.isPointerDragging = true;
422
+
423
+ obj.__dragged = true;
424
+ state.onNodeDrag(obj, translate);
425
+ })
426
+ .on('end', ev => {
427
+ const obj = ev.subject;
428
+ const initPos = obj.__initialDragPos;
429
+ const translate = {x: obj.x - initPos.x, y: obj.y - initPos.y};
430
+
431
+ if (initPos.fx === undefined) { obj.fx = undefined; }
432
+ if (initPos.fy === undefined) { obj.fy = undefined; }
433
+ delete(obj.__initialDragPos);
434
+
435
+ if (state.forceGraph.d3AlphaTarget()) {
436
+ state.forceGraph
437
+ .d3AlphaTarget(0) // release engine low intensity
438
+ .resetCountdown(); // let the engine readjust after releasing fixed nodes
439
+ }
440
+
441
+ // drag cursor
442
+ state.canvas.classList.remove('grabbable');
443
+
444
+ state.isPointerDragging = false;
445
+
446
+ if (obj.__dragged) {
447
+ delete(obj.__dragged);
448
+ state.onNodeDragEnd(obj, translate);
449
+ }
450
+ })
451
+ );
452
+
453
+ // Setup zoom / pan interaction
454
+ state.zoom(state.zoom.__baseElem = d3Select(state.canvas)); // Attach controlling elem for easy access
455
+
456
+ state.zoom.__baseElem.on('dblclick.zoom', null); // Disable double-click to zoom
457
+
458
+ state.zoom
459
+ .filter(ev =>
460
+ // disable zoom interaction
461
+ !ev.button
462
+ && state.enableZoomPanInteraction
463
+ && (state.enableZoomInteraction || ev.type !== 'wheel')
464
+ && (state.enablePanInteraction || ev.type === 'wheel')
465
+ )
466
+ .on('zoom', ev => {
467
+ const t = ev.transform;
468
+ [ctx, shadowCtx].forEach(c => {
469
+ resetTransform(c);
470
+ c.translate(t.x, t.y);
471
+ c.scale(t.k, t.k);
472
+ });
473
+ state.onZoom({ ...t });
474
+ state.needsRedraw = true;
475
+ })
476
+ .on('end', ev => state.onZoomEnd({ ...ev.transform }));
477
+
478
+ adjustCanvasSize(state);
479
+
480
+ state.forceGraph
481
+ .onNeedsRedraw(() => state.needsRedraw = true)
482
+ .onFinishUpdate(() => {
483
+ // re-zoom, if still in default position (not user modified)
484
+ if (d3ZoomTransform(state.canvas).k === state.lastSetZoom && state.graphData.nodes.length) {
485
+ state.zoom.scaleTo(state.zoom.__baseElem,
486
+ state.lastSetZoom = ZOOM2NODES_FACTOR / Math.cbrt(state.graphData.nodes.length)
487
+ );
488
+ state.needsRedraw = true;
489
+ }
490
+ });
491
+
492
+ // Setup tooltip
493
+ const toolTipElem = document.createElement('div');
494
+ toolTipElem.classList.add('graph-tooltip');
495
+ container.appendChild(toolTipElem);
496
+
497
+ // Capture pointer coords on move or touchstart
498
+ ['pointermove', 'pointerdown'].forEach(evType =>
499
+ container.addEventListener(evType, ev => {
500
+ if (evType === 'pointerdown') {
501
+ state.isPointerPressed = true; // track click state
502
+ state.pointerDownEvent = ev;
503
+ }
504
+
505
+ // detect pointer drag on canvas pan
506
+ !state.isPointerDragging && ev.type === 'pointermove'
507
+ && (state.onBackgroundClick) // only bother detecting drags this way if background clicks are enabled (so they don't trigger accidentally on canvas panning)
508
+ && (ev.pressure > 0 || state.isPointerPressed) // ev.pressure always 0 on Safari, so we use the isPointerPressed tracker
509
+ && (ev.pointerType !== 'touch' || ev.movementX === undefined || [ev.movementX, ev.movementY].some(m => Math.abs(m) > 1)) // relax drag trigger sensitivity on touch events
510
+ && (state.isPointerDragging = true);
511
+
512
+ // update the pointer pos
513
+ const offset = getOffset(container);
514
+ pointerPos.x = ev.pageX - offset.left;
515
+ pointerPos.y = ev.pageY - offset.top;
516
+
517
+ // Move tooltip
518
+ toolTipElem.style.top = `${pointerPos.y}px`;
519
+ toolTipElem.style.left = `${pointerPos.x}px`;
520
+
521
+ //
522
+
523
+ function getOffset(el) {
524
+ const rect = el.getBoundingClientRect(),
525
+ scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
526
+ scrollTop = window.pageYOffset || document.documentElement.scrollTop;
527
+ return { top: rect.top + scrollTop, left: rect.left + scrollLeft };
528
+ }
529
+ }, { passive: true })
530
+ );
531
+
532
+ // Handle click/touch events on nodes/links
533
+ container.addEventListener('pointerup', ev => {
534
+ state.isPointerPressed = false;
535
+ if (state.isPointerDragging) {
536
+ state.isPointerDragging = false;
537
+ return; // don't trigger click events after pointer drag (pan / node drag functionality)
538
+ }
539
+
540
+ const cbEvents = [ev, state.pointerDownEvent];
541
+ requestAnimationFrame(() => { // trigger click events asynchronously, to allow hoverObj to be set (on frame)
542
+ if (ev.button === 0) { // mouse left-click or touch
543
+ if (state.hoverObj) {
544
+ const fn = state[`on${state.hoverObj.type}Click`];
545
+ fn && fn(state.hoverObj.d, ...cbEvents);
546
+ } else {
547
+ state.onBackgroundClick && state.onBackgroundClick(...cbEvents);
548
+ }
549
+ }
550
+
551
+ if (ev.button === 2) { // mouse right-click
552
+ if (state.hoverObj) {
553
+ const fn = state[`on${state.hoverObj.type}RightClick`];
554
+ fn && fn(state.hoverObj.d, ...cbEvents);
555
+ } else {
556
+ state.onBackgroundRightClick && state.onBackgroundRightClick(...cbEvents);
557
+ }
558
+ }
559
+ });
560
+ }, { passive: true });
561
+
562
+ container.addEventListener('contextmenu', ev => {
563
+ if (!state.onBackgroundRightClick && !state.onNodeRightClick && !state.onLinkRightClick) return true; // default contextmenu behavior
564
+ ev.preventDefault();
565
+ return false;
566
+ });
567
+
568
+ state.forceGraph(ctx);
569
+ state.shadowGraph(shadowCtx);
570
+
571
+ //
572
+
573
+ const refreshShadowCanvas = throttle(() => {
574
+ // wipe canvas
575
+ clearCanvas(shadowCtx, state.width, state.height);
576
+
577
+ // Adjust link hover area
578
+ state.shadowGraph.linkWidth(l => accessorFn(state.linkWidth)(l) + state.linkHoverPrecision);
579
+
580
+ // redraw
581
+ const t = d3ZoomTransform(state.canvas);
582
+ state.shadowGraph.globalScale(t.k).tickFrame();
583
+ }, HOVER_CANVAS_THROTTLE_DELAY);
584
+ state.flushShadowCanvas = refreshShadowCanvas.flush; // hook to immediately invoke shadow canvas paint
585
+
586
+ // Kick-off renderer
587
+ (this._animationCycle = function animate() { // IIFE
588
+ const doRedraw = !state.autoPauseRedraw || !!state.needsRedraw || state.forceGraph.isEngineRunning()
589
+ || state.graphData.links.some(d => d.__photons && d.__photons.length);
590
+ state.needsRedraw = false;
591
+
592
+ if (state.enablePointerInteraction) {
593
+ // Update tooltip and trigger onHover events
594
+ const obj = !state.isPointerDragging ? getObjUnderPointer() : null; // don't hover during drag
595
+ if (obj !== state.hoverObj) {
596
+ const prevObj = state.hoverObj;
597
+ const prevObjType = prevObj ? prevObj.type : null;
598
+ const objType = obj ? obj.type : null;
599
+
600
+ if (prevObjType && prevObjType !== objType) {
601
+ // Hover out
602
+ const fn = state[`on${prevObjType}Hover`];
603
+ fn && fn(null, prevObj.d);
604
+ }
605
+ if (objType) {
606
+ // Hover in
607
+ const fn = state[`on${objType}Hover`];
608
+ fn && fn(obj.d, prevObjType === objType ? prevObj.d : null);
609
+ }
610
+
611
+ const tooltipContent = obj ? accessorFn(state[`${obj.type.toLowerCase()}Label`])(obj.d) || '' : '';
612
+ toolTipElem.style.visibility = tooltipContent ? 'visible' : 'hidden';
613
+ toolTipElem.innerHTML = tooltipContent;
614
+
615
+ // set pointer if hovered object is clickable
616
+ state.canvas.classList[
617
+ ((obj && state[`on${objType}Click`]) || (!obj && state.onBackgroundClick)) ? 'add' : 'remove'
618
+ ]('clickable');
619
+
620
+ state.hoverObj = obj;
621
+ }
622
+
623
+ doRedraw && refreshShadowCanvas();
624
+ }
625
+
626
+ if(doRedraw) {
627
+ // Wipe canvas
628
+ clearCanvas(ctx, state.width, state.height);
629
+
630
+ // Frame cycle
631
+ const globalScale = d3ZoomTransform(state.canvas).k;
632
+ state.onRenderFramePre && state.onRenderFramePre(ctx, globalScale);
633
+ state.forceGraph.globalScale(globalScale).tickFrame();
634
+ state.onRenderFramePost && state.onRenderFramePost(ctx, globalScale);
635
+ }
636
+
637
+ TWEEN.update(); // update canvas animation tweens
638
+
639
+ state.animationFrameRequestId = requestAnimationFrame(animate);
640
+ })();
641
+ },
642
+
643
+ update: function updateFn(state) {}
644
+ });