UNPKG

force-graph

Version:

2D force-directed graph rendered on HTML5 canvas

108 lines (96 loc) 3.89 kB
<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>