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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/force-graph.common.js +1754 -0
- package/dist/force-graph.d.ts +195 -0
- package/dist/force-graph.js +12168 -0
- package/dist/force-graph.js.map +1 -0
- package/dist/force-graph.min.js +5 -0
- package/dist/force-graph.module.js +1743 -0
- package/example/auto-colored/index.html +34 -0
- package/example/basic/index.html +29 -0
- package/example/build-a-graph/index.html +108 -0
- package/example/click-to-focus/index.html +28 -0
- package/example/collision-detection/index.html +50 -0
- package/example/curved-links/index.html +37 -0
- package/example/curved-links-computed-curvature/index.html +76 -0
- package/example/custom-node-shape/index.html +44 -0
- package/example/dag-yarn/index.html +96 -0
- package/example/dagre/index.html +119 -0
- package/example/dash-odd-links/index.html +47 -0
- package/example/datasets/blocks.json +1 -0
- package/example/datasets/d3-dependencies.csv +464 -0
- package/example/datasets/miserables.json +337 -0
- package/example/datasets/mplate.mtx +74090 -0
- package/example/directional-links-arrows/index.html +29 -0
- package/example/directional-links-particles/index.html +22 -0
- package/example/dynamic/index.html +42 -0
- package/example/emit-particles/index.html +50 -0
- package/example/expandable-nodes/index.html +66 -0
- package/example/expandable-tree/index.html +85 -0
- package/example/fit-to-canvas/index.html +34 -0
- package/example/fix-dragged-nodes/index.html +24 -0
- package/example/highlight/index.html +84 -0
- package/example/huge-1M/index.html +37 -0
- package/example/img-nodes/imgs/cat.jpg +0 -0
- package/example/img-nodes/imgs/dog.jpg +0 -0
- package/example/img-nodes/imgs/eagle.jpg +0 -0
- package/example/img-nodes/imgs/elephant.jpg +0 -0
- package/example/img-nodes/imgs/grasshopper.jpg +0 -0
- package/example/img-nodes/imgs/octopus.jpg +0 -0
- package/example/img-nodes/imgs/owl.jpg +0 -0
- package/example/img-nodes/imgs/panda.jpg +0 -0
- package/example/img-nodes/imgs/squirrel.jpg +0 -0
- package/example/img-nodes/imgs/tiger.jpg +0 -0
- package/example/img-nodes/imgs/whale.jpg +0 -0
- package/example/img-nodes/index.html +43 -0
- package/example/large-graph/index.html +41 -0
- package/example/load-json/index.html +24 -0
- package/example/medium-graph/index.html +26 -0
- package/example/medium-graph/preview.png +0 -0
- package/example/move-viewport/index.html +42 -0
- package/example/multi-selection/index.html +57 -0
- package/example/responsive/index.html +37 -0
- package/example/text-links/index.html +69 -0
- package/example/text-nodes/index.html +42 -0
- package/example/tree/index.html +71 -0
- package/package.json +72 -0
- package/src/canvas-force-graph.js +544 -0
- package/src/color-utils.js +17 -0
- package/src/dagDepths.js +51 -0
- package/src/force-graph.css +35 -0
- package/src/force-graph.js +644 -0
- package/src/index.d.ts +195 -0
- package/src/index.js +3 -0
- 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>
|