395 lines
12 KiB
JavaScript
395 lines
12 KiB
JavaScript
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
|