UNPKG

@dagrejs/dagre

Version:

Graph layout for JavaScript

125 lines (101 loc) 3.35 kB
let Graph = require("@dagrejs/graphlib").Graph; let List = require("./data/list"); /* * A greedy heuristic for finding a feedback arc set for a graph. A feedback * arc set is a set of edges that can be removed to make a graph acyclic. * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and * effective heuristic for the feedback arc set problem." This implementation * adjusts that from the paper to allow for weighted edges. */ module.exports = greedyFAS; let DEFAULT_WEIGHT_FN = () => 1; function greedyFAS(g, weightFn) { if (g.nodeCount() <= 1) { return []; } let state = buildState(g, weightFn || DEFAULT_WEIGHT_FN); let results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx); // Expand multi-edges return results.flatMap(e => g.outEdges(e.v, e.w)); } function doGreedyFAS(g, buckets, zeroIdx) { let results = []; let sources = buckets[buckets.length - 1]; let sinks = buckets[0]; let entry; while (g.nodeCount()) { while ((entry = sinks.dequeue())) { removeNode(g, buckets, zeroIdx, entry); } while ((entry = sources.dequeue())) { removeNode(g, buckets, zeroIdx, entry); } if (g.nodeCount()) { for (let i = buckets.length - 2; i > 0; --i) { entry = buckets[i].dequeue(); if (entry) { results = results.concat(removeNode(g, buckets, zeroIdx, entry, true)); break; } } } } return results; } function removeNode(g, buckets, zeroIdx, entry, collectPredecessors) { let results = collectPredecessors ? [] : undefined; g.inEdges(entry.v).forEach(edge => { let weight = g.edge(edge); let uEntry = g.node(edge.v); if (collectPredecessors) { results.push({ v: edge.v, w: edge.w }); } uEntry.out -= weight; assignBucket(buckets, zeroIdx, uEntry); }); g.outEdges(entry.v).forEach(edge => { let weight = g.edge(edge); let w = edge.w; let wEntry = g.node(w); wEntry["in"] -= weight; assignBucket(buckets, zeroIdx, wEntry); }); g.removeNode(entry.v); return results; } function buildState(g, weightFn) { let fasGraph = new Graph(); let maxIn = 0; let maxOut = 0; g.nodes().forEach(v => { fasGraph.setNode(v, { v: v, "in": 0, out: 0 }); }); // Aggregate weights on nodes, but also sum the weights across multi-edges // into a single edge for the fasGraph. g.edges().forEach(e => { let prevWeight = fasGraph.edge(e.v, e.w) || 0; let weight = weightFn(e); let edgeWeight = prevWeight + weight; fasGraph.setEdge(e.v, e.w, edgeWeight); maxOut = Math.max(maxOut, fasGraph.node(e.v).out += weight); maxIn = Math.max(maxIn, fasGraph.node(e.w)["in"] += weight); }); let buckets = range(maxOut + maxIn + 3).map(() => new List()); let zeroIdx = maxIn + 1; fasGraph.nodes().forEach(v => { assignBucket(buckets, zeroIdx, fasGraph.node(v)); }); return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx }; } function assignBucket(buckets, zeroIdx, entry) { if (!entry.out) { buckets[0].enqueue(entry); } else if (!entry["in"]) { buckets[buckets.length - 1].enqueue(entry); } else { buckets[entry.out - entry["in"] + zeroIdx].enqueue(entry); } } function range(limit) { const range = []; for (let i = 0; i < limit; i++) { range.push(i); } return range; }