layout.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import _ from 'lodash'
  2. import { Graph } from 'graphlibrary'
  3. import acyclic from './acyclic'
  4. import normalize from './normalize'
  5. import rank from './rank'
  6. import util, { normalizeRanks, removeEmptyRanks } from './util'
  7. import parentDummyChains from './parent-dummy-chains'
  8. import nestingGraph from './nesting-graph'
  9. import addBorderSegments from './add-border-segments'
  10. import coordinateSystem from './coordinate-system'
  11. import order from './order'
  12. import position from './position'
  13. function layout (g, opts) {
  14. const time = opts && opts.debugTiming ? util.time : util.notime
  15. time('layout', function () {
  16. const layoutGraph = time(' buildLayoutGraph',
  17. function () { return buildLayoutGraph(g) })
  18. time(' runLayout', function () { runLayout(layoutGraph, time) })
  19. time(' updateInputGraph', function () { updateInputGraph(g, layoutGraph) })
  20. })
  21. }
  22. function runLayout (g, time) {
  23. time(' makeSpaceForEdgeLabels', function () { makeSpaceForEdgeLabels(g) })
  24. time(' removeSelfEdges', function () { removeSelfEdges(g) })
  25. time(' acyclic', function () { acyclic.run(g) })
  26. time(' nestingGraph.run', function () { nestingGraph.run(g) })
  27. time(' rank', function () { rank(util.asNonCompoundGraph(g)) })
  28. time(' injectEdgeLabelProxies', function () { injectEdgeLabelProxies(g) })
  29. time(' removeEmptyRanks', function () { removeEmptyRanks(g) })
  30. time(' nestingGraph.cleanup', function () { nestingGraph.cleanup(g) })
  31. time(' normalizeRanks', function () { normalizeRanks(g) })
  32. time(' assignRankMinMax', function () { assignRankMinMax(g) })
  33. time(' removeEdgeLabelProxies', function () { removeEdgeLabelProxies(g) })
  34. time(' normalize.run', function () { normalize.run(g) })
  35. time(' parentDummyChains', function () { parentDummyChains(g) })
  36. time(' addBorderSegments', function () { addBorderSegments(g) })
  37. time(' order', function () { order(g) })
  38. time(' insertSelfEdges', function () { insertSelfEdges(g) })
  39. time(' adjustCoordinateSystem', function () { coordinateSystem.adjust(g) })
  40. time(' position', function () { position(g) })
  41. time(' positionSelfEdges', function () { positionSelfEdges(g) })
  42. time(' removeBorderNodes', function () { removeBorderNodes(g) })
  43. time(' normalize.undo', function () { normalize.undo(g) })
  44. time(' fixupEdgeLabelCoords', function () { fixupEdgeLabelCoords(g) })
  45. time(' undoCoordinateSystem', function () { coordinateSystem.undo(g) })
  46. time(' translateGraph', function () { translateGraph(g) })
  47. time(' assignNodeIntersects', function () { assignNodeIntersects(g) })
  48. time(' reversePoints', function () { reversePointsForReversedEdges(g) })
  49. time(' acyclic.undo', function () { acyclic.undo(g) })
  50. }
  51. /*
  52. * Copies final layout information from the layout graph back to the input
  53. * graph. This process only copies whitelisted attributes from the layout graph
  54. * to the input graph, so it serves as a good place to determine what
  55. * attributes can influence layout.
  56. */
  57. function updateInputGraph (inputGraph, layoutGraph) {
  58. _.forEach(inputGraph.nodes(), function (v) {
  59. const inputLabel = inputGraph.node(v)
  60. const layoutLabel = layoutGraph.node(v)
  61. if (inputLabel) {
  62. inputLabel.x = layoutLabel.x
  63. inputLabel.y = layoutLabel.y
  64. if (layoutGraph.children(v).length) {
  65. inputLabel.width = layoutLabel.width
  66. inputLabel.height = layoutLabel.height
  67. }
  68. }
  69. })
  70. _.forEach(inputGraph.edges(), function (e) {
  71. const inputLabel = inputGraph.edge(e)
  72. const layoutLabel = layoutGraph.edge(e)
  73. inputLabel.points = layoutLabel.points
  74. if (_.has(layoutLabel, 'x')) {
  75. inputLabel.x = layoutLabel.x
  76. inputLabel.y = layoutLabel.y
  77. }
  78. })
  79. inputGraph.graph().width = layoutGraph.graph().width
  80. inputGraph.graph().height = layoutGraph.graph().height
  81. }
  82. const graphNumAttrs = ['nodesep', 'edgesep', 'ranksep', 'marginx', 'marginy']
  83. const graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: 'tb' }
  84. const graphAttrs = ['acyclicer', 'ranker', 'rankdir', 'align']
  85. const nodeNumAttrs = ['width', 'height']
  86. const nodeDefaults = { width: 0, height: 0 }
  87. const edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset']
  88. const edgeDefaults = {
  89. minlen: 1,
  90. weight: 1,
  91. width: 0,
  92. height: 0,
  93. labeloffset: 10,
  94. labelpos: 'r'
  95. }
  96. const edgeAttrs = ['labelpos']
  97. /*
  98. * Constructs a new graph from the input graph, which can be used for layout.
  99. * This process copies only whitelisted attributes from the input graph to the
  100. * layout graph. Thus this function serves as a good place to determine what
  101. * attributes can influence layout.
  102. */
  103. function buildLayoutGraph (inputGraph) {
  104. const g = new Graph({ multigraph: true, compound: true })
  105. const graph = canonicalize(inputGraph.graph())
  106. g.setGraph(_.merge({},
  107. graphDefaults,
  108. selectNumberAttrs(graph, graphNumAttrs),
  109. _.pick(graph, graphAttrs)))
  110. _.forEach(inputGraph.nodes(), function (v) {
  111. const node = canonicalize(inputGraph.node(v))
  112. g.setNode(v, _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults))
  113. g.setParent(v, inputGraph.parent(v))
  114. })
  115. _.forEach(inputGraph.edges(), function (e) {
  116. const edge = canonicalize(inputGraph.edge(e))
  117. g.setEdge(e, _.merge({},
  118. edgeDefaults,
  119. selectNumberAttrs(edge, edgeNumAttrs),
  120. _.pick(edge, edgeAttrs)))
  121. })
  122. return g
  123. }
  124. /*
  125. * This idea comes from the Gansner paper: to account for edge labels in our
  126. * layout we split each rank in half by doubling minlen and halving ranksep.
  127. * Then we can place labels at these mid-points between nodes.
  128. *
  129. * We also add some minimal padding to the width to push the label for the edge
  130. * away from the edge itself a bit.
  131. */
  132. function makeSpaceForEdgeLabels (g) {
  133. const graph = g.graph()
  134. graph.ranksep /= 2
  135. _.forEach(g.edges(), function (e) {
  136. const edge = g.edge(e)
  137. edge.minlen *= 2
  138. if (edge.labelpos.toLowerCase() !== 'c') {
  139. if (graph.rankdir === 'TB' || graph.rankdir === 'BT') {
  140. edge.width += edge.labeloffset
  141. } else {
  142. edge.height += edge.labeloffset
  143. }
  144. }
  145. })
  146. }
  147. /*
  148. * Creates temporary dummy nodes that capture the rank in which each edge's
  149. * label is going to, if it has one of non-zero width and height. We do this
  150. * so that we can safely remove empty ranks while preserving balance for the
  151. * label's position.
  152. */
  153. function injectEdgeLabelProxies (g) {
  154. _.forEach(g.edges(), function (e) {
  155. const edge = g.edge(e)
  156. if (edge.width && edge.height) {
  157. const v = g.node(e.v)
  158. const w = g.node(e.w)
  159. const label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e }
  160. util.addDummyNode(g, 'edge-proxy', label, '_ep')
  161. }
  162. })
  163. }
  164. function assignRankMinMax (g) {
  165. let maxRank = 0
  166. _.forEach(g.nodes(), function (v) {
  167. const node = g.node(v)
  168. if (node.borderTop) {
  169. node.minRank = g.node(node.borderTop).rank
  170. node.maxRank = g.node(node.borderBottom).rank
  171. maxRank = Math.max(maxRank, node.maxRank)
  172. }
  173. })
  174. g.graph().maxRank = maxRank
  175. }
  176. function removeEdgeLabelProxies (g) {
  177. _.forEach(g.nodes(), function (v) {
  178. const node = g.node(v)
  179. if (node.dummy === 'edge-proxy') {
  180. g.edge(node.e).labelRank = node.rank
  181. g.removeNode(v)
  182. }
  183. })
  184. }
  185. function translateGraph (g) {
  186. let minX = Number.POSITIVE_INFINITY
  187. let maxX = 0
  188. let minY = Number.POSITIVE_INFINITY
  189. let maxY = 0
  190. const graphLabel = g.graph()
  191. const marginX = graphLabel.marginx || 0
  192. const marginY = graphLabel.marginy || 0
  193. function getExtremes (attrs) {
  194. const x = attrs.x
  195. const y = attrs.y
  196. const w = attrs.width
  197. const h = attrs.height
  198. minX = Math.min(minX, x - w / 2)
  199. maxX = Math.max(maxX, x + w / 2)
  200. minY = Math.min(minY, y - h / 2)
  201. maxY = Math.max(maxY, y + h / 2)
  202. }
  203. _.forEach(g.nodes(), function (v) { getExtremes(g.node(v)) })
  204. _.forEach(g.edges(), function (e) {
  205. const edge = g.edge(e)
  206. if (_.has(edge, 'x')) {
  207. getExtremes(edge)
  208. }
  209. })
  210. minX -= marginX
  211. minY -= marginY
  212. _.forEach(g.nodes(), function (v) {
  213. const node = g.node(v)
  214. node.x -= minX
  215. node.y -= minY
  216. })
  217. _.forEach(g.edges(), function (e) {
  218. const edge = g.edge(e)
  219. _.forEach(edge.points, function (p) {
  220. p.x -= minX
  221. p.y -= minY
  222. })
  223. if (_.has(edge, 'x')) { edge.x -= minX }
  224. if (_.has(edge, 'y')) { edge.y -= minY }
  225. })
  226. graphLabel.width = maxX - minX + marginX
  227. graphLabel.height = maxY - minY + marginY
  228. }
  229. function assignNodeIntersects (g) {
  230. _.forEach(g.edges(), function (e) {
  231. const edge = g.edge(e)
  232. const nodeV = g.node(e.v)
  233. const nodeW = g.node(e.w)
  234. let p1 = null
  235. let p2 = null
  236. if (!edge.points) {
  237. edge.points = []
  238. p1 = nodeW
  239. p2 = nodeV
  240. } else {
  241. p1 = edge.points[0]
  242. p2 = edge.points[edge.points.length - 1]
  243. }
  244. edge.points.unshift(util.intersectRect(nodeV, p1))
  245. edge.points.push(util.intersectRect(nodeW, p2))
  246. })
  247. }
  248. function fixupEdgeLabelCoords (g) {
  249. _.forEach(g.edges(), function (e) {
  250. const edge = g.edge(e)
  251. if (_.has(edge, 'x')) {
  252. if (edge.labelpos === 'l' || edge.labelpos === 'r') {
  253. edge.width -= edge.labeloffset
  254. }
  255. switch (edge.labelpos) {
  256. case 'l': edge.x -= edge.width / 2 + edge.labeloffset; break
  257. case 'r': edge.x += edge.width / 2 + edge.labeloffset; break
  258. }
  259. }
  260. })
  261. }
  262. function reversePointsForReversedEdges (g) {
  263. _.forEach(g.edges(), function (e) {
  264. const edge = g.edge(e)
  265. if (edge.reversed) {
  266. edge.points.reverse()
  267. }
  268. })
  269. }
  270. function removeBorderNodes (g) {
  271. _.forEach(g.nodes(), function (v) {
  272. if (g.children(v).length) {
  273. const node = g.node(v)
  274. const t = g.node(node.borderTop)
  275. const b = g.node(node.borderBottom)
  276. const l = g.node(_.last(node.borderLeft))
  277. const r = g.node(_.last(node.borderRight))
  278. node.width = Math.abs(r.x - l.x)
  279. node.height = Math.abs(b.y - t.y)
  280. node.x = l.x + node.width / 2
  281. node.y = t.y + node.height / 2
  282. }
  283. })
  284. _.forEach(g.nodes(), function (v) {
  285. if (g.node(v).dummy === 'border') {
  286. g.removeNode(v)
  287. }
  288. })
  289. }
  290. function removeSelfEdges (g) {
  291. _.forEach(g.edges(), function (e) {
  292. if (e.v === e.w) {
  293. const node = g.node(e.v)
  294. if (!node.selfEdges) {
  295. node.selfEdges = []
  296. }
  297. node.selfEdges.push({ e: e, label: g.edge(e) })
  298. g.removeEdge(e)
  299. }
  300. })
  301. }
  302. function insertSelfEdges (g) {
  303. const layers = util.buildLayerMatrix(g)
  304. _.forEach(layers, function (layer) {
  305. let orderShift = 0
  306. _.forEach(layer, function (v, i) {
  307. const node = g.node(v)
  308. node.order = i + orderShift
  309. _.forEach(node.selfEdges, function (selfEdge) {
  310. util.addDummyNode(g, 'selfedge', {
  311. width: selfEdge.label.width,
  312. height: selfEdge.label.height,
  313. rank: node.rank,
  314. order: i + (++orderShift),
  315. e: selfEdge.e,
  316. label: selfEdge.label
  317. }, '_se')
  318. })
  319. delete node.selfEdges
  320. })
  321. })
  322. }
  323. function positionSelfEdges (g) {
  324. _.forEach(g.nodes(), function (v) {
  325. const node = g.node(v)
  326. if (node.dummy === 'selfedge') {
  327. const selfNode = g.node(node.e.v)
  328. const x = selfNode.x + selfNode.width / 2
  329. const y = selfNode.y
  330. const dx = node.x - x
  331. const dy = selfNode.height / 2
  332. g.setEdge(node.e, node.label)
  333. g.removeNode(v)
  334. node.label.points = [
  335. { x: x + 2 * dx / 3, y: y - dy },
  336. { x: x + 5 * dx / 6, y: y - dy },
  337. { x: x + dx, y: y },
  338. { x: x + 5 * dx / 6, y: y + dy },
  339. { x: x + 2 * dx / 3, y: y + dy }
  340. ]
  341. node.label.x = node.x
  342. node.label.y = node.y
  343. }
  344. })
  345. }
  346. function selectNumberAttrs (obj, attrs) {
  347. return _.mapValues(_.pick(obj, attrs), Number)
  348. }
  349. function canonicalize (attrs) {
  350. const newAttrs = {}
  351. _.forEach(attrs, function (v, k) {
  352. newAttrs[k.toLowerCase()] = v
  353. })
  354. return newAttrs
  355. }
  356. export default layout