@dagrejs/dagre
Version:
Graph layout for JavaScript
127 lines (108 loc) • 3.54 kB
JavaScript
let util = require("./util");
module.exports = {
run,
cleanup,
};
/*
* A nesting graph creates dummy nodes for the tops and bottoms of subgraphs,
* adds appropriate edges to ensure that all cluster nodes are placed between
* these boundaries, and ensures that the graph is connected.
*
* In addition we ensure, through the use of the minlen property, that nodes
* and subgraph border nodes to not end up on the same rank.
*
* Preconditions:
*
* 1. Input graph is a DAG
* 2. Nodes in the input graph has a minlen attribute
*
* Postconditions:
*
* 1. Input graph is connected.
* 2. Dummy nodes are added for the tops and bottoms of subgraphs.
* 3. The minlen attribute for nodes is adjusted to ensure nodes do not
* get placed on the same rank as subgraph border nodes.
*
* The nesting graph idea comes from Sander, "Layout of Compound Directed
* Graphs."
*/
function run(g) {
let root = util.addDummyNode(g, "root", {}, "_root");
let depths = treeDepths(g);
let depthsArr = Object.values(depths);
let height = util.applyWithChunking(Math.max, depthsArr) - 1; // Note: depths is an Object not an array
let nodeSep = 2 * height + 1;
g.graph().nestingRoot = root;
// Multiply minlen by nodeSep to align nodes on non-border ranks.
g.edges().forEach(e => g.edge(e).minlen *= nodeSep);
// Calculate a weight that is sufficient to keep subgraphs vertically compact
let weight = sumWeights(g) + 1;
// Create border nodes and link them up
g.children().forEach(child => dfs(g, root, nodeSep, weight, height, depths, child));
// Save the multiplier for node layers for later removal of empty border
// layers.
g.graph().nodeRankFactor = nodeSep;
}
function dfs(g, root, nodeSep, weight, height, depths, v) {
let children = g.children(v);
if (!children.length) {
if (v !== root) {
g.setEdge(root, v, { weight: 0, minlen: nodeSep });
}
return;
}
let top = util.addBorderNode(g, "_bt");
let bottom = util.addBorderNode(g, "_bb");
let label = g.node(v);
g.setParent(top, v);
label.borderTop = top;
g.setParent(bottom, v);
label.borderBottom = bottom;
children.forEach(child => {
dfs(g, root, nodeSep, weight, height, depths, child);
let childNode = g.node(child);
let childTop = childNode.borderTop ? childNode.borderTop : child;
let childBottom = childNode.borderBottom ? childNode.borderBottom : child;
let thisWeight = childNode.borderTop ? weight : 2 * weight;
let minlen = childTop !== childBottom ? 1 : height - depths[v] + 1;
g.setEdge(top, childTop, {
weight: thisWeight,
minlen: minlen,
nestingEdge: true
});
g.setEdge(childBottom, bottom, {
weight: thisWeight,
minlen: minlen,
nestingEdge: true
});
});
if (!g.parent(v)) {
g.setEdge(root, top, { weight: 0, minlen: height + depths[v] });
}
}
function treeDepths(g) {
var depths = {};
function dfs(v, depth) {
var children = g.children(v);
if (children && children.length) {
children.forEach(child => dfs(child, depth + 1));
}
depths[v] = depth;
}
g.children().forEach(v => dfs(v, 1));
return depths;
}
function sumWeights(g) {
return g.edges().reduce((acc, e) => acc + g.edge(e).weight, 0);
}
function cleanup(g) {
var graphLabel = g.graph();
g.removeNode(graphLabel.nestingRoot);
delete graphLabel.nestingRoot;
g.edges().forEach(e => {
var edge = g.edge(e);
if (edge.nestingEdge) {
g.removeEdge(e);
}
});
}