blob: e14880c808856037ddd32595ec7f6834a66a0579 [file] [log] [blame]
library graphlib.layout.nesting_graph;
import "../graph.dart" show Graph;
import "util.dart" as util;
/// 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."
run(Graph g) {
var root = util.addDummyNode(g, "root", {}, "_root"),
depths = treeDepths(g);
var height = util.max(depths.values) - 1,
nodeSep = 2 * height + 1;
g.graph().nestingRoot = root;
// Multiply minlen by nodeSep to align nodes on non-border ranks.
g.edges.forEach((e) { g.edgeObj(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
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;
}
dfs(Graph g, root, num nodeSep, num weight, num height, Map depths, v) {
var children = g.children(v);
if (children.length == 0) {
if (v != root) {
g.setEdge(root, v, { "weight": 0, "minlen": nodeSep });
}
return;
}
var top = util.addBorderNode(g, "_bt"),
bottom = util.addBorderNode(g, "_bb"),
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);
var childNode = g.node(child),
childTop = childNode.borderTop ? childNode.borderTop : child,
childBottom = childNode.borderBottom ? childNode.borderBottom : child,
thisWeight = childNode.borderTop ? weight : 2 * weight,
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) == null) {
g.setEdge(root, top, { "weight": 0, "minlen": height + depths[v] });
}
}
Map treeDepths(Graph g) {
var depths = {};
dfs(v, depth) {
var children = g.children(v);
if (children != null && children.length != 0) {
children.forEach((child) {
dfs(child, depth + 1);
});
}
depths[v] = depth;
}
g.children().forEach((v) { dfs(v, 1); });
return depths;
}
num sumWeights(Graph g) {
num acc = 0;
g.edges.forEach((e) {
return acc + g.edgeObj(e)["weight"];
});
return acc;
}
cleanup(Graph g) {
Map graphLabel = g.graph();
g.removeNode(graphLabel["nestingRoot"]);
graphLabel.remove("nestingRoot");
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
if (edge["nestingEdge"]) {
g.removeEdgeObj(e);
}
});
}