nesting-graph.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import _ from 'lodash'
  2. import util from './util'
  3. /*
  4. * A nesting graph creates dummy nodes for the tops and bottoms of subgraphs,
  5. * adds appropriate edges to ensure that all cluster nodes are placed between
  6. * these boundries, and ensures that the graph is connected.
  7. *
  8. * In addition we ensure, through the use of the minlen property, that nodes
  9. * and subgraph border nodes to not end up on the same rank.
  10. *
  11. * Preconditions:
  12. *
  13. * 1. Input graph is a DAG
  14. * 2. Nodes in the input graph has a minlen attribute
  15. *
  16. * Postconditions:
  17. *
  18. * 1. Input graph is connected.
  19. * 2. Dummy nodes are added for the tops and bottoms of subgraphs.
  20. * 3. The minlen attribute for nodes is adjusted to ensure nodes do not
  21. * get placed on the same rank as subgraph border nodes.
  22. *
  23. * The nesting graph idea comes from Sander, "Layout of Compound Directed
  24. * Graphs."
  25. */
  26. function run (g) {
  27. const root = util.addDummyNode(g, 'root', {}, '_root')
  28. const depths = treeDepths(g)
  29. const height = _.max(_.values(depths)) - 1
  30. const nodeSep = 2 * height + 1
  31. g.graph().nestingRoot = root
  32. // Multiply minlen by nodeSep to align nodes on non-border ranks.
  33. _.forEach(g.edges(), function (e) { g.edge(e).minlen *= nodeSep })
  34. // Calculate a weight that is sufficient to keep subgraphs vertically compact
  35. const weight = sumWeights(g) + 1
  36. // Create border nodes and link them up
  37. _.forEach(g.children(), function (child) {
  38. dfs(g, root, nodeSep, weight, height, depths, child)
  39. })
  40. // Save the multiplier for node layers for later removal of empty border
  41. // layers.
  42. g.graph().nodeRankFactor = nodeSep
  43. }
  44. function dfs (g, root, nodeSep, weight, height, depths, v) {
  45. const children = g.children(v)
  46. if (!children.length) {
  47. if (v !== root) {
  48. g.setEdge(root, v, { weight: 0, minlen: nodeSep })
  49. }
  50. return
  51. }
  52. const top = util.addBorderNode(g, '_bt')
  53. const bottom = util.addBorderNode(g, '_bb')
  54. const label = g.node(v)
  55. g.setParent(top, v)
  56. label.borderTop = top
  57. g.setParent(bottom, v)
  58. label.borderBottom = bottom
  59. _.forEach(children, function (child) {
  60. dfs(g, root, nodeSep, weight, height, depths, child)
  61. const childNode = g.node(child)
  62. const childTop = childNode.borderTop ? childNode.borderTop : child
  63. const childBottom = childNode.borderBottom ? childNode.borderBottom : child
  64. const thisWeight = childNode.borderTop ? weight : 2 * weight
  65. const minlen = childTop !== childBottom ? 1 : height - depths[v] + 1
  66. g.setEdge(top, childTop, {
  67. weight: thisWeight,
  68. minlen: minlen,
  69. nestingEdge: true
  70. })
  71. g.setEdge(childBottom, bottom, {
  72. weight: thisWeight,
  73. minlen: minlen,
  74. nestingEdge: true
  75. })
  76. })
  77. if (!g.parent(v)) {
  78. g.setEdge(root, top, { weight: 0, minlen: height + depths[v] })
  79. }
  80. }
  81. function treeDepths (g) {
  82. const depths = {}
  83. function dfs (v, depth) {
  84. const children = g.children(v)
  85. if (children && children.length) {
  86. _.forEach(children, function (child) {
  87. dfs(child, depth + 1)
  88. })
  89. }
  90. depths[v] = depth
  91. }
  92. _.forEach(g.children(), function (v) { dfs(v, 1) })
  93. return depths
  94. }
  95. function sumWeights (g) {
  96. return _.reduce(g.edges(), function (acc, e) {
  97. return acc + g.edge(e).weight
  98. }, 0)
  99. }
  100. function cleanup (g) {
  101. const graphLabel = g.graph()
  102. g.removeNode(graphLabel.nestingRoot)
  103. delete graphLabel.nestingRoot
  104. _.forEach(g.edges(), function (e) {
  105. const edge = g.edge(e)
  106. if (edge.nestingEdge) {
  107. g.removeEdge(e)
  108. }
  109. })
  110. }
  111. export default {
  112. run,
  113. cleanup
  114. }