| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- import _ from 'lodash'
- import { Graph } from 'graphlibrary'
- import acyclic from './acyclic'
- import normalize from './normalize'
- import rank from './rank'
- import util, { normalizeRanks, removeEmptyRanks } from './util'
- import parentDummyChains from './parent-dummy-chains'
- import nestingGraph from './nesting-graph'
- import addBorderSegments from './add-border-segments'
- import coordinateSystem from './coordinate-system'
- import order from './order'
- import position from './position'
- function layout (g, opts) {
- const time = opts && opts.debugTiming ? util.time : util.notime
- time('layout', function () {
- const 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 () { removeEmptyRanks(g) })
- time(' nestingGraph.cleanup', function () { nestingGraph.cleanup(g) })
- time(' normalizeRanks', function () { 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) {
- const inputLabel = inputGraph.node(v)
- const 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) {
- const inputLabel = inputGraph.edge(e)
- const 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
- }
- const graphNumAttrs = ['nodesep', 'edgesep', 'ranksep', 'marginx', 'marginy']
- const graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: 'tb' }
- const graphAttrs = ['acyclicer', 'ranker', 'rankdir', 'align']
- const nodeNumAttrs = ['width', 'height']
- const nodeDefaults = { width: 0, height: 0 }
- const edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset']
- const edgeDefaults = {
- minlen: 1,
- weight: 1,
- width: 0,
- height: 0,
- labeloffset: 10,
- labelpos: 'r'
- }
- const 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) {
- const g = new Graph({ multigraph: true, compound: true })
- const graph = canonicalize(inputGraph.graph())
- g.setGraph(_.merge({},
- graphDefaults,
- selectNumberAttrs(graph, graphNumAttrs),
- _.pick(graph, graphAttrs)))
- _.forEach(inputGraph.nodes(), function (v) {
- const node = canonicalize(inputGraph.node(v))
- g.setNode(v, _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults))
- g.setParent(v, inputGraph.parent(v))
- })
- _.forEach(inputGraph.edges(), function (e) {
- const 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) {
- const graph = g.graph()
- graph.ranksep /= 2
- _.forEach(g.edges(), function (e) {
- const 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) {
- const edge = g.edge(e)
- if (edge.width && edge.height) {
- const v = g.node(e.v)
- const w = g.node(e.w)
- const label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e }
- util.addDummyNode(g, 'edge-proxy', label, '_ep')
- }
- })
- }
- function assignRankMinMax (g) {
- let maxRank = 0
- _.forEach(g.nodes(), function (v) {
- const 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) {
- _.forEach(g.nodes(), function (v) {
- const 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
- const graphLabel = g.graph()
- const marginX = graphLabel.marginx || 0
- const marginY = graphLabel.marginy || 0
- function getExtremes (attrs) {
- const x = attrs.x
- const y = attrs.y
- const w = attrs.width
- const 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) {
- const edge = g.edge(e)
- if (_.has(edge, 'x')) {
- getExtremes(edge)
- }
- })
- minX -= marginX
- minY -= marginY
- _.forEach(g.nodes(), function (v) {
- const node = g.node(v)
- node.x -= minX
- node.y -= minY
- })
- _.forEach(g.edges(), function (e) {
- const 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) {
- const edge = g.edge(e)
- const nodeV = g.node(e.v)
- const nodeW = g.node(e.w)
- let p1 = null
- let p2 = null
- 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) {
- const 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) {
- const edge = g.edge(e)
- if (edge.reversed) {
- edge.points.reverse()
- }
- })
- }
- function removeBorderNodes (g) {
- _.forEach(g.nodes(), function (v) {
- if (g.children(v).length) {
- const node = g.node(v)
- const t = g.node(node.borderTop)
- const b = g.node(node.borderBottom)
- const l = g.node(_.last(node.borderLeft))
- const 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) {
- const 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) {
- const layers = util.buildLayerMatrix(g)
- _.forEach(layers, function (layer) {
- let orderShift = 0
- _.forEach(layer, function (v, i) {
- const 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) {
- const node = g.node(v)
- if (node.dummy === 'selfedge') {
- const selfNode = g.node(node.e.v)
- const x = selfNode.x + selfNode.width / 2
- const y = selfNode.y
- const dx = node.x - x
- const 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) {
- const newAttrs = {}
- _.forEach(attrs, function (v, k) {
- newAttrs[k.toLowerCase()] = v
- })
- return newAttrs
- }
- export default layout
|