@antv/layout
Version:
graph layout algorithm
231 lines • 8.29 kB
JavaScript
import { dfs, minBy, simplify } from '../util';
import { feasibleTree } from './feasible-tree';
import { longestPath as initRank, slack } from './util';
/*
* The network simplex algorithm assigns ranks to each node in the input graph
* and iteratively improves the ranking to reduce the length of edges.
*
* Preconditions:
*
* 1. The input graph must be a DAG.
* 2. All nodes in the graph must have an object value.
* 3. All edges in the graph must have "minlen" and "weight" attributes.
*
* Postconditions:
*
* 1. All nodes in the graph will have an assigned "rank" attribute that has
* been optimized by the network simplex algorithm. Ranks start at 0.
*
*
* A rough sketch of the algorithm is as follows:
*
* 1. Assign initial ranks to each node. We use the longest path algorithm,
* which assigns ranks to the lowest position possible. In general this
* leads to very wide bottom ranks and unnecessarily long edges.
* 2. Construct a feasible tight tree. A tight tree is one such that all
* edges in the tree have no slack (difference between length of edge
* and minlen for the edge). This by itself greatly improves the assigned
* rankings by shorting edges.
* 3. Iteratively find edges that have negative cut values. Generally a
* negative cut value indicates that the edge could be removed and a new
* tree edge could be added to produce a more compact graph.
*
* Much of the algorithms here are derived from Gansner, et al., "A Technique
* for Drawing Directed Graphs." The structure of the file roughly follows the
* structure of the overall algorithm.
*/
export const networkSimplex = (og) => {
const g = simplify(og);
initRank(g);
const t = feasibleTree(g);
initLowLimValues(t);
initCutValues(t, g);
let e;
let f;
while ((e = leaveEdge(t))) {
f = enterEdge(t, g, e);
exchangeEdges(t, g, e, f);
}
};
/*
* Initializes cut values for all edges in the tree.
*/
export const initCutValues = (t, g) => {
let vs = dfs(t, t.getAllNodes(), 'post', false);
vs = vs.slice(0, (vs === null || vs === void 0 ? void 0 : vs.length) - 1);
vs.forEach((v) => {
assignCutValue(t, g, v);
});
};
const assignCutValue = (t, g, child) => {
const childLab = t.getNode(child);
const parent = childLab.data.parent;
// FIXME: use undirected edge?
const edge = t
.getRelatedEdges(child, 'both')
.find((e) => e.target === parent || e.source === parent);
edge.data.cutvalue = calcCutValue(t, g, child);
};
/*
* Given the tight tree, its graph, and a child in the graph calculate and
* return the cut value for the edge between the child and its parent.
*/
export const calcCutValue = (t, g, child) => {
const childLab = t.getNode(child);
const parent = childLab.data.parent;
// True if the child is on the tail end of the edge in the directed graph
let childIsTail = true;
// The graph's view of the tree edge we're inspecting
let graphEdge = g
.getRelatedEdges(child, 'out')
.find((e) => e.target === parent);
// The accumulated cut value for the edge between this node and its parent
let cutValue = 0;
if (!graphEdge) {
childIsTail = false;
graphEdge = g
.getRelatedEdges(parent, 'out')
.find((e) => e.target === child);
}
cutValue = graphEdge.data.weight;
g.getRelatedEdges(child, 'both').forEach((e) => {
const isOutEdge = e.source === child;
const other = isOutEdge ? e.target : e.source;
if (other !== parent) {
const pointsToHead = isOutEdge === childIsTail;
const otherWeight = e.data.weight;
cutValue += pointsToHead ? otherWeight : -otherWeight;
if (isTreeEdge(t, child, other)) {
// FIXME: use undirected edge?
const otherCutValue = t
.getRelatedEdges(child, 'both')
.find((e) => e.source === other || e.target === other).data
.cutvalue;
cutValue += pointsToHead ? -otherCutValue : otherCutValue;
}
}
});
return cutValue;
};
export const initLowLimValues = (tree, root = tree.getAllNodes()[0].id) => {
dfsAssignLowLim(tree, {}, 1, root);
};
const dfsAssignLowLim = (tree, visited, nextLim, v, parent) => {
var _a;
const low = nextLim;
let useNextLim = nextLim;
const label = tree.getNode(v);
visited[v] = true;
(_a = tree.getNeighbors(v)) === null || _a === void 0 ? void 0 : _a.forEach((w) => {
if (!visited[w.id]) {
useNextLim = dfsAssignLowLim(tree, visited, useNextLim, w.id, v);
}
});
label.data.low = low;
label.data.lim = useNextLim++;
if (parent) {
label.data.parent = parent;
}
else {
// TODO should be able to remove this when we incrementally update low lim
delete label.data.parent;
}
return useNextLim;
};
export const leaveEdge = (tree) => {
return tree.getAllEdges().find((e) => {
return e.data.cutvalue < 0;
});
};
export const enterEdge = (t, g, edge) => {
let v = edge.source;
let w = edge.target;
// For the rest of this function we assume that v is the tail and w is the
// head, so if we don't have this edge in the graph we should flip it to
// match the correct orientation.
if (!g.getRelatedEdges(v, 'out').find((e) => e.target === w)) {
v = edge.target;
w = edge.source;
}
const vLabel = t.getNode(v);
const wLabel = t.getNode(w);
let tailLabel = vLabel;
let flip = false;
// If the root is in the tail of the edge then we need to flip the logic that
// checks for the head and tail nodes in the candidates function below.
if (vLabel.data.lim > wLabel.data.lim) {
tailLabel = wLabel;
flip = true;
}
const candidates = g.getAllEdges().filter((edge) => {
return (flip === isDescendant(t.getNode(edge.source), tailLabel) &&
flip !== isDescendant(t.getNode(edge.target), tailLabel));
});
return minBy(candidates, (edge) => {
return slack(g, edge);
});
};
/**
*
* @param t
* @param g
* @param e edge to remove
* @param f edge to add
*/
export const exchangeEdges = (t, g, e, f) => {
// FIXME: use undirected edge?
const existed = t
.getRelatedEdges(e.source, 'both')
.find((edge) => edge.source === e.target || edge.target === e.target);
if (existed) {
t.removeEdge(existed.id);
}
t.addEdge({
id: `e${Math.random()}`,
source: f.source,
target: f.target,
data: {},
});
initLowLimValues(t);
initCutValues(t, g);
updateRanks(t, g);
};
const updateRanks = (t, g) => {
const root = t.getAllNodes().find((v) => {
return !v.data.parent;
});
let vs = dfs(t, root, 'pre', false);
vs = vs.slice(1);
vs.forEach((v) => {
const parent = t.getNode(v).data.parent;
let edge = g.getRelatedEdges(v, 'out').find((e) => e.target === parent);
// let edge = g.edgeFromArgs(v, parent);
let flipped = false;
if (!edge && g.hasNode(parent)) {
// edge = g.edgeFromArgs(parent, v)!;
edge = g.getRelatedEdges(parent, 'out').find((e) => e.target === v);
flipped = true;
}
g.getNode(v).data.rank =
((g.hasNode(parent) && g.getNode(parent).data.rank) || 0) +
(flipped ? edge === null || edge === void 0 ? void 0 : edge.data.minlen : -(edge === null || edge === void 0 ? void 0 : edge.data.minlen));
});
};
/*
* Returns true if the edge is in the tree.
*/
const isTreeEdge = (tree, u, v) => {
// FIXME: use undirected edge?
return tree
.getRelatedEdges(u, 'both')
.find((e) => e.source === v || e.target === v);
};
/*
* Returns true if the specified node is descendant of the root node per the
* assigned low and lim attributes in the tree.
*/
const isDescendant = (vLabel, rootLabel) => {
return (rootLabel.data.low <= vLabel.data.lim &&
vLabel.data.lim <= rootLabel.data.lim);
};
//# sourceMappingURL=network-simplex.js.map