force-graph
Version:
2D force-directed graph rendered on HTML5 canvas
108 lines (96 loc) • 3.89 kB
HTML
<head>
<style> body { margin: 0; } </style>
<script src="//cdn.jsdelivr.net/npm/force-graph"></script>
<!--<script src="../../dist/force-graph.js"></script>-->
</head>
<body>
<br/>
<div style="text-align: center; color: silver">
<b>New node:</b> click on the canvas, <b>New link:</b> drag one node close enough to another one,
<b>Rename</b> node or link by clicking on it, <b>Remove</b> node or link by right-clicking on it
</div>
<div id="graph"></div>
<script>
let nodeIdCounter = 0, linkIdCounter = 0;
let nodes = [], links = [];
let dragSourceNode = null, interimLink = null;
const snapInDistance = 15;
const snapOutDistance = 40;
const updateGraphData = () => {
Graph.graphData({ nodes: nodes, links: links });
};
const distance = (node1, node2) => {
return Math.sqrt(Math.pow(node1.x - node2.x, 2) + Math.pow(node1.y - node2.y, 2));
};
const rename = (nodeOrLink, type) => {
let value = prompt('Name this ' + type + ':', nodeOrLink.name);
if (!value) {
return;
}
nodeOrLink.name = value;
updateGraphData();
};
const setInterimLink = (source, target) => {
let linkId = linkIdCounter ++;
interimLink = { id: linkId, source: source, target: target, name: 'link_' + linkId };
links.push(interimLink);
updateGraphData();
};
const removeLink = link => {
links.splice(links.indexOf(link), 1);
};
const removeInterimLinkWithoutAddingIt = () => {
removeLink(interimLink);
interimLink = null;
updateGraphData();
};
const removeNode = node => {
links.filter(link => link.source === node || link.target === node).forEach(link => removeLink(link));
nodes.splice(nodes.indexOf(node), 1);
};
const Graph = new ForceGraph(document.getElementById('graph'))
.linkDirectionalArrowLength(6)
.linkDirectionalArrowRelPos(1)
.onNodeDrag(dragNode => {
dragSourceNode = dragNode;
for (let node of nodes) {
if (dragNode === node) {
continue;
}
// close enough: snap onto node as target for suggested link
if (!interimLink && distance(dragNode, node) < snapInDistance) {
setInterimLink(dragSourceNode, node);
}
// close enough to other node: snap over to other node as target for suggested link
if (interimLink && node !== interimLink.target && distance(dragNode, node) < snapInDistance) {
removeLink(interimLink);
setInterimLink(dragSourceNode, node);
}
}
// far away enough: snap out of the current target node
if (interimLink && distance(dragNode, interimLink.target) > snapOutDistance) {
removeInterimLinkWithoutAddingIt();
}
})
.onNodeDragEnd(() => {
dragSourceNode = null;
interimLink = null;
updateGraphData();
})
.nodeColor(node => node === dragSourceNode || (interimLink &&
(node === interimLink.source || node === interimLink.target)) ? 'orange' : null)
.linkColor(link => link === interimLink ? 'orange' : '#bbbbbb')
.linkLineDash(link => link === interimLink ? [2, 2] : [])
.onNodeClick((node, event) => rename(node, 'node'))
.onNodeRightClick((node, event) => removeNode(node))
.onLinkClick((link, event) => rename(link, 'link'))
.onLinkRightClick((link, event) => removeLink(link))
.onBackgroundClick(event => {
let coords = Graph.screen2GraphCoords(event.layerX, event.layerY);
let nodeId = nodeIdCounter ++;
nodes.push({ id: nodeId, x: coords.x, y: coords.y, name: 'node_' + nodeId });
updateGraphData();
});
updateGraphData();
</script>
</body>