Spaces:
Build error
Build error
| import * as _ from 'lodash-es'; | |
| import { Graph } from '../../graphlib/index.js'; | |
| import * as util from '../util.js'; | |
| /* | |
| * This module provides coordinate assignment based on Brandes and Köpf, "Fast | |
| * and Simple Horizontal Coordinate Assignment." | |
| */ | |
| export { | |
| positionX, | |
| findType1Conflicts, | |
| findType2Conflicts, | |
| addConflict, | |
| hasConflict, | |
| verticalAlignment, | |
| horizontalCompaction, | |
| alignCoordinates, | |
| findSmallestWidthAlignment, | |
| balance, | |
| }; | |
| /* | |
| * Marks all edges in the graph with a type-1 conflict with the "type1Conflict" | |
| * property. A type-1 conflict is one where a non-inner segment crosses an | |
| * inner segment. An inner segment is an edge with both incident nodes marked | |
| * with the "dummy" property. | |
| * | |
| * This algorithm scans layer by layer, starting with the second, for type-1 | |
| * conflicts between the current layer and the previous layer. For each layer | |
| * it scans the nodes from left to right until it reaches one that is incident | |
| * on an inner segment. It then scans predecessors to determine if they have | |
| * edges that cross that inner segment. At the end a final scan is done for all | |
| * nodes on the current rank to see if they cross the last visited inner | |
| * segment. | |
| * | |
| * This algorithm (safely) assumes that a dummy node will only be incident on a | |
| * single node in the layers being scanned. | |
| */ | |
| function findType1Conflicts(g, layering) { | |
| var conflicts = {}; | |
| function visitLayer(prevLayer, layer) { | |
| var // last visited node in the previous layer that is incident on an inner | |
| // segment. | |
| k0 = 0, | |
| // Tracks the last node in this layer scanned for crossings with a type-1 | |
| // segment. | |
| scanPos = 0, | |
| prevLayerLength = prevLayer.length, | |
| lastNode = _.last(layer); | |
| _.forEach(layer, function (v, i) { | |
| var w = findOtherInnerSegmentNode(g, v), | |
| k1 = w ? g.node(w).order : prevLayerLength; | |
| if (w || v === lastNode) { | |
| _.forEach(layer.slice(scanPos, i + 1), function (scanNode) { | |
| _.forEach(g.predecessors(scanNode), function (u) { | |
| var uLabel = g.node(u), | |
| uPos = uLabel.order; | |
| if ((uPos < k0 || k1 < uPos) && !(uLabel.dummy && g.node(scanNode).dummy)) { | |
| addConflict(conflicts, u, scanNode); | |
| } | |
| }); | |
| }); | |
| // @ts-expect-error | |
| scanPos = i + 1; | |
| k0 = k1; | |
| } | |
| }); | |
| return layer; | |
| } | |
| _.reduce(layering, visitLayer); | |
| return conflicts; | |
| } | |
| function findType2Conflicts(g, layering) { | |
| var conflicts = {}; | |
| function scan(south, southPos, southEnd, prevNorthBorder, nextNorthBorder) { | |
| var v; | |
| _.forEach(_.range(southPos, southEnd), function (i) { | |
| v = south[i]; | |
| if (g.node(v).dummy) { | |
| _.forEach(g.predecessors(v), function (u) { | |
| var uNode = g.node(u); | |
| if (uNode.dummy && (uNode.order < prevNorthBorder || uNode.order > nextNorthBorder)) { | |
| addConflict(conflicts, u, v); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| function visitLayer(north, south) { | |
| var prevNorthPos = -1, | |
| nextNorthPos, | |
| southPos = 0; | |
| _.forEach(south, function (v, southLookahead) { | |
| if (g.node(v).dummy === 'border') { | |
| var predecessors = g.predecessors(v); | |
| if (predecessors.length) { | |
| nextNorthPos = g.node(predecessors[0]).order; | |
| scan(south, southPos, southLookahead, prevNorthPos, nextNorthPos); | |
| // @ts-expect-error | |
| southPos = southLookahead; | |
| prevNorthPos = nextNorthPos; | |
| } | |
| } | |
| scan(south, southPos, south.length, nextNorthPos, north.length); | |
| }); | |
| return south; | |
| } | |
| _.reduce(layering, visitLayer); | |
| return conflicts; | |
| } | |
| function findOtherInnerSegmentNode(g, v) { | |
| if (g.node(v).dummy) { | |
| return _.find(g.predecessors(v), function (u) { | |
| return g.node(u).dummy; | |
| }); | |
| } | |
| } | |
| function addConflict(conflicts, v, w) { | |
| if (v > w) { | |
| var tmp = v; | |
| v = w; | |
| w = tmp; | |
| } | |
| var conflictsV = conflicts[v]; | |
| if (!conflictsV) { | |
| conflicts[v] = conflictsV = {}; | |
| } | |
| conflictsV[w] = true; | |
| } | |
| function hasConflict(conflicts, v, w) { | |
| if (v > w) { | |
| var tmp = v; | |
| v = w; | |
| w = tmp; | |
| } | |
| return _.has(conflicts[v], w); | |
| } | |
| /* | |
| * Try to align nodes into vertical "blocks" where possible. This algorithm | |
| * attempts to align a node with one of its median neighbors. If the edge | |
| * connecting a neighbor is a type-1 conflict then we ignore that possibility. | |
| * If a previous node has already formed a block with a node after the node | |
| * we're trying to form a block with, we also ignore that possibility - our | |
| * blocks would be split in that scenario. | |
| */ | |
| function verticalAlignment(g, layering, conflicts, neighborFn) { | |
| var root = {}, | |
| align = {}, | |
| pos = {}; | |
| // We cache the position here based on the layering because the graph and | |
| // layering may be out of sync. The layering matrix is manipulated to | |
| // generate different extreme alignments. | |
| _.forEach(layering, function (layer) { | |
| _.forEach(layer, function (v, order) { | |
| root[v] = v; | |
| align[v] = v; | |
| pos[v] = order; | |
| }); | |
| }); | |
| _.forEach(layering, function (layer) { | |
| var prevIdx = -1; | |
| _.forEach(layer, function (v) { | |
| var ws = neighborFn(v); | |
| if (ws.length) { | |
| ws = _.sortBy(ws, function (w) { | |
| return pos[w]; | |
| }); | |
| var mp = (ws.length - 1) / 2; | |
| for (var i = Math.floor(mp), il = Math.ceil(mp); i <= il; ++i) { | |
| var w = ws[i]; | |
| if (align[v] === v && prevIdx < pos[w] && !hasConflict(conflicts, v, w)) { | |
| align[w] = v; | |
| align[v] = root[v] = root[w]; | |
| prevIdx = pos[w]; | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| return { root: root, align: align }; | |
| } | |
| function horizontalCompaction(g, layering, root, align, reverseSep) { | |
| // This portion of the algorithm differs from BK due to a number of problems. | |
| // Instead of their algorithm we construct a new block graph and do two | |
| // sweeps. The first sweep places blocks with the smallest possible | |
| // coordinates. The second sweep removes unused space by moving blocks to the | |
| // greatest coordinates without violating separation. | |
| var xs = {}, | |
| blockG = buildBlockGraph(g, layering, root, reverseSep), | |
| borderType = reverseSep ? 'borderLeft' : 'borderRight'; | |
| function iterate(setXsFunc, nextNodesFunc) { | |
| var stack = blockG.nodes(); | |
| var elem = stack.pop(); | |
| var visited = {}; | |
| while (elem) { | |
| if (visited[elem]) { | |
| setXsFunc(elem); | |
| } else { | |
| visited[elem] = true; | |
| stack.push(elem); | |
| stack = stack.concat(nextNodesFunc(elem)); | |
| } | |
| elem = stack.pop(); | |
| } | |
| } | |
| // First pass, assign smallest coordinates | |
| function pass1(elem) { | |
| xs[elem] = blockG.inEdges(elem).reduce(function (acc, e) { | |
| return Math.max(acc, xs[e.v] + blockG.edge(e)); | |
| }, 0); | |
| } | |
| // Second pass, assign greatest coordinates | |
| function pass2(elem) { | |
| var min = blockG.outEdges(elem).reduce(function (acc, e) { | |
| return Math.min(acc, xs[e.w] - blockG.edge(e)); | |
| }, Number.POSITIVE_INFINITY); | |
| var node = g.node(elem); | |
| if (min !== Number.POSITIVE_INFINITY && node.borderType !== borderType) { | |
| xs[elem] = Math.max(xs[elem], min); | |
| } | |
| } | |
| iterate(pass1, blockG.predecessors.bind(blockG)); | |
| iterate(pass2, blockG.successors.bind(blockG)); | |
| // Assign x coordinates to all nodes | |
| _.forEach(align, function (v) { | |
| xs[v] = xs[root[v]]; | |
| }); | |
| return xs; | |
| } | |
| function buildBlockGraph(g, layering, root, reverseSep) { | |
| var blockGraph = new Graph(), | |
| graphLabel = g.graph(), | |
| sepFn = sep(graphLabel.nodesep, graphLabel.edgesep, reverseSep); | |
| _.forEach(layering, function (layer) { | |
| var u; | |
| _.forEach(layer, function (v) { | |
| var vRoot = root[v]; | |
| blockGraph.setNode(vRoot); | |
| if (u) { | |
| var uRoot = root[u], | |
| prevMax = blockGraph.edge(uRoot, vRoot); | |
| blockGraph.setEdge(uRoot, vRoot, Math.max(sepFn(g, v, u), prevMax || 0)); | |
| } | |
| u = v; | |
| }); | |
| }); | |
| return blockGraph; | |
| } | |
| /* | |
| * Returns the alignment that has the smallest width of the given alignments. | |
| */ | |
| function findSmallestWidthAlignment(g, xss) { | |
| return _.minBy(_.values(xss), function (xs) { | |
| var max = Number.NEGATIVE_INFINITY; | |
| var min = Number.POSITIVE_INFINITY; | |
| _.forIn(xs, function (x, v) { | |
| var halfWidth = width(g, v) / 2; | |
| max = Math.max(x + halfWidth, max); | |
| min = Math.min(x - halfWidth, min); | |
| }); | |
| return max - min; | |
| }); | |
| } | |
| /* | |
| * Align the coordinates of each of the layout alignments such that | |
| * left-biased alignments have their minimum coordinate at the same point as | |
| * the minimum coordinate of the smallest width alignment and right-biased | |
| * alignments have their maximum coordinate at the same point as the maximum | |
| * coordinate of the smallest width alignment. | |
| */ | |
| function alignCoordinates(xss, alignTo) { | |
| var alignToVals = _.values(alignTo), | |
| alignToMin = _.min(alignToVals), | |
| alignToMax = _.max(alignToVals); | |
| _.forEach(['u', 'd'], function (vert) { | |
| _.forEach(['l', 'r'], function (horiz) { | |
| var alignment = vert + horiz, | |
| xs = xss[alignment], | |
| delta; | |
| if (xs === alignTo) return; | |
| var xsVals = _.values(xs); | |
| delta = horiz === 'l' ? alignToMin - _.min(xsVals) : alignToMax - _.max(xsVals); | |
| if (delta) { | |
| xss[alignment] = _.mapValues(xs, function (x) { | |
| return x + delta; | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| function balance(xss, align) { | |
| return _.mapValues(xss.ul, function (ignore, v) { | |
| if (align) { | |
| return xss[align.toLowerCase()][v]; | |
| } else { | |
| var xs = _.sortBy(_.map(xss, v)); | |
| return (xs[1] + xs[2]) / 2; | |
| } | |
| }); | |
| } | |
| function positionX(g) { | |
| var layering = util.buildLayerMatrix(g); | |
| var conflicts = _.merge(findType1Conflicts(g, layering), findType2Conflicts(g, layering)); | |
| var xss = {}; | |
| var adjustedLayering; | |
| _.forEach(['u', 'd'], function (vert) { | |
| adjustedLayering = vert === 'u' ? layering : _.values(layering).reverse(); | |
| _.forEach(['l', 'r'], function (horiz) { | |
| if (horiz === 'r') { | |
| adjustedLayering = _.map(adjustedLayering, function (inner) { | |
| return _.values(inner).reverse(); | |
| }); | |
| } | |
| var neighborFn = (vert === 'u' ? g.predecessors : g.successors).bind(g); | |
| var align = verticalAlignment(g, adjustedLayering, conflicts, neighborFn); | |
| var xs = horizontalCompaction(g, adjustedLayering, align.root, align.align, horiz === 'r'); | |
| if (horiz === 'r') { | |
| xs = _.mapValues(xs, function (x) { | |
| return -x; | |
| }); | |
| } | |
| xss[vert + horiz] = xs; | |
| }); | |
| }); | |
| var smallestWidth = findSmallestWidthAlignment(g, xss); | |
| alignCoordinates(xss, smallestWidth); | |
| return balance(xss, g.graph().align); | |
| } | |
| function sep(nodeSep, edgeSep, reverseSep) { | |
| return function (g, v, w) { | |
| var vLabel = g.node(v); | |
| var wLabel = g.node(w); | |
| var sum = 0; | |
| var delta; | |
| sum += vLabel.width / 2; | |
| if (_.has(vLabel, 'labelpos')) { | |
| switch (vLabel.labelpos.toLowerCase()) { | |
| case 'l': | |
| delta = -vLabel.width / 2; | |
| break; | |
| case 'r': | |
| delta = vLabel.width / 2; | |
| break; | |
| } | |
| } | |
| if (delta) { | |
| sum += reverseSep ? delta : -delta; | |
| } | |
| delta = 0; | |
| sum += (vLabel.dummy ? edgeSep : nodeSep) / 2; | |
| sum += (wLabel.dummy ? edgeSep : nodeSep) / 2; | |
| sum += wLabel.width / 2; | |
| if (_.has(wLabel, 'labelpos')) { | |
| switch (wLabel.labelpos.toLowerCase()) { | |
| case 'l': | |
| delta = wLabel.width / 2; | |
| break; | |
| case 'r': | |
| delta = -wLabel.width / 2; | |
| break; | |
| } | |
| } | |
| if (delta) { | |
| sum += reverseSep ? delta : -delta; | |
| } | |
| delta = 0; | |
| return sum; | |
| }; | |
| } | |
| function width(g, v) { | |
| return g.node(v).width; | |
| } | |