UNPKG

@dagrejs/dagre

Version:

Graph layout for JavaScript

406 lines (365 loc) 12.2 kB
"use strict"; let acyclic = require("./acyclic"); let normalize = require("./normalize"); let rank = require("./rank"); let normalizeRanks = require("./util").normalizeRanks; let parentDummyChains = require("./parent-dummy-chains"); let removeEmptyRanks = require("./util").removeEmptyRanks; let nestingGraph = require("./nesting-graph"); let addBorderSegments = require("./add-border-segments"); let coordinateSystem = require("./coordinate-system"); let order = require("./order"); let position = require("./position"); let util = require("./util"); let Graph = require("@dagrejs/graphlib").Graph; module.exports = layout; function layout(g, opts) { let time = opts && opts.debugTiming ? util.time : util.notime; time("layout", () => { let layoutGraph = time(" buildLayoutGraph", () => buildLayoutGraph(g)); time(" runLayout", () => runLayout(layoutGraph, time, opts)); time(" updateInputGraph", () => updateInputGraph(g, layoutGraph)); }); } function runLayout(g, time, opts) { 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", () => removeEmptyRanks(g)); time(" nestingGraph.cleanup", () => nestingGraph.cleanup(g)); time(" normalizeRanks", () => 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, opts)); 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) { inputGraph.nodes().forEach(v => { let inputLabel = inputGraph.node(v); let layoutLabel = layoutGraph.node(v); if (inputLabel) { inputLabel.x = layoutLabel.x; inputLabel.y = layoutLabel.y; inputLabel.rank = layoutLabel.rank; if (layoutGraph.children(v).length) { inputLabel.width = layoutLabel.width; inputLabel.height = layoutLabel.height; } } }); inputGraph.edges().forEach(e => { let inputLabel = inputGraph.edge(e); let layoutLabel = layoutGraph.edge(e); inputLabel.points = layoutLabel.points; if (Object.hasOwn(layoutLabel, "x")) { inputLabel.x = layoutLabel.x; inputLabel.y = layoutLabel.y; } }); inputGraph.graph().width = layoutGraph.graph().width; inputGraph.graph().height = layoutGraph.graph().height; } let graphNumAttrs = ["nodesep", "edgesep", "ranksep", "marginx", "marginy"]; let graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: "tb" }; let graphAttrs = ["acyclicer", "ranker", "rankdir", "align"]; let nodeNumAttrs = ["width", "height"]; let nodeDefaults = { width: 0, height: 0 }; let edgeNumAttrs = ["minlen", "weight", "width", "height", "labeloffset"]; let edgeDefaults = { minlen: 1, weight: 1, width: 0, height: 0, labeloffset: 10, labelpos: "r" }; let 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) { let g = new Graph({ multigraph: true, compound: true }); let graph = canonicalize(inputGraph.graph()); g.setGraph(Object.assign({}, graphDefaults, selectNumberAttrs(graph, graphNumAttrs), util.pick(graph, graphAttrs))); inputGraph.nodes().forEach(v => { let node = canonicalize(inputGraph.node(v)); const newNode = selectNumberAttrs(node, nodeNumAttrs); Object.keys(nodeDefaults).forEach(k => { if (newNode[k] === undefined) { newNode[k] = nodeDefaults[k]; } }); g.setNode(v, newNode); g.setParent(v, inputGraph.parent(v)); }); inputGraph.edges().forEach(e => { let edge = canonicalize(inputGraph.edge(e)); g.setEdge(e, Object.assign({}, edgeDefaults, selectNumberAttrs(edge, edgeNumAttrs), util.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) { let graph = g.graph(); graph.ranksep /= 2; g.edges().forEach(e => { let 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) { g.edges().forEach(e => { let edge = g.edge(e); if (edge.width && edge.height) { let v = g.node(e.v); let w = g.node(e.w); let label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e }; util.addDummyNode(g, "edge-proxy", label, "_ep"); } }); } function assignRankMinMax(g) { let maxRank = 0; g.nodes().forEach(v => { let node = g.node(v); if (node.borderTop) { node.minRank = g.node(node.borderTop).rank; node.maxRank = g.node(node.borderBottom).rank; maxRank = Math.max(maxRank, node.maxRank); } }); g.graph().maxRank = maxRank; } function removeEdgeLabelProxies(g) { g.nodes().forEach(v => { let node = g.node(v); if (node.dummy === "edge-proxy") { g.edge(node.e).labelRank = node.rank; g.removeNode(v); } }); } function translateGraph(g) { let minX = Number.POSITIVE_INFINITY; let maxX = 0; let minY = Number.POSITIVE_INFINITY; let maxY = 0; let graphLabel = g.graph(); let marginX = graphLabel.marginx || 0; let marginY = graphLabel.marginy || 0; function getExtremes(attrs) { let x = attrs.x; let y = attrs.y; let w = attrs.width; let 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); } g.nodes().forEach(v => getExtremes(g.node(v))); g.edges().forEach(e => { let edge = g.edge(e); if (Object.hasOwn(edge, "x")) { getExtremes(edge); } }); minX -= marginX; minY -= marginY; g.nodes().forEach(v => { let node = g.node(v); node.x -= minX; node.y -= minY; }); g.edges().forEach(e => { let edge = g.edge(e); edge.points.forEach(p => { p.x -= minX; p.y -= minY; }); if (Object.hasOwn(edge, "x")) { edge.x -= minX; } if (Object.hasOwn(edge, "y")) { edge.y -= minY; } }); graphLabel.width = maxX - minX + marginX; graphLabel.height = maxY - minY + marginY; } function assignNodeIntersects(g) { g.edges().forEach(e => { let edge = g.edge(e); let nodeV = g.node(e.v); let nodeW = g.node(e.w); let 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) { g.edges().forEach(e => { let edge = g.edge(e); if (Object.hasOwn(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) { g.edges().forEach(e => { let edge = g.edge(e); if (edge.reversed) { edge.points.reverse(); } }); } function removeBorderNodes(g) { g.nodes().forEach(v => { if (g.children(v).length) { let node = g.node(v); let t = g.node(node.borderTop); let b = g.node(node.borderBottom); let l = g.node(node.borderLeft[node.borderLeft.length - 1]); let r = g.node(node.borderRight[node.borderRight.length - 1]); 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; } }); g.nodes().forEach(v => { if (g.node(v).dummy === "border") { g.removeNode(v); } }); } function removeSelfEdges(g) { g.edges().forEach(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); layers.forEach(layer => { var orderShift = 0; layer.forEach((v, i) => { var node = g.node(v); node.order = i + orderShift; (node.selfEdges || []).forEach(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) { g.nodes().forEach(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 util.mapValues(util.pick(obj, attrs), Number); } function canonicalize(attrs) { var newAttrs = {}; if (attrs) { Object.entries(attrs).forEach(([k, v]) => { if (typeof k === "string") { k = k.toLowerCase(); } newAttrs[k] = v; }); } return newAttrs; }