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,34 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ // Random tree
13
+ const NODES = 300;
14
+ const GROUPS = 12;
15
+ const gData = {
16
+ nodes: [...Array(NODES).keys()].map(i => ({
17
+ id: i,
18
+ group: Math.ceil(Math.random() * GROUPS)
19
+ })),
20
+ links: [...Array(NODES).keys()]
21
+ .filter(id => id)
22
+ .map(id => ({
23
+ source: id,
24
+ target: Math.round(Math.random() * (id-1))
25
+ }))
26
+ }
27
+
28
+ const Graph = ForceGraph()
29
+ (document.getElementById('graph'))
30
+ .nodeAutoColorBy('group')
31
+ .linkAutoColorBy(d => gData.nodes[d.source].group)
32
+ .graphData(gData);
33
+ </script>
34
+ </body>
@@ -0,0 +1,29 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ // Random tree
13
+ const N = 300;
14
+ const gData = {
15
+ nodes: [...Array(N).keys()].map(i => ({ id: i })),
16
+ links: [...Array(N).keys()]
17
+ .filter(id => id)
18
+ .map(id => ({
19
+ source: id,
20
+ target: Math.round(Math.random() * (id-1))
21
+ }))
22
+ };
23
+
24
+ const Graph = ForceGraph()
25
+ (document.getElementById('graph'))
26
+ .linkDirectionalParticles(2)
27
+ .graphData(gData);
28
+ </script>
29
+ </body>
@@ -0,0 +1,108 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <br/>
10
+ <div style="text-align: center; color: silver">
11
+ <b>New node:</b> click on the canvas, <b>New link:</b> drag one node close enough to another one,
12
+ <b>Rename</b> node or link by clicking on it, <b>Remove</b> node or link by right-clicking on it
13
+ </div>
14
+ <div id="graph"></div>
15
+
16
+ <script>
17
+ let nodeIdCounter = 0, linkIdCounter = 0;
18
+ let nodes = [], links = [];
19
+ let dragSourceNode = null, interimLink = null;
20
+ const snapInDistance = 15;
21
+ const snapOutDistance = 40;
22
+
23
+ const updateGraphData = () => {
24
+ Graph.graphData({ nodes: nodes, links: links });
25
+ };
26
+
27
+ const distance = (node1, node2) => {
28
+ return Math.sqrt(Math.pow(node1.x - node2.x, 2) + Math.pow(node1.y - node2.y, 2));
29
+ };
30
+
31
+ const rename = (nodeOrLink, type) => {
32
+ let value = prompt('Name this ' + type + ':', nodeOrLink.name);
33
+ if (!value) {
34
+ return;
35
+ }
36
+ nodeOrLink.name = value;
37
+ updateGraphData();
38
+ };
39
+
40
+ const setInterimLink = (source, target) => {
41
+ let linkId = linkIdCounter ++;
42
+ interimLink = { id: linkId, source: source, target: target, name: 'link_' + linkId };
43
+ links.push(interimLink);
44
+ updateGraphData();
45
+ };
46
+
47
+ const removeLink = link => {
48
+ links.splice(links.indexOf(link), 1);
49
+ };
50
+
51
+ const removeInterimLinkWithoutAddingIt = () => {
52
+ removeLink(interimLink);
53
+ interimLink = null;
54
+ updateGraphData();
55
+ };
56
+
57
+ const removeNode = node => {
58
+ links.filter(link => link.source === node || link.target === node).forEach(link => removeLink(link));
59
+ nodes.splice(nodes.indexOf(node), 1);
60
+ };
61
+
62
+ const Graph = ForceGraph()
63
+ (document.getElementById('graph'))
64
+ .linkDirectionalArrowLength(6)
65
+ .linkDirectionalArrowRelPos(1)
66
+ .onNodeDrag(dragNode => {
67
+ dragSourceNode = dragNode;
68
+ for (let node of nodes) {
69
+ if (dragNode === node) {
70
+ continue;
71
+ }
72
+ // close enough: snap onto node as target for suggested link
73
+ if (!interimLink && distance(dragNode, node) < snapInDistance) {
74
+ setInterimLink(dragSourceNode, node);
75
+ }
76
+ // close enough to other node: snap over to other node as target for suggested link
77
+ if (interimLink && node !== interimLink.target && distance(dragNode, node) < snapInDistance) {
78
+ removeLink(interimLink);
79
+ setInterimLink(dragSourceNode, node);
80
+ }
81
+ }
82
+ // far away enough: snap out of the current target node
83
+ if (interimLink && distance(dragNode, interimLink.target) > snapOutDistance) {
84
+ removeInterimLinkWithoutAddingIt();
85
+ }
86
+ })
87
+ .onNodeDragEnd(() => {
88
+ dragSourceNode = null;
89
+ interimLink = null;
90
+ updateGraphData();
91
+ })
92
+ .nodeColor(node => node === dragSourceNode || (interimLink &&
93
+ (node === interimLink.source || node === interimLink.target)) ? 'orange' : null)
94
+ .linkColor(link => link === interimLink ? 'orange' : '#bbbbbb')
95
+ .linkLineDash(link => link === interimLink ? [2, 2] : [])
96
+ .onNodeClick((node, event) => rename(node, 'node'))
97
+ .onNodeRightClick((node, event) => removeNode(node))
98
+ .onLinkClick((link, event) => rename(link, 'link'))
99
+ .onLinkRightClick((link, event) => removeLink(link))
100
+ .onBackgroundClick(event => {
101
+ let coords = Graph.screen2GraphCoords(event.layerX, event.layerY);
102
+ let nodeId = nodeIdCounter ++;
103
+ nodes.push({ id: nodeId, x: coords.x, y: coords.y, name: 'node_' + nodeId });
104
+ updateGraphData();
105
+ });
106
+ updateGraphData();
107
+ </script>
108
+ </body>
@@ -0,0 +1,28 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ fetch('../datasets/miserables.json').then(res => res.json()).then(data => {
13
+ const elem = document.getElementById('graph');
14
+
15
+ const Graph = ForceGraph()(elem)
16
+ .graphData(data)
17
+ .nodeLabel('id')
18
+ .nodeAutoColorBy('group')
19
+ .linkDirectionalParticles(2)
20
+ .linkDirectionalParticleWidth(1.4)
21
+ .onNodeClick(node => {
22
+ // Center/zoom on node
23
+ Graph.centerAt(node.x, node.y, 1000);
24
+ Graph.zoom(8, 2000);
25
+ });
26
+ });
27
+ </script>
28
+ </body>
@@ -0,0 +1,50 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+
7
+ <script src="//unpkg.com/d3-quadtree"></script>
8
+ <script src="//unpkg.com/d3-force"></script>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="graph"></div>
13
+
14
+ <script>
15
+ const N = 80;
16
+ const nodes = [...Array(N).keys()].map(() => ({
17
+ // Initial velocity in random direction
18
+ vx: (Math.random() * 2) - 1,
19
+ vy: (Math.random() * 2) - 1
20
+ }));
21
+
22
+ const Graph = ForceGraph()
23
+ (document.getElementById('graph'));
24
+
25
+ Graph.cooldownTime(Infinity)
26
+ .d3AlphaDecay(0)
27
+ .d3VelocityDecay(0)
28
+
29
+ // Deactivate existing forces
30
+ .d3Force('center', null)
31
+ .d3Force('charge', null)
32
+
33
+ // Add collision and bounding box forces
34
+ .d3Force('collide', d3.forceCollide(Graph.nodeRelSize()))
35
+ .d3Force('box', () => {
36
+ const SQUARE_HALF_SIDE = Graph.nodeRelSize() * N * 0.5;
37
+
38
+ nodes.forEach(node => {
39
+ const x = node.x || 0, y = node.y || 0;
40
+
41
+ // bounce on box walls
42
+ if (Math.abs(x) > SQUARE_HALF_SIDE) { node.vx *= -1; }
43
+ if (Math.abs(y) > SQUARE_HALF_SIDE) { node.vy *= -1; }
44
+ });
45
+ })
46
+
47
+ // Add nodes
48
+ .graphData({ nodes, links: [] });
49
+ </script>
50
+ </body>
@@ -0,0 +1,37 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ const gData = {
13
+ nodes: [...Array(9).keys()].map(i => ({ id: i })),
14
+ links: [
15
+ { source: 1, target: 4, curvature: 0 },
16
+ { source: 1, target: 4, curvature: 0.5 },
17
+ { source: 1, target: 4, curvature: -0.5 },
18
+ { source: 5, target: 2, curvature: 0.3 },
19
+ { source: 2, target: 5, curvature: 0.3 },
20
+ { source: 0, target: 3, curvature: 0 },
21
+ { source: 3, target: 3, curvature: 0.5 },
22
+ { source: 0, target: 4, curvature: 0.2 },
23
+ { source: 4, target: 5, curvature: 0.5 },
24
+ { source: 5, target: 6, curvature: 0.7 },
25
+ { source: 6, target: 7, curvature: 1 },
26
+ { source: 7, target: 8, curvature: 2 },
27
+ { source: 8, target: 0, curvature: 0.5 }
28
+ ]
29
+ };
30
+
31
+ const Graph = ForceGraph()
32
+ (document.getElementById('graph'))
33
+ .linkDirectionalParticles(2)
34
+ .linkCurvature('curvature')
35
+ .graphData(gData);
36
+ </script>
37
+ </body>
@@ -0,0 +1,76 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ const gData = {
13
+ nodes: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }],
14
+ links: [
15
+ { source: 0, target: 1 },
16
+ { source: 0, target: 1 },
17
+ { source: 1, target: 0 },
18
+ { source: 1, target: 2 },
19
+ { source: 2, target: 2 },
20
+ { source: 2, target: 2 },
21
+ { source: 2, target: 2 },
22
+ { source: 2, target: 3 },
23
+ { source: 3, target: 4 },
24
+ { source: 4, target: 3 }
25
+ ]
26
+ };
27
+
28
+ let selfLoopLinks = {};
29
+ let sameNodesLinks = {};
30
+ const curvatureMinMax = 0.5;
31
+
32
+ // 1. assign each link a nodePairId that combines their source and target independent of the links direction
33
+ // 2. group links together that share the same two nodes or are self-loops
34
+ gData.links.forEach(link => {
35
+ link.nodePairId = link.source <= link.target ? (link.source + "_" + link.target) : (link.target + "_" + link.source);
36
+ let map = link.source === link.target ? selfLoopLinks : sameNodesLinks;
37
+ if (!map[link.nodePairId]) {
38
+ map[link.nodePairId] = [];
39
+ }
40
+ map[link.nodePairId].push(link);
41
+ });
42
+
43
+ // Compute the curvature for self-loop links to avoid overlaps
44
+ Object.keys(selfLoopLinks).forEach(id => {
45
+ let links = selfLoopLinks[id];
46
+ let lastIndex = links.length - 1;
47
+ links[lastIndex].curvature = 1;
48
+ let delta = (1 - curvatureMinMax) / lastIndex;
49
+ for (let i = 0; i < lastIndex; i++) {
50
+ links[i].curvature = curvatureMinMax + i * delta;
51
+ }
52
+ });
53
+
54
+ // Compute the curvature for links sharing the same two nodes to avoid overlaps
55
+ Object.keys(sameNodesLinks).filter(nodePairId => sameNodesLinks[nodePairId].length > 1).forEach(nodePairId => {
56
+ let links = sameNodesLinks[nodePairId];
57
+ let lastIndex = links.length - 1;
58
+ let lastLink = links[lastIndex];
59
+ lastLink.curvature = curvatureMinMax;
60
+ let delta = 2 * curvatureMinMax / lastIndex;
61
+ for (let i = 0; i < lastIndex; i++) {
62
+ links[i].curvature = - curvatureMinMax + i * delta;
63
+ if (lastLink.source !== links[i].source) {
64
+ links[i].curvature *= -1; // flip it around, otherwise they overlap
65
+ }
66
+ }
67
+ });
68
+
69
+ const Graph = ForceGraph()
70
+ (document.getElementById('graph'))
71
+ .linkCurvature('curvature')
72
+ .linkDirectionalArrowLength(6)
73
+ .linkDirectionalArrowRelPos(1)
74
+ .graphData(gData);
75
+ </script>
76
+ </body>
@@ -0,0 +1,44 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!--<script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ // Random tree
13
+ const N = 20;
14
+ const gData = {
15
+ nodes: [...Array(N).keys()].map(i => ({ id: i })),
16
+ links: [...Array(N).keys()]
17
+ .filter(id => id)
18
+ .map(id => ({
19
+ source: id,
20
+ target: Math.round(Math.random() * (id-1))
21
+ }))
22
+ };
23
+
24
+ // gen a number persistent color from around the palette
25
+ const getColor = n => '#' + ((n * 1234567) % Math.pow(2, 24)).toString(16).padStart(6, '0');
26
+
27
+ const Graph = ForceGraph()
28
+ (document.getElementById('graph'))
29
+ .nodeCanvasObject((node, ctx) => nodePaint(node, getColor(node.id), ctx))
30
+ .nodePointerAreaPaint(nodePaint)
31
+ .nodeLabel('id')
32
+ .graphData(gData);
33
+
34
+ function nodePaint({ id, x, y }, color, ctx) {
35
+ ctx.fillStyle = color;
36
+ [
37
+ () => { ctx.fillRect(x - 6, y - 4, 12, 8); }, // rectangle
38
+ () => { ctx.beginPath(); ctx.moveTo(x, y - 5); ctx.lineTo(x - 5, y + 5); ctx.lineTo(x + 5, y + 5); ctx.fill(); }, // triangle
39
+ () => { ctx.beginPath(); ctx.arc(x, y, 5, 0, 2 * Math.PI, false); ctx.fill(); }, // circle
40
+ () => { ctx.font = '10px Sans-Serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('Text', x, y); } // text
41
+ ][id%4]();
42
+ }
43
+ </script>
44
+ </body>
@@ -0,0 +1,96 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//bundle.run/@yarnpkg/lockfile@1.1.0"></script>
5
+ <script src="//unpkg.com/dat.gui"></script>
6
+ <script src="//unpkg.com/d3-quadtree"></script>
7
+ <script src="//unpkg.com/d3-force"></script>
8
+
9
+ <script src="//unpkg.com/force-graph"></script>
10
+ <!--<script src="../../dist/force-graph.js"></script>-->
11
+ </head>
12
+
13
+ <body>
14
+ <div id="graph"></div>
15
+
16
+ <script>
17
+ // controls
18
+ const controls = { 'DAG Orientation': 'lr'};
19
+ const gui = new dat.GUI();
20
+ gui.add(controls, 'DAG Orientation', ['lr', 'td', 'radialout', null])
21
+ .onChange(orientation => graph && graph.dagMode(orientation));
22
+
23
+ // graph config
24
+ const graph = ForceGraph()
25
+ .backgroundColor('#101020')
26
+ .linkColor(() => 'rgba(255,255,255,0.2)')
27
+ .dagMode('lr')
28
+ .dagLevelDistance(300)
29
+ .nodeId('package')
30
+ .linkCurvature(d =>
31
+ 0.07 * // max curvature
32
+ // curve outwards from source, using gradual straightening within a margin of a few px
33
+ (['td', 'bu'].includes(graph.dagMode())
34
+ ? Math.max(-1, Math.min(1, (d.source.x - d.target.x) / 25)) :
35
+ ['lr', 'rl'].includes(graph.dagMode())
36
+ ? Math.max(-1, Math.min(1, (d.target.y - d.source.y) / 25))
37
+ : ['radialout', 'radialin'].includes(graph.dagMode()) ? 0 : 1
38
+ )
39
+ )
40
+ .linkDirectionalParticles(2)
41
+ .linkDirectionalParticleWidth(3)
42
+ .nodeCanvasObject((node, ctx) => {
43
+ const label = node.package;
44
+ const fontSize = 15;
45
+ ctx.font = `${fontSize}px Sans-Serif`;
46
+ const textWidth = ctx.measureText(label).width;
47
+ const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
48
+
49
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
50
+ ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions);
51
+
52
+ ctx.textAlign = 'center';
53
+ ctx.textBaseline = 'middle';
54
+ ctx.fillStyle = 'lightsteelblue';
55
+ ctx.fillText(label, node.x, node.y);
56
+
57
+ node.__bckgDimensions = bckgDimensions; // to re-use in nodePointerAreaPaint
58
+ })
59
+ .nodePointerAreaPaint((node, color, ctx) => {
60
+ ctx.fillStyle = color;
61
+ const bckgDimensions = node.__bckgDimensions;
62
+ bckgDimensions && ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions);
63
+ })
64
+ .d3Force('collide', d3.forceCollide(13))
65
+ .d3AlphaDecay(0.02)
66
+ .d3VelocityDecay(0.3);
67
+
68
+ fetch('//unpkg.com/d3@5.9.7/yarn.lock')
69
+ .then(r => r.text())
70
+ .then(text => {
71
+ const yarnlock = _yarnpkg_lockfile.parse(text);
72
+ if (yarnlock.type !== 'success') throw new Error('invalid yarn.lock');
73
+ return yarnlock.object;
74
+ })
75
+ .then(yarnlock => {
76
+ const nodes = [];
77
+ const links = [];
78
+
79
+ Object.entries(yarnlock).forEach(([package, details]) => {
80
+ nodes.push({
81
+ package,
82
+ version: details.version
83
+ });
84
+
85
+ if (details.dependencies) {
86
+ Object.entries(details.dependencies).forEach(([dep, version]) => {
87
+ links.push({source: package, target: `${dep}@${version}`});
88
+ });
89
+ }
90
+ });
91
+
92
+ graph(document.getElementById('graph'))
93
+ .graphData({ nodes, links });
94
+ });
95
+ </script>
96
+ </body>
@@ -0,0 +1,119 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//bundle.run/@yarnpkg/lockfile"></script>
5
+
6
+ <script src="//unpkg.com/dagre/dist/dagre.min.js"></script>
7
+ <script src="//unpkg.com/accessor-fn"></script>
8
+
9
+ <script src="//unpkg.com/force-graph"></script>
10
+ <!--<script src="../../dist/force-graph.js"></script>-->
11
+ </head>
12
+
13
+ <body>
14
+ <div id="graph"></div>
15
+
16
+ <script>
17
+ const Graph = ForceGraph()(document.getElementById('graph'))
18
+ .nodeId('id')
19
+ .nodeLabel('id')
20
+ .cooldownTicks(0) // pre-defined layout, cancel force engine iterations
21
+ .linkDirectionalArrowLength(3)
22
+ .linkDirectionalArrowRelPos(1)
23
+ .linkCurvature(d =>
24
+ 0.07 * // max curvature
25
+ // curve outwards from source, using gradual straightening within a margin of a few px
26
+ Math.max(-1, Math.min(1, (d.source.x - d.target.x) / 5)) *
27
+ Math.max(-1, Math.min(1, (d.target.y - d.source.y) / 5))
28
+ );
29
+
30
+ fetch('../../yarn.lock')
31
+ .then(r => r.text())
32
+ .then(text => {
33
+ const yarnlock = _yarnpkg_lockfile.parse(text);
34
+ if (yarnlock.type !== 'success') throw new Error('invalid yarn.lock');
35
+ return yarnlock.object;
36
+ })
37
+ .then(yarnlock => {
38
+ const nodes = [];
39
+ const links = [];
40
+ Object.entries(yarnlock).forEach(([package, details]) => {
41
+ nodes.push({ id: package });
42
+ if (details.dependencies) {
43
+ Object.entries(details.dependencies).forEach(([dep, version]) => {
44
+ links.push({source: package, target: `${dep}@${version}`});
45
+ });
46
+ }
47
+ });
48
+ return { nodes, links };
49
+ }).then(data => {
50
+ const nodeDiameter = Graph.nodeRelSize() * 2;
51
+ const layoutData = getLayout(data.nodes, data.links, {
52
+ nodeWidth: nodeDiameter,
53
+ nodeHeight: nodeDiameter,
54
+ nodesep: nodeDiameter * 0.5,
55
+ ranksep: nodeDiameter * Math.sqrt(data.nodes.length) * 0.6,
56
+
57
+ // root nodes aligned on top
58
+ rankDir: 'BT',
59
+ ranker: 'longest-path',
60
+ linkSource: 'target',
61
+ linkTarget: 'source'
62
+ });
63
+ layoutData.nodes.forEach(node => { node.fx = node.x; node.fy = node.y; }); // fix nodes
64
+
65
+ Graph.graphData(layoutData);
66
+ Graph.zoomToFit();
67
+ });
68
+
69
+ //
70
+
71
+ function getLayout(nodes, links, {
72
+ nodeId = 'id',
73
+ linkSource = 'source',
74
+ linkTarget = 'target',
75
+ nodeWidth = 0,
76
+ nodeHeight = 0,
77
+ ...graphCfg
78
+ } = {}) {
79
+ const getNodeWidth = accessorFn(nodeWidth);
80
+ const getNodeHeight = accessorFn(nodeHeight);
81
+
82
+ const g = new dagre.graphlib.Graph();
83
+ g.setGraph({
84
+ // rankDir: 'LR',
85
+ // ranker: 'network-simplex' // 'tight-tree', 'longest-path'
86
+ // acyclicer: 'greedy'
87
+ nodesep: 5,
88
+ edgesep: 1,
89
+ ranksep: 20,
90
+ ...graphCfg
91
+ });
92
+
93
+ nodes.forEach(node =>
94
+ g.setNode(
95
+ node[nodeId],
96
+ Object.assign({}, node, {
97
+ width: getNodeWidth(node),
98
+ height: getNodeHeight(node)
99
+ })
100
+ )
101
+ );
102
+ links.forEach(link =>
103
+ g.setEdge(link[linkSource], link[linkTarget], Object.assign({}, link))
104
+ );
105
+
106
+ dagre.layout(g);
107
+
108
+ return {
109
+ nodes: g.nodes().map(n => {
110
+ const node = g.node(n);
111
+ delete node.width;
112
+ delete node.height;
113
+ return node;
114
+ }),
115
+ links: g.edges().map(e => g.edge(e))
116
+ };
117
+ }
118
+ </script>
119
+ </body>
@@ -0,0 +1,47 @@
1
+ <head>
2
+ <style> body { margin: 0; } </style>
3
+
4
+ <script src="//unpkg.com/force-graph"></script>
5
+ <!-- <script src="../../dist/force-graph.js"></script>-->
6
+ </head>
7
+
8
+ <body>
9
+ <div id="graph"></div>
10
+
11
+ <script>
12
+ // Random tree
13
+ const N = 100;
14
+ const gData = {
15
+ nodes: [...Array(N).keys()].map(i => ({ id: i })),
16
+ links: [...Array(N).keys()]
17
+ .filter(id => id)
18
+ .map(id => ({
19
+ source: id,
20
+ target: Math.round(Math.random() * (id-1)),
21
+ dashed: (id % 2 === 0)
22
+ }))
23
+ };
24
+
25
+ const elem = document.getElementById('graph');
26
+
27
+ const dashLen = 6;
28
+ const gapLen = 8;
29
+
30
+ const Graph = ForceGraph()(elem)
31
+ .graphData(gData)
32
+ .nodeRelSize(8)
33
+ .linkWidth(3)
34
+ .linkLineDash(link => link.dashed && [dashLen, gapLen]);
35
+
36
+ // Dash animation
37
+ const st = +new Date();
38
+ const dashAnimateTime = 300; // time to animate a single dash
39
+ (function animate() {
40
+ const t = ((+new Date() - st) % dashAnimateTime) / dashAnimateTime;
41
+ const lineDash = t < 0.5 ? [0, gapLen * t * 2, dashLen, gapLen * (1 - t * 2)] : [dashLen * (t - 0.5) * 2, gapLen, dashLen * (1 - (t - 0.5) * 2), 0];
42
+ Graph.linkLineDash(link => link.dashed && lineDash);
43
+
44
+ requestAnimationFrame(animate);
45
+ })(); // IIFE
46
+ </script>
47
+ </body>