| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- import _ from 'lodash'
- import util from './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."
- */
- function run (g) {
- const root = util.addDummyNode(g, 'root', {}, '_root')
- const depths = treeDepths(g)
- const height = _.max(_.values(depths)) - 1
- const 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
- const 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) {
- const children = g.children(v)
- if (!children.length) {
- if (v !== root) {
- g.setEdge(root, v, { weight: 0, minlen: nodeSep })
- }
- return
- }
- const top = util.addBorderNode(g, '_bt')
- const bottom = util.addBorderNode(g, '_bb')
- const 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)
- const childNode = g.node(child)
- const childTop = childNode.borderTop ? childNode.borderTop : child
- const childBottom = childNode.borderBottom ? childNode.borderBottom : child
- const thisWeight = childNode.borderTop ? weight : 2 * weight
- const 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) {
- const depths = {}
- function dfs (v, depth) {
- const 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) {
- const graphLabel = g.graph()
- g.removeNode(graphLabel.nestingRoot)
- delete graphLabel.nestingRoot
- _.forEach(g.edges(), function (e) {
- const edge = g.edge(e)
- if (edge.nestingEdge) {
- g.removeEdge(e)
- }
- })
- }
- export default {
- run,
- cleanup
- }
|