UNPKG

force-graph

Version:

2D force-directed graph rendered on HTML5 canvas

51 lines (44 loc) 1.87 kB
export default function({ nodes, links }, idAccessor, { nodeFilter = () => true, onLoopError = loopIds => { throw `Invalid DAG structure! Found cycle in node path: ${loopIds.join(' -> ')}.` } } = {}) { // linked graph const graph = {}; nodes.forEach(node => graph[idAccessor(node)] = { data: node, out : [], depth: -1, skip: !nodeFilter(node) }); links.forEach(({ source, target }) => { const sourceId = getNodeId(source); const targetId = getNodeId(target); if (!graph.hasOwnProperty(sourceId)) throw `Missing source node with id: ${sourceId}`; if (!graph.hasOwnProperty(targetId)) throw `Missing target node with id: ${targetId}`; const sourceNode = graph[sourceId]; const targetNode = graph[targetId]; sourceNode.out.push(targetNode); function getNodeId(node) { return typeof node === 'object' ? idAccessor(node) : node; } }); const foundLoops = []; traverse(Object.values(graph)); const nodeDepths = Object.assign({}, ...Object.entries(graph) .filter(([, node]) => !node.skip) .map(([id, node]) => ({ [id]: node.depth })) ); return nodeDepths; function traverse(nodes, nodeStack = [], currentDepth = 0) { for (let i=0, l=nodes.length; i<l; i++) { const node = nodes[i]; if (nodeStack.indexOf(node) !== -1) { const loop = [...nodeStack.slice(nodeStack.indexOf(node)), node].map(d => idAccessor(d.data)); if (!foundLoops.some(foundLoop => foundLoop.length === loop.length && foundLoop.every((id, idx) => id === loop[idx]))) { foundLoops.push(loop); onLoopError(loop); } continue; } if (currentDepth > node.depth) { // Don't unnecessarily revisit chunks of the graph node.depth = currentDepth; traverse(node.out, [...nodeStack, node], currentDepth + (node.skip ? 0 : 1)); } } } }