bk.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import _ from 'lodash'
  2. import { Graph } from 'graphlibrary'
  3. import util from '../util'
  4. /*
  5. * This module provides coordinate assignment based on Brandes and Köpf, "Fast
  6. * and Simple Horizontal Coordinate Assignment."
  7. */
  8. /*
  9. * Marks all edges in the graph with a type-1 conflict with the "type1Conflict"
  10. * property. A type-1 conflict is one where a non-inner segment crosses an
  11. * inner segment. An inner segment is an edge with both incident nodes marked
  12. * with the "dummy" property.
  13. *
  14. * This algorithm scans layer by layer, starting with the second, for type-1
  15. * conflicts between the current layer and the previous layer. For each layer
  16. * it scans the nodes from left to right until it reaches one that is incident
  17. * on an inner segment. It then scans predecessors to determine if they have
  18. * edges that cross that inner segment. At the end a final scan is done for all
  19. * nodes on the current rank to see if they cross the last visited inner
  20. * segment.
  21. *
  22. * This algorithm (safely) assumes that a dummy node will only be incident on a
  23. * single node in the layers being scanned.
  24. */
  25. function findType1Conflicts (g, layering) {
  26. const conflicts = {}
  27. function visitLayer (prevLayer, layer) {
  28. // last visited node in the previous layer that is incident on an inner
  29. // segment.
  30. let k0 = 0
  31. // Tracks the last node in this layer scanned for crossings with a type-1
  32. // segment.
  33. let scanPos = 0
  34. const prevLayerLength = prevLayer.length
  35. const lastNode = _.last(layer)
  36. _.forEach(layer, function (v, i) {
  37. const w = findOtherInnerSegmentNode(g, v)
  38. const k1 = w ? g.node(w).order : prevLayerLength
  39. if (w || v === lastNode) {
  40. _.forEach(layer.slice(scanPos, i + 1), function (scanNode) {
  41. _.forEach(g.predecessors(scanNode), function (u) {
  42. const uLabel = g.node(u)
  43. const uPos = uLabel.order
  44. if ((uPos < k0 || k1 < uPos) &&
  45. !(uLabel.dummy && g.node(scanNode).dummy)) {
  46. addConflict(conflicts, u, scanNode)
  47. }
  48. })
  49. })
  50. scanPos = i + 1
  51. k0 = k1
  52. }
  53. })
  54. return layer
  55. }
  56. _.reduce(layering, visitLayer)
  57. return conflicts
  58. }
  59. function findType2Conflicts (g, layering) {
  60. const conflicts = {}
  61. function scan (south, southPos, southEnd, prevNorthBorder, nextNorthBorder) {
  62. let v
  63. _.forEach(_.range(southPos, southEnd), function (i) {
  64. v = south[i]
  65. if (g.node(v).dummy) {
  66. _.forEach(g.predecessors(v), function (u) {
  67. const uNode = g.node(u)
  68. if (uNode.dummy &&
  69. (uNode.order < prevNorthBorder || uNode.order > nextNorthBorder)) {
  70. addConflict(conflicts, u, v)
  71. }
  72. })
  73. }
  74. })
  75. }
  76. function visitLayer (north, south) {
  77. let prevNorthPos = -1
  78. let nextNorthPos
  79. let southPos = 0
  80. _.forEach(south, function (v, southLookahead) {
  81. if (g.node(v).dummy === 'border') {
  82. const predecessors = g.predecessors(v)
  83. if (predecessors.length) {
  84. nextNorthPos = g.node(predecessors[0]).order
  85. scan(south, southPos, southLookahead, prevNorthPos, nextNorthPos)
  86. southPos = southLookahead
  87. prevNorthPos = nextNorthPos
  88. }
  89. }
  90. scan(south, southPos, south.length, nextNorthPos, north.length)
  91. })
  92. return south
  93. }
  94. _.reduce(layering, visitLayer)
  95. return conflicts
  96. }
  97. function findOtherInnerSegmentNode (g, v) {
  98. if (g.node(v).dummy) {
  99. return _.find(g.predecessors(v), function (u) {
  100. return g.node(u).dummy
  101. })
  102. }
  103. }
  104. function addConflict (conflicts, v, w) {
  105. if (v > w) {
  106. const tmp = v
  107. v = w
  108. w = tmp
  109. }
  110. let conflictsV = conflicts[v]
  111. if (!conflictsV) {
  112. conflicts[v] = conflictsV = {}
  113. }
  114. conflictsV[w] = true
  115. }
  116. function hasConflict (conflicts, v, w) {
  117. if (v > w) {
  118. const tmp = v
  119. v = w
  120. w = tmp
  121. }
  122. return _.has(conflicts[v], w)
  123. }
  124. /*
  125. * Try to align nodes into vertical "blocks" where possible. This algorithm
  126. * attempts to align a node with one of its median neighbors. If the edge
  127. * connecting a neighbor is a type-1 conflict then we ignore that possibility.
  128. * If a previous node has already formed a block with a node after the node
  129. * we're trying to form a block with, we also ignore that possibility - our
  130. * blocks would be split in that scenario.
  131. */
  132. function verticalAlignment (g, layering, conflicts, neighborFn) {
  133. const root = {}
  134. const align = {}
  135. const pos = {}
  136. // We cache the position here based on the layering because the graph and
  137. // layering may be out of sync. The layering matrix is manipulated to
  138. // generate different extreme alignments.
  139. _.forEach(layering, function (layer) {
  140. _.forEach(layer, function (v, order) {
  141. root[v] = v
  142. align[v] = v
  143. pos[v] = order
  144. })
  145. })
  146. _.forEach(layering, function (layer) {
  147. let prevIdx = -1
  148. _.forEach(layer, function (v) {
  149. let ws = neighborFn(v)
  150. if (ws.length) {
  151. ws = _.sortBy(ws, function (w) { return pos[w] })
  152. const mp = (ws.length - 1) / 2
  153. for (let i = Math.floor(mp), il = Math.ceil(mp); i <= il; ++i) {
  154. const w = ws[i]
  155. if (align[v] === v && prevIdx < pos[w] && !hasConflict(conflicts, v, w)) {
  156. align[w] = v
  157. align[v] = root[v] = root[w]
  158. prevIdx = pos[w]
  159. }
  160. }
  161. }
  162. })
  163. })
  164. return { root: root, align: align }
  165. }
  166. function horizontalCompaction (g, layering, root, align, reverseSep) {
  167. // This portion of the algorithm differs from BK due to a number of problems.
  168. // Instead of their algorithm we construct a new block graph and do two
  169. // sweeps. The first sweep places blocks with the smallest possible
  170. // coordinates. The second sweep removes unused space by moving blocks to the
  171. // greatest coordinates without violating separation.
  172. const xs = {}
  173. const blockG = buildBlockGraph(g, layering, root, reverseSep)
  174. // First pass, assign smallest coordinates via DFS
  175. const visited = {}
  176. function pass1 (v) {
  177. if (!_.has(visited, v)) {
  178. visited[v] = true
  179. xs[v] = _.reduce(blockG.inEdges(v), function (max, e) {
  180. pass1(e.v)
  181. return Math.max(max, xs[e.v] + blockG.edge(e))
  182. }, 0)
  183. }
  184. }
  185. _.forEach(blockG.nodes(), pass1)
  186. const borderType = reverseSep ? 'borderLeft' : 'borderRight'
  187. function pass2 (v) {
  188. if (visited[v] !== 2) {
  189. visited[v]++
  190. const node = g.node(v)
  191. const min = _.reduce(blockG.outEdges(v), function (min, e) {
  192. pass2(e.w)
  193. return Math.min(min, xs[e.w] - blockG.edge(e))
  194. }, Number.POSITIVE_INFINITY)
  195. if (min !== Number.POSITIVE_INFINITY && node.borderType !== borderType) {
  196. xs[v] = Math.max(xs[v], min)
  197. }
  198. }
  199. }
  200. _.forEach(blockG.nodes(), pass2)
  201. // Assign x coordinates to all nodes
  202. _.forEach(align, function (v) {
  203. xs[v] = xs[root[v]]
  204. })
  205. return xs
  206. }
  207. function buildBlockGraph (g, layering, root, reverseSep) {
  208. const blockGraph = new Graph()
  209. const graphLabel = g.graph()
  210. const sepFn = sep(graphLabel.nodesep, graphLabel.edgesep, reverseSep)
  211. _.forEach(layering, function (layer) {
  212. let u
  213. _.forEach(layer, function (v) {
  214. const vRoot = root[v]
  215. blockGraph.setNode(vRoot)
  216. if (u) {
  217. const uRoot = root[u]
  218. const prevMax = blockGraph.edge(uRoot, vRoot)
  219. blockGraph.setEdge(uRoot, vRoot, Math.max(sepFn(g, v, u), prevMax || 0))
  220. }
  221. u = v
  222. })
  223. })
  224. return blockGraph
  225. }
  226. /*
  227. * Returns the alignment that has the smallest width of the given alignments.
  228. */
  229. function findSmallestWidthAlignment (g, xss) {
  230. return _.minBy(_.values(xss), function (xs) {
  231. const min = (_.minBy(_.toPairs(xs), (pair) => pair[1] - width(g, pair[0]) / 2) || ['k', 0])[1]
  232. const max = (_.maxBy(_.toPairs(xs), (pair) => pair[1] + width(g, pair[0]) / 2) || ['k', 0])[1]
  233. return max - min
  234. })
  235. }
  236. /*
  237. * Align the coordinates of each of the layout alignments such that
  238. * left-biased alignments have their minimum coordinate at the same point as
  239. * the minimum coordinate of the smallest width alignment and right-biased
  240. * alignments have their maximum coordinate at the same point as the maximum
  241. * coordinate of the smallest width alignment.
  242. */
  243. function alignCoordinates (xss, alignTo) {
  244. const alignToVals = _.values(alignTo)
  245. const alignToMin = _.min(alignToVals)
  246. const alignToMax = _.max(alignToVals)
  247. _.forEach(['u', 'd'], function (vert) {
  248. _.forEach(['l', 'r'], function (horiz) {
  249. const alignment = vert + horiz
  250. const xs = xss[alignment]
  251. if (xs === alignTo) {
  252. return
  253. }
  254. const xsVals = _.values(xs)
  255. const delta = horiz === 'l' ? alignToMin - _.min(xsVals) : alignToMax - _.max(xsVals)
  256. if (delta) {
  257. xss[alignment] = _.mapValues(xs, function (x) { return x + delta })
  258. }
  259. })
  260. })
  261. }
  262. function balance (xss, align) {
  263. return _.mapValues(xss.ul, function (ignore, v) {
  264. if (align) {
  265. return xss[align.toLowerCase()][v]
  266. } else {
  267. const xs = _.sortBy(_.map(xss, v))
  268. return (xs[1] + xs[2]) / 2
  269. }
  270. })
  271. }
  272. export function positionX (g) {
  273. const layering = util.buildLayerMatrix(g)
  274. const conflicts = _.merge(findType1Conflicts(g, layering), findType2Conflicts(g, layering))
  275. const xss = {}
  276. let adjustedLayering
  277. _.forEach(['u', 'd'], function (vert) {
  278. adjustedLayering = vert === 'u' ? layering : _.values(layering).reverse()
  279. _.forEach(['l', 'r'], function (horiz) {
  280. if (horiz === 'r') {
  281. adjustedLayering = _.map(adjustedLayering, function (inner) {
  282. return _.values(inner).reverse()
  283. })
  284. }
  285. const neighborFn = _.bind(vert === 'u' ? g.predecessors : g.successors, g)
  286. const align = verticalAlignment(g, adjustedLayering, conflicts, neighborFn)
  287. let xs = horizontalCompaction(g, adjustedLayering,
  288. align.root, align.align,
  289. horiz === 'r')
  290. if (horiz === 'r') {
  291. xs = _.mapValues(xs, function (x) { return -x })
  292. }
  293. xss[vert + horiz] = xs
  294. })
  295. })
  296. const smallestWidth = findSmallestWidthAlignment(g, xss)
  297. alignCoordinates(xss, smallestWidth)
  298. return balance(xss, g.graph().align)
  299. }
  300. function sep (nodeSep, edgeSep, reverseSep) {
  301. return function (g, v, w) {
  302. const vLabel = g.node(v)
  303. const wLabel = g.node(w)
  304. let sum = 0
  305. let delta
  306. sum += vLabel.width / 2
  307. if (_.has(vLabel, 'labelpos')) {
  308. switch (vLabel.labelpos.toLowerCase()) {
  309. case 'l': delta = -vLabel.width / 2; break
  310. case 'r': delta = vLabel.width / 2; break
  311. }
  312. }
  313. if (delta) {
  314. sum += reverseSep ? delta : -delta
  315. }
  316. delta = 0
  317. sum += (vLabel.dummy ? edgeSep : nodeSep) / 2
  318. sum += (wLabel.dummy ? edgeSep : nodeSep) / 2
  319. sum += wLabel.width / 2
  320. if (_.has(wLabel, 'labelpos')) {
  321. switch (wLabel.labelpos.toLowerCase()) {
  322. case 'l': delta = wLabel.width / 2; break
  323. case 'r': delta = -wLabel.width / 2; break
  324. }
  325. }
  326. if (delta) {
  327. sum += reverseSep ? delta : -delta
  328. }
  329. delta = 0
  330. return sum
  331. }
  332. }
  333. function width (g, v) {
  334. return g.node(v).width
  335. }
  336. export default {
  337. positionX: positionX,
  338. findType1Conflicts: findType1Conflicts,
  339. findType2Conflicts: findType2Conflicts,
  340. addConflict: addConflict,
  341. hasConflict: hasConflict,
  342. verticalAlignment: verticalAlignment,
  343. horizontalCompaction: horizontalCompaction,
  344. alignCoordinates: alignCoordinates,
  345. findSmallestWidthAlignment: findSmallestWidthAlignment,
  346. balance: balance
  347. }