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,544 @@
1
+ import {
2
+ forceSimulation as d3ForceSimulation,
3
+ forceLink as d3ForceLink,
4
+ forceManyBody as d3ForceManyBody,
5
+ forceCenter as d3ForceCenter,
6
+ forceRadial as d3ForceRadial
7
+ } from 'd3-force-3d';
8
+
9
+ import { Bezier } from 'bezier-js';
10
+
11
+ import Kapsule from 'kapsule';
12
+ import accessorFn from 'accessor-fn';
13
+ import indexBy from 'index-array-by';
14
+
15
+ import { autoColorObjects } from './color-utils';
16
+ import getDagDepths from './dagDepths';
17
+
18
+ //
19
+
20
+ const DAG_LEVEL_NODE_RATIO = 2;
21
+
22
+ // whenever styling props are changed that require a canvas redraw
23
+ const notifyRedraw = (_, state) => state.onNeedsRedraw && state.onNeedsRedraw();
24
+
25
+ export default Kapsule({
26
+
27
+ props: {
28
+ graphData: {
29
+ default: {
30
+ nodes: [],
31
+ links: []
32
+ },
33
+ onChange(_, state) {
34
+ state.engineRunning = false;
35
+ } // Pause simulation
36
+ },
37
+ dagMode: { onChange(dagMode, state) { // td, bu, lr, rl, radialin, radialout
38
+ !dagMode && (state.graphData.nodes || []).forEach(n => n.fx = n.fy = undefined); // unfix nodes when disabling dag mode
39
+ }},
40
+ dagLevelDistance: {},
41
+ dagNodeFilter: { default: node => true },
42
+ onDagError: { triggerUpdate: false },
43
+ nodeRelSize: { default: 4, triggerUpdate: false, onChange: notifyRedraw }, // area per val unit
44
+ nodeId: { default: 'id' },
45
+ nodeVal: { default: 'val', triggerUpdate: false, onChange: notifyRedraw },
46
+ nodeColor: { default: 'color', triggerUpdate: false, onChange: notifyRedraw },
47
+ nodeAutoColorBy: {},
48
+ nodeCanvasObject: { triggerUpdate: false, onChange: notifyRedraw },
49
+ nodeCanvasObjectMode: { default: () => 'replace', triggerUpdate: false, onChange: notifyRedraw },
50
+ nodeVisibility: { default: true, triggerUpdate: false, onChange: notifyRedraw },
51
+ linkSource: { default: 'source' },
52
+ linkTarget: { default: 'target' },
53
+ linkVisibility: { default: true, triggerUpdate: false, onChange: notifyRedraw },
54
+ linkColor: { default: 'color', triggerUpdate: false, onChange: notifyRedraw },
55
+ linkAutoColorBy: {},
56
+ linkLineDash: { triggerUpdate: false, onChange: notifyRedraw },
57
+ linkWidth: { default: 1, triggerUpdate: false, onChange: notifyRedraw },
58
+ linkCurvature: { default: 0, triggerUpdate: false, onChange: notifyRedraw },
59
+ linkCanvasObject: { triggerUpdate: false, onChange: notifyRedraw },
60
+ linkCanvasObjectMode: { default: () => 'replace', triggerUpdate: false, onChange: notifyRedraw },
61
+ linkDirectionalArrowLength: { default: 0, triggerUpdate: false, onChange: notifyRedraw },
62
+ linkDirectionalArrowColor: { triggerUpdate: false, onChange: notifyRedraw },
63
+ linkDirectionalArrowRelPos: { default: 0.5, triggerUpdate: false, onChange: notifyRedraw }, // value between 0<>1 indicating the relative pos along the (exposed) line
64
+ linkDirectionalParticles: { default: 0 }, // animate photons travelling in the link direction
65
+ linkDirectionalParticleSpeed: { default: 0.01, triggerUpdate: false }, // in link length ratio per frame
66
+ linkDirectionalParticleWidth: { default: 4, triggerUpdate: false },
67
+ linkDirectionalParticleColor: { triggerUpdate: false },
68
+ globalScale: { default: 1, triggerUpdate: false },
69
+ d3AlphaMin: { default: 0, triggerUpdate: false},
70
+ d3AlphaDecay: { default: 0.0228, triggerUpdate: false, onChange(alphaDecay, state) { state.forceLayout.alphaDecay(alphaDecay) }},
71
+ d3AlphaTarget: { default: 0, triggerUpdate: false, onChange(alphaTarget, state) { state.forceLayout.alphaTarget(alphaTarget) }},
72
+ d3VelocityDecay: { default: 0.4, triggerUpdate: false, onChange(velocityDecay, state) { state.forceLayout.velocityDecay(velocityDecay) } },
73
+ warmupTicks: { default: 0, triggerUpdate: false }, // how many times to tick the force engine at init before starting to render
74
+ cooldownTicks: { default: Infinity, triggerUpdate: false },
75
+ cooldownTime: { default: 15000, triggerUpdate: false }, // ms
76
+ onUpdate: { default: () => {}, triggerUpdate: false },
77
+ onFinishUpdate: { default: () => {}, triggerUpdate: false },
78
+ onEngineTick: { default: () => {}, triggerUpdate: false },
79
+ onEngineStop: { default: () => {}, triggerUpdate: false },
80
+ onNeedsRedraw: { triggerUpdate: false },
81
+ isShadow: { default: false, triggerUpdate: false }
82
+ },
83
+
84
+ methods: {
85
+ // Expose d3 forces for external manipulation
86
+ d3Force: function(state, forceName, forceFn) {
87
+ if (forceFn === undefined) {
88
+ return state.forceLayout.force(forceName); // Force getter
89
+ }
90
+ state.forceLayout.force(forceName, forceFn); // Force setter
91
+ return this;
92
+ },
93
+ d3ReheatSimulation: function(state) {
94
+ state.forceLayout.alpha(1);
95
+ this.resetCountdown();
96
+ return this;
97
+ },
98
+ // reset cooldown state
99
+ resetCountdown: function(state) {
100
+ state.cntTicks = 0;
101
+ state.startTickTime = new Date();
102
+ state.engineRunning = true;
103
+ return this;
104
+ },
105
+ isEngineRunning: state => !!state.engineRunning,
106
+ tickFrame: function(state) {
107
+ !state.isShadow && layoutTick();
108
+ paintLinks();
109
+ !state.isShadow && paintArrows();
110
+ !state.isShadow && paintPhotons();
111
+ paintNodes();
112
+
113
+ return this;
114
+
115
+ //
116
+
117
+ function layoutTick() {
118
+ if (state.engineRunning) {
119
+ if (
120
+ ++state.cntTicks > state.cooldownTicks ||
121
+ (new Date()) - state.startTickTime > state.cooldownTime ||
122
+ (state.d3AlphaMin > 0 && state.forceLayout.alpha() < state.d3AlphaMin)
123
+ ) {
124
+ state.engineRunning = false; // Stop ticking graph
125
+ state.onEngineStop();
126
+ } else {
127
+ state.forceLayout.tick(); // Tick it
128
+ state.onEngineTick();
129
+ }
130
+ }
131
+ }
132
+
133
+ function paintNodes() {
134
+ const getVisibility = accessorFn(state.nodeVisibility);
135
+ const getVal = accessorFn(state.nodeVal);
136
+ const getColor = accessorFn(state.nodeColor);
137
+ const getNodeCanvasObjectMode = accessorFn(state.nodeCanvasObjectMode);
138
+
139
+ const ctx = state.ctx;
140
+
141
+ // Draw wider nodes by 1px on shadow canvas for more precise hovering (due to boundary anti-aliasing)
142
+ const padAmount = state.isShadow / state.globalScale;
143
+
144
+ const visibleNodes = state.graphData.nodes.filter(getVisibility);
145
+
146
+ ctx.save();
147
+ visibleNodes.forEach(node => {
148
+ const nodeCanvasObjectMode = getNodeCanvasObjectMode(node);
149
+
150
+ if (state.nodeCanvasObject && (nodeCanvasObjectMode === 'before' || nodeCanvasObjectMode === 'replace')) {
151
+ // Custom node before/replace paint
152
+ state.nodeCanvasObject(node, ctx, state.globalScale);
153
+
154
+ if (nodeCanvasObjectMode === 'replace') {
155
+ ctx.restore();
156
+ return;
157
+ }
158
+ }
159
+
160
+ // Draw wider nodes by 1px on shadow canvas for more precise hovering (due to boundary anti-aliasing)
161
+ const r = Math.sqrt(Math.max(0, getVal(node) || 1)) * state.nodeRelSize + padAmount;
162
+
163
+ ctx.beginPath();
164
+ ctx.arc(node.x, node.y, r, 0, 2 * Math.PI, false);
165
+ ctx.fillStyle = getColor(node) || 'rgba(31, 120, 180, 0.92)';
166
+ ctx.fill();
167
+
168
+ if (state.nodeCanvasObject && nodeCanvasObjectMode === 'after') {
169
+ // Custom node after paint
170
+ state.nodeCanvasObject(node, state.ctx, state.globalScale);
171
+ }
172
+ });
173
+ ctx.restore();
174
+ }
175
+
176
+ function paintLinks() {
177
+ const getVisibility = accessorFn(state.linkVisibility);
178
+ const getColor = accessorFn(state.linkColor);
179
+ const getWidth = accessorFn(state.linkWidth);
180
+ const getLineDash = accessorFn(state.linkLineDash);
181
+ const getCurvature = accessorFn(state.linkCurvature);
182
+ const getLinkCanvasObjectMode = accessorFn(state.linkCanvasObjectMode);
183
+
184
+ const ctx = state.ctx;
185
+
186
+ // Draw wider lines by 2px on shadow canvas for more precise hovering (due to boundary anti-aliasing)
187
+ const padAmount = state.isShadow * 2;
188
+
189
+ const visibleLinks = state.graphData.links.filter(getVisibility);
190
+
191
+ visibleLinks.forEach(calcLinkControlPoints); // calculate curvature control points for all visible links
192
+
193
+ let beforeCustomLinks = [], afterCustomLinks = [], defaultPaintLinks = visibleLinks;
194
+ if (state.linkCanvasObject) {
195
+ const replaceCustomLinks = [], otherCustomLinks = [];
196
+
197
+ visibleLinks.forEach(d =>
198
+ ({
199
+ before: beforeCustomLinks,
200
+ after: afterCustomLinks,
201
+ replace: replaceCustomLinks
202
+ }[getLinkCanvasObjectMode(d)] || otherCustomLinks).push(d)
203
+ );
204
+ defaultPaintLinks = [...beforeCustomLinks, ...afterCustomLinks, ...otherCustomLinks];
205
+ beforeCustomLinks = beforeCustomLinks.concat(replaceCustomLinks);
206
+ }
207
+
208
+ // Custom link before paints
209
+ ctx.save();
210
+ beforeCustomLinks.forEach(link => state.linkCanvasObject(link, ctx, state.globalScale));
211
+ ctx.restore();
212
+
213
+ // Bundle strokes per unique color/width/dash for performance optimization
214
+ const linksPerColor = indexBy(defaultPaintLinks, [getColor, getWidth, getLineDash]);
215
+
216
+ ctx.save();
217
+ Object.entries(linksPerColor).forEach(([color, linksPerWidth]) => {
218
+ const lineColor = !color || color === 'undefined' ? 'rgba(0,0,0,0.15)' : color;
219
+ Object.entries(linksPerWidth).forEach(([width, linesPerLineDash]) => {
220
+ const lineWidth = (width || 1) / state.globalScale + padAmount;
221
+ Object.entries(linesPerLineDash).forEach(([dashSegments, links]) => {
222
+ const lineDashSegments = getLineDash(links[0]);
223
+ ctx.beginPath();
224
+ links.forEach(link => {
225
+ const start = link.source;
226
+ const end = link.target;
227
+ if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
228
+
229
+ ctx.moveTo(start.x, start.y);
230
+
231
+ const controlPoints = link.__controlPoints;
232
+
233
+ if (!controlPoints) { // Straight line
234
+ ctx.lineTo(end.x, end.y);
235
+ } else {
236
+ // Use quadratic curves for regular lines and bezier for loops
237
+ ctx[controlPoints.length === 2 ? 'quadraticCurveTo' : 'bezierCurveTo'](...controlPoints, end.x, end.y);
238
+ }
239
+ });
240
+ ctx.strokeStyle = lineColor;
241
+ ctx.lineWidth = lineWidth;
242
+ ctx.setLineDash(lineDashSegments || []);
243
+ ctx.stroke();
244
+ });
245
+ });
246
+ });
247
+ ctx.restore();
248
+
249
+ // Custom link after paints
250
+ ctx.save();
251
+ afterCustomLinks.forEach(link => state.linkCanvasObject(link, ctx, state.globalScale));
252
+ ctx.restore();
253
+
254
+ //
255
+
256
+ function calcLinkControlPoints(link) {
257
+ const curvature = getCurvature(link);
258
+
259
+ if (!curvature) { // straight line
260
+ link.__controlPoints = null;
261
+ return;
262
+ }
263
+
264
+ const start = link.source;
265
+ const end = link.target;
266
+ if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
267
+
268
+ const l = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); // line length
269
+
270
+ if (l > 0) {
271
+ const a = Math.atan2(end.y - start.y, end.x - start.x); // line angle
272
+ const d = l * curvature; // control point distance
273
+
274
+ const cp = { // control point
275
+ x: (start.x + end.x) / 2 + d * Math.cos(a - Math.PI / 2),
276
+ y: (start.y + end.y) / 2 + d * Math.sin(a - Math.PI / 2)
277
+ };
278
+
279
+ link.__controlPoints = [cp.x, cp.y];
280
+ } else { // Same point, draw a loop
281
+ const d = curvature * 70;
282
+ link.__controlPoints = [end.x, end.y - d, end.x + d, end.y];
283
+ }
284
+ }
285
+ }
286
+
287
+ function paintArrows() {
288
+ const ARROW_WH_RATIO = 1.6;
289
+ const ARROW_VLEN_RATIO = 0.2;
290
+
291
+ const getLength = accessorFn(state.linkDirectionalArrowLength);
292
+ const getRelPos = accessorFn(state.linkDirectionalArrowRelPos);
293
+ const getVisibility = accessorFn(state.linkVisibility);
294
+ const getColor = accessorFn(state.linkDirectionalArrowColor || state.linkColor);
295
+ const getNodeVal = accessorFn(state.nodeVal);
296
+ const ctx = state.ctx;
297
+
298
+ ctx.save();
299
+ state.graphData.links.filter(getVisibility).forEach(link => {
300
+ const arrowLength = getLength(link);
301
+ if (!arrowLength || arrowLength < 0) return;
302
+
303
+ const start = link.source;
304
+ const end = link.target;
305
+
306
+ if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
307
+
308
+ const startR = Math.sqrt(Math.max(0, getNodeVal(start) || 1)) * state.nodeRelSize;
309
+ const endR = Math.sqrt(Math.max(0, getNodeVal(end) || 1)) * state.nodeRelSize;
310
+
311
+ const arrowRelPos = Math.min(1, Math.max(0, getRelPos(link)));
312
+ const arrowColor = getColor(link) || 'rgba(0,0,0,0.28)';
313
+ const arrowHalfWidth = arrowLength / ARROW_WH_RATIO / 2;
314
+
315
+ // Construct bezier for curved lines
316
+ const bzLine = link.__controlPoints && new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y);
317
+
318
+ const getCoordsAlongLine = bzLine
319
+ ? t => bzLine.get(t) // get position along bezier line
320
+ : t => ({ // straight line: interpolate linearly
321
+ x: start.x + (end.x - start.x) * t || 0,
322
+ y: start.y + (end.y - start.y) * t || 0
323
+ });
324
+
325
+ const lineLen = bzLine
326
+ ? bzLine.length()
327
+ : Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
328
+
329
+ const posAlongLine = startR + arrowLength + (lineLen - startR - endR - arrowLength) * arrowRelPos;
330
+
331
+ const arrowHead = getCoordsAlongLine(posAlongLine / lineLen);
332
+ const arrowTail = getCoordsAlongLine((posAlongLine - arrowLength) / lineLen);
333
+ const arrowTailVertex = getCoordsAlongLine((posAlongLine - arrowLength * (1 - ARROW_VLEN_RATIO)) / lineLen);
334
+
335
+ const arrowTailAngle = Math.atan2(arrowHead.y - arrowTail.y, arrowHead.x - arrowTail.x) - Math.PI / 2;
336
+
337
+ ctx.beginPath();
338
+
339
+ ctx.moveTo(arrowHead.x, arrowHead.y);
340
+ ctx.lineTo(arrowTail.x + arrowHalfWidth * Math.cos(arrowTailAngle), arrowTail.y + arrowHalfWidth * Math.sin(arrowTailAngle));
341
+ ctx.lineTo(arrowTailVertex.x, arrowTailVertex.y);
342
+ ctx.lineTo(arrowTail.x - arrowHalfWidth * Math.cos(arrowTailAngle), arrowTail.y - arrowHalfWidth * Math.sin(arrowTailAngle));
343
+
344
+ ctx.fillStyle = arrowColor;
345
+ ctx.fill();
346
+ });
347
+ ctx.restore();
348
+ }
349
+
350
+ function paintPhotons() {
351
+ const getNumPhotons = accessorFn(state.linkDirectionalParticles);
352
+ const getSpeed = accessorFn(state.linkDirectionalParticleSpeed);
353
+ const getDiameter = accessorFn(state.linkDirectionalParticleWidth);
354
+ const getVisibility = accessorFn(state.linkVisibility);
355
+ const getColor = accessorFn(state.linkDirectionalParticleColor || state.linkColor);
356
+ const ctx = state.ctx;
357
+
358
+ ctx.save();
359
+ state.graphData.links.filter(getVisibility).forEach(link => {
360
+ const numCyclePhotons = getNumPhotons(link);
361
+
362
+ if (!link.hasOwnProperty('__photons') || !link.__photons.length) return;
363
+
364
+ const start = link.source;
365
+ const end = link.target;
366
+
367
+ if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
368
+
369
+ const particleSpeed = getSpeed(link);
370
+ const photons = link.__photons || [];
371
+ const photonR = Math.max(0, getDiameter(link) / 2) / Math.sqrt(state.globalScale);
372
+ const photonColor = getColor(link) || 'rgba(0,0,0,0.28)';
373
+
374
+ ctx.fillStyle = photonColor;
375
+
376
+ // Construct bezier for curved lines
377
+ const bzLine = link.__controlPoints
378
+ ? new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y)
379
+ : null;
380
+
381
+ let cyclePhotonIdx = 0;
382
+ let needsCleanup = false; // whether some photons need to be removed from list
383
+ photons.forEach(photon => {
384
+ const singleHop = !!photon.__singleHop;
385
+
386
+ if (!photon.hasOwnProperty('__progressRatio')) {
387
+ photon.__progressRatio = singleHop ? 0 : cyclePhotonIdx / numCyclePhotons;
388
+ }
389
+
390
+ !singleHop && cyclePhotonIdx++; // increase regular photon index
391
+
392
+ photon.__progressRatio += particleSpeed;
393
+
394
+ if (photon.__progressRatio >=1) {
395
+ if (!singleHop) {
396
+ photon.__progressRatio = photon.__progressRatio % 1;
397
+ } else {
398
+ needsCleanup = true;
399
+ return;
400
+ }
401
+ }
402
+
403
+ const photonPosRatio = photon.__progressRatio;
404
+
405
+ const coords = bzLine
406
+ ? bzLine.get(photonPosRatio) // get position along bezier line
407
+ : { // straight line: interpolate linearly
408
+ x: start.x + (end.x - start.x) * photonPosRatio || 0,
409
+ y: start.y + (end.y - start.y) * photonPosRatio || 0
410
+ };
411
+
412
+ ctx.beginPath();
413
+ ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false);
414
+ ctx.fill();
415
+ });
416
+
417
+ if (needsCleanup) {
418
+ // remove expired single hop photons
419
+ link.__photons = link.__photons.filter(photon => !photon.__singleHop || photon.__progressRatio <= 1);
420
+ }
421
+ });
422
+ ctx.restore();
423
+ }
424
+ },
425
+ emitParticle: function(state, link) {
426
+ if (link) {
427
+ !link.__photons && (link.__photons = []);
428
+ link.__photons.push({__singleHop: true}); // add a single hop particle
429
+ }
430
+
431
+ return this;
432
+ }
433
+ },
434
+
435
+ stateInit: () => ({
436
+ forceLayout: d3ForceSimulation()
437
+ .force('link', d3ForceLink())
438
+ .force('charge', d3ForceManyBody())
439
+ .force('center', d3ForceCenter())
440
+ .force('dagRadial', null)
441
+ .stop(),
442
+ engineRunning: false
443
+ }),
444
+
445
+ init(canvasCtx, state) {
446
+ // Main canvas object to manipulate
447
+ state.ctx = canvasCtx;
448
+ },
449
+
450
+ update(state) {
451
+ state.engineRunning = false; // Pause simulation
452
+ state.onUpdate();
453
+
454
+ if (state.nodeAutoColorBy !== null) {
455
+ // Auto add color to uncolored nodes
456
+ autoColorObjects(state.graphData.nodes, accessorFn(state.nodeAutoColorBy), state.nodeColor);
457
+ }
458
+ if (state.linkAutoColorBy !== null) {
459
+ // Auto add color to uncolored links
460
+ autoColorObjects(state.graphData.links, accessorFn(state.linkAutoColorBy), state.linkColor);
461
+ }
462
+
463
+ // parse links
464
+ state.graphData.links.forEach(link => {
465
+ link.source = link[state.linkSource];
466
+ link.target = link[state.linkTarget];
467
+ });
468
+
469
+ if (!state.isShadow) {
470
+ // Add photon particles
471
+ const linkParticlesAccessor = accessorFn(state.linkDirectionalParticles);
472
+ state.graphData.links.forEach(link => {
473
+ const numPhotons = Math.round(Math.abs(linkParticlesAccessor(link)));
474
+ if (numPhotons) {
475
+ link.__photons = [...Array(numPhotons)].map(() => ({}));
476
+ } else {
477
+ delete link.__photons;
478
+ }
479
+ });
480
+ }
481
+
482
+ // Feed data to force-directed layout
483
+ state.forceLayout
484
+ .stop()
485
+ .alpha(1) // re-heat the simulation
486
+ .nodes(state.graphData.nodes);
487
+
488
+ // add links (if link force is still active)
489
+ const linkForce = state.forceLayout.force('link');
490
+ if (linkForce) {
491
+ linkForce
492
+ .id(d => d[state.nodeId])
493
+ .links(state.graphData.links);
494
+ }
495
+
496
+ // setup dag force constraints
497
+ const nodeDepths = state.dagMode && getDagDepths(
498
+ state.graphData,
499
+ node => node[state.nodeId],
500
+ {
501
+ nodeFilter: state.dagNodeFilter,
502
+ onLoopError: state.onDagError || undefined
503
+ }
504
+ );
505
+ const maxDepth = Math.max(...Object.values(nodeDepths || []));
506
+ const dagLevelDistance = state.dagLevelDistance || (
507
+ state.graphData.nodes.length / (maxDepth || 1) * DAG_LEVEL_NODE_RATIO
508
+ * (['radialin', 'radialout'].indexOf(state.dagMode) !== -1 ? 0.7 : 1)
509
+ );
510
+
511
+ // Fix nodes to x,y for dag mode
512
+ if (state.dagMode) {
513
+ const getFFn = (fix, invert) => node => !fix
514
+ ? undefined
515
+ : (nodeDepths[node[state.nodeId]] - maxDepth / 2) * dagLevelDistance * (invert ? -1 : 1);
516
+
517
+ const fxFn = getFFn(['lr', 'rl'].indexOf(state.dagMode) !== -1, state.dagMode === 'rl');
518
+ const fyFn = getFFn(['td', 'bu'].indexOf(state.dagMode) !== -1, state.dagMode === 'bu');
519
+
520
+ state.graphData.nodes.filter(state.dagNodeFilter).forEach(node => {
521
+ node.fx = fxFn(node);
522
+ node.fy = fyFn(node);
523
+ });
524
+ }
525
+
526
+ // Use radial force for radial dags
527
+ state.forceLayout.force('dagRadial',
528
+ ['radialin', 'radialout'].indexOf(state.dagMode) !== -1
529
+ ? d3ForceRadial(node => {
530
+ const nodeDepth = nodeDepths[node[state.nodeId]] || -1;
531
+ return (state.dagMode === 'radialin' ? maxDepth - nodeDepth : nodeDepth) * dagLevelDistance;
532
+ })
533
+ .strength(node => state.dagNodeFilter(node) ? 1 : 0)
534
+ : null
535
+ );
536
+
537
+ for (let i=0; (i<state.warmupTicks) && !(state.d3AlphaMin > 0 && state.forceLayout.alpha() < state.d3AlphaMin); i++) {
538
+ state.forceLayout.tick();
539
+ } // Initial ticks before starting to render
540
+
541
+ this.resetCountdown();
542
+ state.onFinishUpdate();
543
+ }
544
+ });
@@ -0,0 +1,17 @@
1
+ import { scaleOrdinal } from 'd3-scale';
2
+ import { schemePaired } from 'd3-scale-chromatic';
3
+
4
+ const autoColorScale = scaleOrdinal(schemePaired);
5
+
6
+ // Autoset attribute colorField by colorByAccessor property
7
+ // If an object has already a color, don't set it
8
+ // Objects can be nodes or links
9
+ function autoColorObjects(objects, colorByAccessor, colorField) {
10
+ if (!colorByAccessor || typeof colorField !== 'string') return;
11
+
12
+ objects.filter(obj => !obj[colorField]).forEach(obj => {
13
+ obj[colorField] = autoColorScale(colorByAccessor(obj));
14
+ });
15
+ }
16
+
17
+ export { autoColorObjects };
@@ -0,0 +1,51 @@
1
+ export default function({ nodes, links }, idAccessor, {
2
+ nodeFilter = () => true,
3
+ onLoopError = loopIds => { throw `Invalid DAG structure! Found cycle in node path: ${loopIds.join(' -> ')}.` }
4
+ } = {}) {
5
+ // linked graph
6
+ const graph = {};
7
+
8
+ nodes.forEach(node => graph[idAccessor(node)] = { data: node, out : [], depth: -1, skip: !nodeFilter(node) });
9
+ links.forEach(({ source, target }) => {
10
+ const sourceId = getNodeId(source);
11
+ const targetId = getNodeId(target);
12
+ if (!graph.hasOwnProperty(sourceId)) throw `Missing source node with id: ${sourceId}`;
13
+ if (!graph.hasOwnProperty(targetId)) throw `Missing target node with id: ${targetId}`;
14
+ const sourceNode = graph[sourceId];
15
+ const targetNode = graph[targetId];
16
+
17
+ sourceNode.out.push(targetNode);
18
+
19
+ function getNodeId(node) {
20
+ return typeof node === 'object' ? idAccessor(node) : node;
21
+ }
22
+ });
23
+
24
+ const foundLoops = [];
25
+ traverse(Object.values(graph));
26
+
27
+ const nodeDepths = Object.assign({}, ...Object.entries(graph)
28
+ .filter(([, node]) => !node.skip)
29
+ .map(([id, node]) => ({ [id]: node.depth }))
30
+ );
31
+
32
+ return nodeDepths;
33
+
34
+ function traverse(nodes, nodeStack = [], currentDepth = 0) {
35
+ for (let i=0, l=nodes.length; i<l; i++) {
36
+ const node = nodes[i];
37
+ if (nodeStack.indexOf(node) !== -1) {
38
+ const loop = [...nodeStack.slice(nodeStack.indexOf(node)), node].map(d => idAccessor(d.data));
39
+ if (!foundLoops.some(foundLoop => foundLoop.length === loop.length && foundLoop.every((id, idx) => id === loop[idx]))) {
40
+ foundLoops.push(loop);
41
+ onLoopError(loop);
42
+ }
43
+ continue;
44
+ }
45
+ if (currentDepth > node.depth) { // Don't unnecessarily revisit chunks of the graph
46
+ node.depth = currentDepth;
47
+ traverse(node.out, [...nodeStack, node], currentDepth + (node.skip ? 0 : 1));
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,35 @@
1
+ .force-graph-container canvas {
2
+ display: block;
3
+ user-select: none;
4
+ outline: none;
5
+ -webkit-tap-highlight-color: transparent;
6
+ }
7
+
8
+ .force-graph-container .graph-tooltip {
9
+ position: absolute;
10
+ transform: translate(-50%, 25px);
11
+ font-family: sans-serif;
12
+ font-size: 16px;
13
+ padding: 4px;
14
+ border-radius: 3px;
15
+ color: #eee;
16
+ background: rgba(0,0,0,0.65);
17
+ visibility: hidden; /* by default */
18
+ }
19
+
20
+ .force-graph-container .clickable {
21
+ cursor: pointer;
22
+ }
23
+
24
+ .force-graph-container .grabbable {
25
+ cursor: move;
26
+ cursor: grab;
27
+ cursor: -moz-grab;
28
+ cursor: -webkit-grab;
29
+ }
30
+
31
+ .force-graph-container .grabbable:active {
32
+ cursor: grabbing;
33
+ cursor: -moz-grabbing;
34
+ cursor: -webkit-grabbing;
35
+ }