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', () => { var layoutGraph = time(' buildLayoutGraph', () => buildLayoutGraph(g)); time(' runLayout', () => runLayout(layoutGraph, time)); time(' updateInputGraph', () => updateInputGraph(g, layoutGraph)); }); } function runLayout(g, time) { time(' makeSpaceForEdgeLabels', () => makeSpaceForEdgeLabels(g)); time(' removeSelfEdges', () => removeSelfEdges(g)); time(' acyclic', () => acyclic.run(g)); time(' nestingGraph.run', () => nestingGraph.run(g)); time(' rank', () => rank(util.asNonCompoundGraph(g))); time(' injectEdgeLabelProxies', () => injectEdgeLabelProxies(g)); time(' removeEmptyRanks', () => util.removeEmptyRanks(g)); time(' nestingGraph.cleanup', () => nestingGraph.cleanup(g)); time(' normalizeRanks', () => util.normalizeRanks(g)); time(' assignRankMinMax', () => assignRankMinMax(g)); time(' removeEdgeLabelProxies', () => removeEdgeLabelProxies(g)); time(' normalize.run', () => normalize.run(g)); time(' parentDummyChains', () => parentDummyChains(g)); time(' addBorderSegments', () => addBorderSegments(g)); time(' order', () => order(g)); time(' insertSelfEdges', () => insertSelfEdges(g)); time(' adjustCoordinateSystem', () => coordinateSystem.adjust(g)); time(' position', () => position(g)); time(' positionSelfEdges', () => positionSelfEdges(g)); time(' removeBorderNodes', () => removeBorderNodes(g)); time(' normalize.undo', () => normalize.undo(g)); time(' fixupEdgeLabelCoords', () => fixupEdgeLabelCoords(g)); time(' undoCoordinateSystem', () => coordinateSystem.undo(g)); time(' translateGraph', () => translateGraph(g)); time(' assignNodeIntersects', () => assignNodeIntersects(g)); time(' reversePoints', () => reversePointsForReversedEdges(g)); time(' acyclic.undo', () => 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 (Object.prototype.hasOwnProperty.call(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 (Object.prototype.hasOwnProperty.call(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 (Object.prototype.hasOwnProperty.call(edge, 'x')) { edge.x -= minX; } if (Object.prototype.hasOwnProperty.call(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 (Object.prototype.hasOwnProperty.call(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; }