@dagrejs/dagre
Version:
Graph layout for JavaScript
332 lines (292 loc) • 7.27 kB
JavaScript
/* eslint "no-console": off */
;
let Graph = require("@dagrejs/graphlib").Graph;
module.exports = {
addBorderNode,
addDummyNode,
applyWithChunking,
asNonCompoundGraph,
buildLayerMatrix,
intersectRect,
mapValues,
maxRank,
normalizeRanks,
notime,
partition,
pick,
predecessorWeights,
range,
removeEmptyRanks,
simplify,
successorWeights,
time,
uniqueId,
zipObject,
};
/*
* Adds a dummy node to the graph and return v.
*/
function addDummyNode(g, type, attrs, name) {
let v;
do {
v = uniqueId(name);
} while (g.hasNode(v));
attrs.dummy = type;
g.setNode(v, attrs);
return v;
}
/*
* Returns a new graph with only simple edges. Handles aggregation of data
* associated with multi-edges.
*/
function simplify(g) {
let simplified = new Graph().setGraph(g.graph());
g.nodes().forEach(v => simplified.setNode(v, g.node(v)));
g.edges().forEach(e => {
let simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 };
let label = g.edge(e);
simplified.setEdge(e.v, e.w, {
weight: simpleLabel.weight + label.weight,
minlen: Math.max(simpleLabel.minlen, label.minlen)
});
});
return simplified;
}
function asNonCompoundGraph(g) {
let simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph());
g.nodes().forEach(v => {
if (!g.children(v).length) {
simplified.setNode(v, g.node(v));
}
});
g.edges().forEach(e => {
simplified.setEdge(e, g.edge(e));
});
return simplified;
}
function successorWeights(g) {
let weightMap = g.nodes().map(v => {
let sucs = {};
g.outEdges(v).forEach(e => {
sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight;
});
return sucs;
});
return zipObject(g.nodes(), weightMap);
}
function predecessorWeights(g) {
let weightMap = g.nodes().map(v => {
let preds = {};
g.inEdges(v).forEach(e => {
preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight;
});
return preds;
});
return zipObject(g.nodes(), weightMap);
}
/*
* Finds where a line starting at point ({x, y}) would intersect a rectangle
* ({x, y, width, height}) if it were pointing at the rectangle's center.
*/
function intersectRect(rect, point) {
let x = rect.x;
let y = rect.y;
// Rectangle intersection algorithm from:
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
let dx = point.x - x;
let dy = point.y - y;
let w = rect.width / 2;
let h = rect.height / 2;
if (!dx && !dy) {
throw new Error("Not possible to find intersection inside of the rectangle");
}
let sx, sy;
if (Math.abs(dy) * w > Math.abs(dx) * h) {
// Intersection is top or bottom of rect.
if (dy < 0) {
h = -h;
}
sx = h * dx / dy;
sy = h;
} else {
// Intersection is left or right of rect.
if (dx < 0) {
w = -w;
}
sx = w;
sy = w * dy / dx;
}
return { x: x + sx, y: y + sy };
}
/*
* Given a DAG with each node assigned "rank" and "order" properties, this
* function will produce a matrix with the ids of each node.
*/
function buildLayerMatrix(g) {
let layering = range(maxRank(g) + 1).map(() => []);
g.nodes().forEach(v => {
let node = g.node(v);
let rank = node.rank;
if (rank !== undefined) {
layering[rank][node.order] = v;
}
});
return layering;
}
/*
* Adjusts the ranks for all nodes in the graph such that all nodes v have
* rank(v) >= 0 and at least one node w has rank(w) = 0.
*/
function normalizeRanks(g) {
let nodeRanks = g.nodes().map(v => {
let rank = g.node(v).rank;
if (rank === undefined) {
return Number.MAX_VALUE;
}
return rank;
});
let min = applyWithChunking(Math.min, nodeRanks);
g.nodes().forEach(v => {
let node = g.node(v);
if (Object.hasOwn(node, "rank")) {
node.rank -= min;
}
});
}
function removeEmptyRanks(g) {
// Ranks may not start at 0, so we need to offset them
let nodeRanks = g.nodes().map(v => g.node(v).rank);
let offset = applyWithChunking(Math.min, nodeRanks);
let layers = [];
g.nodes().forEach(v => {
let rank = g.node(v).rank - offset;
if (!layers[rank]) {
layers[rank] = [];
}
layers[rank].push(v);
});
let delta = 0;
let nodeRankFactor = g.graph().nodeRankFactor;
Array.from(layers).forEach((vs, i) => {
if (vs === undefined && i % nodeRankFactor !== 0) {
--delta;
} else if (vs !== undefined && delta) {
vs.forEach(v => g.node(v).rank += delta);
}
});
}
function addBorderNode(g, prefix, rank, order) {
let node = {
width: 0,
height: 0
};
if (arguments.length >= 4) {
node.rank = rank;
node.order = order;
}
return addDummyNode(g, "border", node, prefix);
}
function splitToChunks(array, chunkSize = CHUNKING_THRESHOLD) {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
const chunk = array.slice(i, i + chunkSize);
chunks.push(chunk);
}
return chunks;
}
const CHUNKING_THRESHOLD = 65535;
function applyWithChunking(fn, argsArray) {
if(argsArray.length > CHUNKING_THRESHOLD) {
const chunks = splitToChunks(argsArray);
return fn.apply(null, chunks.map(chunk => fn.apply(null, chunk)));
} else {
return fn.apply(null, argsArray);
}
}
function maxRank(g) {
const nodes = g.nodes();
const nodeRanks = nodes.map(v => {
let rank = g.node(v).rank;
if (rank === undefined) {
return Number.MIN_VALUE;
}
return rank;
});
return applyWithChunking(Math.max, nodeRanks);
}
/*
* Partition a collection into two groups: `lhs` and `rhs`. If the supplied
* function returns true for an entry it goes into `lhs`. Otherwise it goes
* into `rhs.
*/
function partition(collection, fn) {
let result = { lhs: [], rhs: [] };
collection.forEach(value => {
if (fn(value)) {
result.lhs.push(value);
} else {
result.rhs.push(value);
}
});
return result;
}
/*
* Returns a new function that wraps `fn` with a timer. The wrapper logs the
* time it takes to execute the function.
*/
function time(name, fn) {
let start = Date.now();
try {
return fn();
} finally {
console.log(name + " time: " + (Date.now() - start) + "ms");
}
}
function notime(name, fn) {
return fn();
}
let idCounter = 0;
function uniqueId(prefix) {
var id = ++idCounter;
return toString(prefix) + id;
}
function range(start, limit, step = 1) {
if (limit == null) {
limit = start;
start = 0;
}
let endCon = (i) => i < limit;
if (step < 0) {
endCon = (i) => limit < i;
}
const range = [];
for (let i = start; endCon(i); i += step) {
range.push(i);
}
return range;
}
function pick(source, keys) {
const dest = {};
for (const key of keys) {
if (source[key] !== undefined) {
dest[key] = source[key];
}
}
return dest;
}
function mapValues(obj, funcOrProp) {
let func = funcOrProp;
if (typeof funcOrProp === 'string') {
func = (val) => val[funcOrProp];
}
return Object.entries(obj).reduce((acc, [k, v]) => {
acc[k] = func(v, k);
return acc;
}, {});
}
function zipObject(props, values) {
return props.reduce((acc, key, i) => {
acc[key] = values[i];
return acc;
}, {});
}