Spaces:
Build error
Build error
| import * as _ from 'lodash-es'; | |
| import { Graph } from '../graphlib/index.js'; | |
| import { addBorderSegments } from './add-border-segments.js'; | |
| import * as coordinateSystem from './coordinate-system.js'; | |
| import * as acyclic from './acyclic.js'; | |
| import * as normalize from './normalize.js'; | |
| import { rank } from './rank/index.js'; | |
| import * as nestingGraph from './nesting-graph.js'; | |
| import { order } from './order/index.js'; | |
| import { parentDummyChains } from './parent-dummy-chains.js'; | |
| import { position } from './position/index.js'; | |
| import * as util from './util.js'; | |
| export { layout }; | |
| function layout(g, opts) { | |
| var time = opts && opts.debugTiming ? util.time : util.notime; | |
| time('layout', function () { | |
| var layoutGraph = time(' buildLayoutGraph', function () { | |
| return buildLayoutGraph(g); | |
| }); | |
| time(' runLayout', function () { | |
| runLayout(layoutGraph, time); | |
| }); | |
| time(' updateInputGraph', function () { | |
| updateInputGraph(g, layoutGraph); | |
| }); | |
| }); | |
| } | |
| function runLayout(g, time) { | |
| time(' makeSpaceForEdgeLabels', function () { | |
| makeSpaceForEdgeLabels(g); | |
| }); | |
| time(' removeSelfEdges', function () { | |
| removeSelfEdges(g); | |
| }); | |
| time(' acyclic', function () { | |
| acyclic.run(g); | |
| }); | |
| time(' nestingGraph.run', function () { | |
| nestingGraph.run(g); | |
| }); | |
| time(' rank', function () { | |
| rank(util.asNonCompoundGraph(g)); | |
| }); | |
| time(' injectEdgeLabelProxies', function () { | |
| injectEdgeLabelProxies(g); | |
| }); | |
| time(' removeEmptyRanks', function () { | |
| util.removeEmptyRanks(g); | |
| }); | |
| time(' nestingGraph.cleanup', function () { | |
| nestingGraph.cleanup(g); | |
| }); | |
| time(' normalizeRanks', function () { | |
| util.normalizeRanks(g); | |
| }); | |
| time(' assignRankMinMax', function () { | |
| assignRankMinMax(g); | |
| }); | |
| time(' removeEdgeLabelProxies', function () { | |
| removeEdgeLabelProxies(g); | |
| }); | |
| time(' normalize.run', function () { | |
| normalize.run(g); | |
| }); | |
| time(' parentDummyChains', function () { | |
| parentDummyChains(g); | |
| }); | |
| time(' addBorderSegments', function () { | |
| addBorderSegments(g); | |
| }); | |
| time(' order', function () { | |
| order(g); | |
| }); | |
| time(' insertSelfEdges', function () { | |
| insertSelfEdges(g); | |
| }); | |
| time(' adjustCoordinateSystem', function () { | |
| coordinateSystem.adjust(g); | |
| }); | |
| time(' position', function () { | |
| position(g); | |
| }); | |
| time(' positionSelfEdges', function () { | |
| positionSelfEdges(g); | |
| }); | |
| time(' removeBorderNodes', function () { | |
| removeBorderNodes(g); | |
| }); | |
| time(' normalize.undo', function () { | |
| normalize.undo(g); | |
| }); | |
| time(' fixupEdgeLabelCoords', function () { | |
| fixupEdgeLabelCoords(g); | |
| }); | |
| time(' undoCoordinateSystem', function () { | |
| coordinateSystem.undo(g); | |
| }); | |
| time(' translateGraph', function () { | |
| translateGraph(g); | |
| }); | |
| time(' assignNodeIntersects', function () { | |
| assignNodeIntersects(g); | |
| }); | |
| time(' reversePoints', function () { | |
| reversePointsForReversedEdges(g); | |
| }); | |
| time(' acyclic.undo', function () { | |
| acyclic.undo(g); | |
| }); | |
| } | |
| /* | |
| * Copies final layout information from the layout graph back to the input | |
| * graph. This process only copies whitelisted attributes from the layout graph | |
| * to the input graph, so it serves as a good place to determine what | |
| * attributes can influence layout. | |
| */ | |
| function updateInputGraph(inputGraph, layoutGraph) { | |
| _.forEach(inputGraph.nodes(), function (v) { | |
| var inputLabel = inputGraph.node(v); | |
| var layoutLabel = layoutGraph.node(v); | |
| if (inputLabel) { | |
| inputLabel.x = layoutLabel.x; | |
| inputLabel.y = layoutLabel.y; | |
| if (layoutGraph.children(v).length) { | |
| inputLabel.width = layoutLabel.width; | |
| inputLabel.height = layoutLabel.height; | |
| } | |
| } | |
| }); | |
| _.forEach(inputGraph.edges(), function (e) { | |
| var inputLabel = inputGraph.edge(e); | |
| var layoutLabel = layoutGraph.edge(e); | |
| inputLabel.points = layoutLabel.points; | |
| if (_.has(layoutLabel, 'x')) { | |
| inputLabel.x = layoutLabel.x; | |
| inputLabel.y = layoutLabel.y; | |
| } | |
| }); | |
| inputGraph.graph().width = layoutGraph.graph().width; | |
| inputGraph.graph().height = layoutGraph.graph().height; | |
| } | |
| var graphNumAttrs = ['nodesep', 'edgesep', 'ranksep', 'marginx', 'marginy']; | |
| var graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: 'tb' }; | |
| var graphAttrs = ['acyclicer', 'ranker', 'rankdir', 'align']; | |
| var nodeNumAttrs = ['width', 'height']; | |
| var nodeDefaults = { width: 0, height: 0 }; | |
| var edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset']; | |
| var edgeDefaults = { | |
| minlen: 1, | |
| weight: 1, | |
| width: 0, | |
| height: 0, | |
| labeloffset: 10, | |
| labelpos: 'r', | |
| }; | |
| var edgeAttrs = ['labelpos']; | |
| /* | |
| * Constructs a new graph from the input graph, which can be used for layout. | |
| * This process copies only whitelisted attributes from the input graph to the | |
| * layout graph. Thus this function serves as a good place to determine what | |
| * attributes can influence layout. | |
| */ | |
| function buildLayoutGraph(inputGraph) { | |
| var g = new Graph({ multigraph: true, compound: true }); | |
| var graph = canonicalize(inputGraph.graph()); | |
| g.setGraph( | |
| _.merge({}, graphDefaults, selectNumberAttrs(graph, graphNumAttrs), _.pick(graph, graphAttrs)) | |
| ); | |
| _.forEach(inputGraph.nodes(), function (v) { | |
| var node = canonicalize(inputGraph.node(v)); | |
| g.setNode(v, _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults)); | |
| g.setParent(v, inputGraph.parent(v)); | |
| }); | |
| _.forEach(inputGraph.edges(), function (e) { | |
| var edge = canonicalize(inputGraph.edge(e)); | |
| g.setEdge( | |
| e, | |
| _.merge({}, edgeDefaults, selectNumberAttrs(edge, edgeNumAttrs), _.pick(edge, edgeAttrs)) | |
| ); | |
| }); | |
| return g; | |
| } | |
| /* | |
| * This idea comes from the Gansner paper: to account for edge labels in our | |
| * layout we split each rank in half by doubling minlen and halving ranksep. | |
| * Then we can place labels at these mid-points between nodes. | |
| * | |
| * We also add some minimal padding to the width to push the label for the edge | |
| * away from the edge itself a bit. | |
| */ | |
| function makeSpaceForEdgeLabels(g) { | |
| var graph = g.graph(); | |
| graph.ranksep /= 2; | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| edge.minlen *= 2; | |
| if (edge.labelpos.toLowerCase() !== 'c') { | |
| if (graph.rankdir === 'TB' || graph.rankdir === 'BT') { | |
| edge.width += edge.labeloffset; | |
| } else { | |
| edge.height += edge.labeloffset; | |
| } | |
| } | |
| }); | |
| } | |
| /* | |
| * Creates temporary dummy nodes that capture the rank in which each edge's | |
| * label is going to, if it has one of non-zero width and height. We do this | |
| * so that we can safely remove empty ranks while preserving balance for the | |
| * label's position. | |
| */ | |
| function injectEdgeLabelProxies(g) { | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| if (edge.width && edge.height) { | |
| var v = g.node(e.v); | |
| var w = g.node(e.w); | |
| var label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e }; | |
| util.addDummyNode(g, 'edge-proxy', label, '_ep'); | |
| } | |
| }); | |
| } | |
| function assignRankMinMax(g) { | |
| var maxRank = 0; | |
| _.forEach(g.nodes(), function (v) { | |
| var node = g.node(v); | |
| if (node.borderTop) { | |
| node.minRank = g.node(node.borderTop).rank; | |
| node.maxRank = g.node(node.borderBottom).rank; | |
| // @ts-expect-error | |
| maxRank = _.max(maxRank, node.maxRank); | |
| } | |
| }); | |
| g.graph().maxRank = maxRank; | |
| } | |
| function removeEdgeLabelProxies(g) { | |
| _.forEach(g.nodes(), function (v) { | |
| var node = g.node(v); | |
| if (node.dummy === 'edge-proxy') { | |
| g.edge(node.e).labelRank = node.rank; | |
| g.removeNode(v); | |
| } | |
| }); | |
| } | |
| function translateGraph(g) { | |
| var minX = Number.POSITIVE_INFINITY; | |
| var maxX = 0; | |
| var minY = Number.POSITIVE_INFINITY; | |
| var maxY = 0; | |
| var graphLabel = g.graph(); | |
| var marginX = graphLabel.marginx || 0; | |
| var marginY = graphLabel.marginy || 0; | |
| function getExtremes(attrs) { | |
| var x = attrs.x; | |
| var y = attrs.y; | |
| var w = attrs.width; | |
| var h = attrs.height; | |
| minX = Math.min(minX, x - w / 2); | |
| maxX = Math.max(maxX, x + w / 2); | |
| minY = Math.min(minY, y - h / 2); | |
| maxY = Math.max(maxY, y + h / 2); | |
| } | |
| _.forEach(g.nodes(), function (v) { | |
| getExtremes(g.node(v)); | |
| }); | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| if (_.has(edge, 'x')) { | |
| getExtremes(edge); | |
| } | |
| }); | |
| minX -= marginX; | |
| minY -= marginY; | |
| _.forEach(g.nodes(), function (v) { | |
| var node = g.node(v); | |
| node.x -= minX; | |
| node.y -= minY; | |
| }); | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| _.forEach(edge.points, function (p) { | |
| p.x -= minX; | |
| p.y -= minY; | |
| }); | |
| if (_.has(edge, 'x')) { | |
| edge.x -= minX; | |
| } | |
| if (_.has(edge, 'y')) { | |
| edge.y -= minY; | |
| } | |
| }); | |
| graphLabel.width = maxX - minX + marginX; | |
| graphLabel.height = maxY - minY + marginY; | |
| } | |
| function assignNodeIntersects(g) { | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| var nodeV = g.node(e.v); | |
| var nodeW = g.node(e.w); | |
| var p1, p2; | |
| if (!edge.points) { | |
| edge.points = []; | |
| p1 = nodeW; | |
| p2 = nodeV; | |
| } else { | |
| p1 = edge.points[0]; | |
| p2 = edge.points[edge.points.length - 1]; | |
| } | |
| edge.points.unshift(util.intersectRect(nodeV, p1)); | |
| edge.points.push(util.intersectRect(nodeW, p2)); | |
| }); | |
| } | |
| function fixupEdgeLabelCoords(g) { | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| if (_.has(edge, 'x')) { | |
| if (edge.labelpos === 'l' || edge.labelpos === 'r') { | |
| edge.width -= edge.labeloffset; | |
| } | |
| switch (edge.labelpos) { | |
| case 'l': | |
| edge.x -= edge.width / 2 + edge.labeloffset; | |
| break; | |
| case 'r': | |
| edge.x += edge.width / 2 + edge.labeloffset; | |
| break; | |
| } | |
| } | |
| }); | |
| } | |
| function reversePointsForReversedEdges(g) { | |
| _.forEach(g.edges(), function (e) { | |
| var edge = g.edge(e); | |
| if (edge.reversed) { | |
| edge.points.reverse(); | |
| } | |
| }); | |
| } | |
| function removeBorderNodes(g) { | |
| _.forEach(g.nodes(), function (v) { | |
| if (g.children(v).length) { | |
| var node = g.node(v); | |
| var t = g.node(node.borderTop); | |
| var b = g.node(node.borderBottom); | |
| var l = g.node(_.last(node.borderLeft)); | |
| var r = g.node(_.last(node.borderRight)); | |
| node.width = Math.abs(r.x - l.x); | |
| node.height = Math.abs(b.y - t.y); | |
| node.x = l.x + node.width / 2; | |
| node.y = t.y + node.height / 2; | |
| } | |
| }); | |
| _.forEach(g.nodes(), function (v) { | |
| if (g.node(v).dummy === 'border') { | |
| g.removeNode(v); | |
| } | |
| }); | |
| } | |
| function removeSelfEdges(g) { | |
| _.forEach(g.edges(), function (e) { | |
| if (e.v === e.w) { | |
| var node = g.node(e.v); | |
| if (!node.selfEdges) { | |
| node.selfEdges = []; | |
| } | |
| node.selfEdges.push({ e: e, label: g.edge(e) }); | |
| g.removeEdge(e); | |
| } | |
| }); | |
| } | |
| function insertSelfEdges(g) { | |
| var layers = util.buildLayerMatrix(g); | |
| _.forEach(layers, function (layer) { | |
| var orderShift = 0; | |
| _.forEach(layer, function (v, i) { | |
| var node = g.node(v); | |
| node.order = i + orderShift; | |
| _.forEach(node.selfEdges, function (selfEdge) { | |
| util.addDummyNode( | |
| g, | |
| 'selfedge', | |
| { | |
| width: selfEdge.label.width, | |
| height: selfEdge.label.height, | |
| rank: node.rank, | |
| order: i + ++orderShift, | |
| e: selfEdge.e, | |
| label: selfEdge.label, | |
| }, | |
| '_se' | |
| ); | |
| }); | |
| delete node.selfEdges; | |
| }); | |
| }); | |
| } | |
| function positionSelfEdges(g) { | |
| _.forEach(g.nodes(), function (v) { | |
| var node = g.node(v); | |
| if (node.dummy === 'selfedge') { | |
| var selfNode = g.node(node.e.v); | |
| var x = selfNode.x + selfNode.width / 2; | |
| var y = selfNode.y; | |
| var dx = node.x - x; | |
| var dy = selfNode.height / 2; | |
| g.setEdge(node.e, node.label); | |
| g.removeNode(v); | |
| node.label.points = [ | |
| { x: x + (2 * dx) / 3, y: y - dy }, | |
| { x: x + (5 * dx) / 6, y: y - dy }, | |
| { x: x + dx, y: y }, | |
| { x: x + (5 * dx) / 6, y: y + dy }, | |
| { x: x + (2 * dx) / 3, y: y + dy }, | |
| ]; | |
| node.label.x = node.x; | |
| node.label.y = node.y; | |
| } | |
| }); | |
| } | |
| function selectNumberAttrs(obj, attrs) { | |
| return _.mapValues(_.pick(obj, attrs), Number); | |
| } | |
| function canonicalize(attrs) { | |
| var newAttrs = {}; | |
| _.forEach(attrs, function (v, k) { | |
| newAttrs[k.toLowerCase()] = v; | |
| }); | |
| return newAttrs; | |
| } | |