You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
3.6 KiB
132 lines
3.6 KiB
var _ = require("./lodash"); |
|
var util = require("./util"); |
|
|
|
module.exports = { |
|
run: run, |
|
cleanup: 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 boundries, 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) { |
|
var root = util.addDummyNode(g, "root", {}, "_root"); |
|
var depths = treeDepths(g); |
|
var height = _.max(_.values(depths)) - 1; // Note: depths is an Object not an array |
|
var nodeSep = 2 * height + 1; |
|
|
|
g.graph().nestingRoot = root; |
|
|
|
// Multiply minlen by nodeSep to align nodes on non-border ranks. |
|
_.forEach(g.edges(), function(e) { g.edge(e).minlen *= nodeSep; }); |
|
|
|
// Calculate a weight that is sufficient to keep subgraphs vertically compact |
|
var weight = sumWeights(g) + 1; |
|
|
|
// Create border nodes and link them up |
|
_.forEach(g.children(), function(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) { |
|
var children = g.children(v); |
|
if (!children.length) { |
|
if (v !== root) { |
|
g.setEdge(root, v, { weight: 0, minlen: nodeSep }); |
|
} |
|
return; |
|
} |
|
|
|
var top = util.addBorderNode(g, "_bt"); |
|
var bottom = util.addBorderNode(g, "_bb"); |
|
var label = g.node(v); |
|
|
|
g.setParent(top, v); |
|
label.borderTop = top; |
|
g.setParent(bottom, v); |
|
label.borderBottom = bottom; |
|
|
|
_.forEach(children, function(child) { |
|
dfs(g, root, nodeSep, weight, height, depths, child); |
|
|
|
var childNode = g.node(child); |
|
var childTop = childNode.borderTop ? childNode.borderTop : child; |
|
var childBottom = childNode.borderBottom ? childNode.borderBottom : child; |
|
var thisWeight = childNode.borderTop ? weight : 2 * weight; |
|
var 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) { |
|
_.forEach(children, function(child) { |
|
dfs(child, depth + 1); |
|
}); |
|
} |
|
depths[v] = depth; |
|
} |
|
_.forEach(g.children(), function(v) { dfs(v, 1); }); |
|
return depths; |
|
} |
|
|
|
function sumWeights(g) { |
|
return _.reduce(g.edges(), function(acc, e) { |
|
return acc + g.edge(e).weight; |
|
}, 0); |
|
} |
|
|
|
function cleanup(g) { |
|
var graphLabel = g.graph(); |
|
g.removeNode(graphLabel.nestingRoot); |
|
delete graphLabel.nestingRoot; |
|
_.forEach(g.edges(), function(e) { |
|
var edge = g.edge(e); |
|
if (edge.nestingEdge) { |
|
g.removeEdge(e); |
|
} |
|
}); |
|
}
|
|
|