d3-dag
Version:
Layout algorithms for visualizing directed acylic graphs.
1,698 lines (1,501 loc) • 208 kB
JavaScript
// d3-dag Version 0.3.3. Copyright 2019 undefined.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.d3 = global.d3 || {})));
}(this, (function (exports) { 'use strict';
// Compute x coordinates for nodes that maximizes the spread of nodes in [0, 1]
function center() {
function coordSpread(layers, separation) {
const maxWidth = Math.max(
...layers.map((layer) => {
layer[0].x = 0;
layer.slice(1).forEach((node, i) => {
const prev = layer[i];
node.x = prev.x + separation(prev, node);
});
return layer[layer.length - 1].x;
})
);
layers.forEach((layer) => {
const halfWidth = layer[layer.length - 1].x / 2;
layer.forEach((node) => {
node.x = (node.x - halfWidth) / maxWidth + 0.5;
});
});
return layers;
}
return coordSpread;
}
// Compute x coordinates for nodes that greedily assigns coordinates and then spaces them out
// TODO Implement other methods for initial greedy assignment
function greedy() {
let assignment = mean;
function coordGreedy(layers, separation) {
// Assign degrees
// The 3 at the end ensures that dummy nodes have the lowest priority
layers.forEach((layer) =>
layer.forEach((n) => (n.degree = n.children.length + (n.data ? 0 : -3)))
);
layers.forEach((layer) =>
layer.forEach((n) => n.children.forEach((c) => ++c.degree))
);
// Set first nodes
layers[0][0].x = 0;
layers[0].slice(1).forEach((node, i) => {
const last = layers[0][i];
node.x = last.x + separation(last, node);
});
// Set remaining nodes
layers.slice(0, layers.length - 1).forEach((top, i) => {
const bottom = layers[i + 1];
assignment(top, bottom);
// FIXME This order is import, i.e. we right and then left. We should actually do both, and then take the average
bottom
.map((n, j) => [n, j])
.sort(([an, aj], [bn, bj]) =>
an.degree === bn.degree ? aj - bj : bn.degree - an.degree
)
.forEach(([n, j]) => {
bottom.slice(j + 1).reduce((last, node) => {
node.x = Math.max(node.x, last.x + separation(last, node));
return node;
}, n);
bottom
.slice(0, j)
.reverse()
.reduce((last, node) => {
node.x = Math.min(node.x, last.x - separation(node, last));
return node;
}, n);
});
});
const min = Math.min(
...layers.map((layer) => Math.min(...layer.map((n) => n.x)))
);
const span =
Math.max(...layers.map((layer) => Math.max(...layer.map((n) => n.x)))) -
min;
layers.forEach((layer) => layer.forEach((n) => (n.x = (n.x - min) / span)));
layers.forEach((layer) => layer.forEach((n) => delete n.degree));
return layers;
}
return coordGreedy;
}
function mean(topLayer, bottomLayer) {
bottomLayer.forEach((node) => {
node.x = 0.0;
node._count = 0.0;
});
topLayer.forEach((n) =>
n.children.forEach((c) => (c.x += (n.x - c.x) / ++c._count))
);
bottomLayer.forEach((n) => delete n._count);
}
let epsilon = 1.0e-60;
let tmpa;
let tmpb;
do {
epsilon += epsilon;
tmpa = 1 + 0.1 * epsilon;
tmpb = 1 + 0.2 * epsilon;
} while (tmpa <= 1 || tmpb <= 1);
var vsmall = epsilon;
function dpori(a, lda, n) {
let kp1, t;
for (let k = 1; k <= n; k += 1) {
a[k][k] = 1 / a[k][k];
t = -a[k][k];
// dscal(k - 1, t, a[1][k], 1);
for (let i = 1; i < k; i += 1) {
a[i][k] *= t;
}
kp1 = k + 1;
if (n < kp1) {
break;
}
for (let j = kp1; j <= n; j += 1) {
t = a[k][j];
a[k][j] = 0;
// daxpy(k, t, a[1][k], 1, a[1][j], 1);
for (let i = 1; i <= k; i += 1) {
a[i][j] += t * a[i][k];
}
}
}
}
var dpori_1 = dpori;
function dposl(a, lda, n, b) {
let k, t;
for (k = 1; k <= n; k += 1) {
// t = ddot(k - 1, a[1][k], 1, b[1], 1);
t = 0;
for (let i = 1; i < k; i += 1) {
t += a[i][k] * b[i];
}
b[k] = (b[k] - t) / a[k][k];
}
for (let kb = 1; kb <= n; kb += 1) {
k = n + 1 - kb;
b[k] /= a[k][k];
t = -b[k];
// daxpy(k - 1, t, a[1][k], 1, b[1], 1);
for (let i = 1; i < k; i += 1) {
b[i] += t * a[i][k];
}
}
}
var dposl_1 = dposl;
function dpofa(a, lda, n, info) {
let jm1, t, s;
for (let j = 1; j <= n; j += 1) {
info[1] = j;
s = 0;
jm1 = j - 1;
if (jm1 < 1) {
s = a[j][j] - s;
if (s <= 0) {
break;
}
a[j][j] = Math.sqrt(s);
} else {
for (let k = 1; k <= jm1; k += 1) {
// t = a[k][j] - ddot(k - 1, a[1][k], 1, a[1][j], 1);
t = a[k][j];
for (let i = 1; i < k; i += 1) {
t -= a[i][j] * a[i][k];
}
t /= a[k][k];
a[k][j] = t;
s += t * t;
}
s = a[j][j] - s;
if (s <= 0) {
break;
}
a[j][j] = Math.sqrt(s);
}
info[1] = 0;
}
}
var dpofa_1 = dpofa;
function qpgen2(dmat, dvec, fddmat, n, sol, lagr, crval, amat, bvec, fdamat, q, meq, iact, nnact = 0, iter, work, ierr) {
let l1, it1, nvl, nact, temp, sum, t1, tt, gc, gs, nu, t1inf, t2min, go;
const r = Math.min(n, q);
let l = 2 * n + (r * (r + 5)) / 2 + 2 * q + 1;
for (let i = 1; i <= n; i += 1) {
work[i] = dvec[i];
}
for (let i = n + 1; i <= l; i += 1) {
work[i] = 0;
}
for (let i = 1; i <= q; i += 1) {
iact[i] = 0;
lagr[i] = 0;
}
const info = [];
if (ierr[1] === 0) {
dpofa_1(dmat, fddmat, n, info);
if (info[1] !== 0) {
ierr[1] = 2;
return;
}
dposl_1(dmat, fddmat, n, dvec);
dpori_1(dmat, fddmat, n);
} else {
for (let j = 1; j <= n; j += 1) {
sol[j] = 0;
for (let i = 1; i <= j; i += 1) {
sol[j] += dmat[i][j] * dvec[i];
}
}
for (let j = 1; j <= n; j += 1) {
dvec[j] = 0;
for (let i = j; i <= n; i += 1) {
dvec[j] += dmat[j][i] * sol[i];
}
}
}
crval[1] = 0;
for (let j = 1; j <= n; j += 1) {
sol[j] = dvec[j];
crval[1] += work[j] * sol[j];
work[j] = 0;
for (let i = j + 1; i <= n; i += 1) {
dmat[i][j] = 0;
}
}
crval[1] = -crval[1] / 2;
ierr[1] = 0;
const iwzv = n;
const iwrv = iwzv + n;
const iwuv = iwrv + r;
const iwrm = iwuv + r + 1;
const iwsv = iwrm + (r * (r + 1)) / 2;
const iwnbv = iwsv + q;
for (let i = 1; i <= q; i += 1) {
sum = 0;
for (let j = 1; j <= n; j += 1) {
sum += amat[j][i] * amat[j][i];
}
work[iwnbv + i] = Math.sqrt(sum);
}
nact = nnact;
iter[1] = 0;
iter[2] = 0;
function fnGoto50() {
iter[1] += 1;
l = iwsv;
for (let i = 1; i <= q; i += 1) {
l += 1;
sum = -bvec[i];
for (let j = 1; j <= n; j += 1) {
sum += amat[j][i] * sol[j];
}
if (Math.abs(sum) < vsmall) {
sum = 0;
}
if (i > meq) {
work[l] = sum;
} else {
work[l] = -Math.abs(sum);
if (sum > 0) {
for (let j = 1; j <= n; j += 1) {
amat[j][i] = -amat[j][i];
}
bvec[i] = -bvec[i];
}
}
}
for (let i = 1; i <= nact; i += 1) {
work[iwsv + iact[i]] = 0;
}
nvl = 0;
temp = 0;
for (let i = 1; i <= q; i += 1) {
if (work[iwsv + i] < temp * work[iwnbv + i]) {
nvl = i;
temp = work[iwsv + i] / work[iwnbv + i];
}
}
if (nvl === 0) {
for (let i = 1; i <= nact; i += 1) {
lagr[iact[i]] = work[iwuv + i];
}
return 999;
}
return 0;
}
function fnGoto55() {
for (let i = 1; i <= n; i += 1) {
sum = 0;
for (let j = 1; j <= n; j += 1) {
sum += dmat[j][i] * amat[j][nvl];
}
work[i] = sum;
}
l1 = iwzv;
for (let i = 1; i <= n; i += 1) {
work[l1 + i] = 0;
}
for (let j = nact + 1; j <= n; j += 1) {
for (let i = 1; i <= n; i += 1) {
work[l1 + i] = work[l1 + i] + dmat[i][j] * work[j];
}
}
t1inf = true;
for (let i = nact; i >= 1; i -= 1) {
sum = work[i];
l = iwrm + (i * (i + 3)) / 2;
l1 = l - i;
for (let j = i + 1; j <= nact; j += 1) {
sum -= work[l] * work[iwrv + j];
l += j;
}
sum /= work[l1];
work[iwrv + i] = sum;
if (iact[i] <= meq) {
continue;
}
if (sum <= 0) {
continue;
}
t1inf = false;
it1 = i;
}
if (!t1inf) {
t1 = work[iwuv + it1] / work[iwrv + it1];
for (let i = 1; i <= nact; i += 1) {
if (iact[i] <= meq) {
continue;
}
if (work[iwrv + i] <= 0) {
continue;
}
temp = work[iwuv + i] / work[iwrv + i];
if (temp < t1) {
t1 = temp;
it1 = i;
}
}
}
sum = 0;
for (let i = iwzv + 1; i <= iwzv + n; i += 1) {
sum += work[i] * work[i];
}
if (Math.abs(sum) <= vsmall) {
if (t1inf) {
ierr[1] = 1;
return 999; // GOTO 999
}
for (let i = 1; i <= nact; i += 1) {
work[iwuv + i] = work[iwuv + i] - t1 * work[iwrv + i];
}
work[iwuv + nact + 1] = work[iwuv + nact + 1] + t1;
return 700; // GOTO 700
}
sum = 0;
for (let i = 1; i <= n; i += 1) {
sum += work[iwzv + i] * amat[i][nvl];
}
tt = -work[iwsv + nvl] / sum;
t2min = true;
if (!t1inf) {
if (t1 < tt) {
tt = t1;
t2min = false;
}
}
for (let i = 1; i <= n; i += 1) {
sol[i] += tt * work[iwzv + i];
if (Math.abs(sol[i]) < vsmall) {
sol[i] = 0;
}
}
crval[1] += tt * sum * (tt / 2 + work[iwuv + nact + 1]);
for (let i = 1; i <= nact; i += 1) {
work[iwuv + i] = work[iwuv + i] - tt * work[iwrv + i];
}
work[iwuv + nact + 1] = work[iwuv + nact + 1] + tt;
if (t2min) {
nact += 1;
iact[nact] = nvl;
l = iwrm + ((nact - 1) * nact) / 2 + 1;
for (let i = 1; i <= nact - 1; i += 1) {
work[l] = work[i];
l += 1;
}
if (nact === n) {
work[l] = work[n];
} else {
for (let i = n; i >= nact + 1; i -= 1) {
if (work[i] === 0) {
continue;
}
gc = Math.max(Math.abs(work[i - 1]), Math.abs(work[i]));
gs = Math.min(Math.abs(work[i - 1]), Math.abs(work[i]));
if (work[i - 1] >= 0) {
temp = Math.abs(gc * Math.sqrt(1 + gs * gs /
(gc * gc)));
} else {
temp = -Math.abs(gc * Math.sqrt(1 + gs * gs /
(gc * gc)));
}
gc = work[i - 1] / temp;
gs = work[i] / temp;
if (gc === 1) {
continue;
}
if (gc === 0) {
work[i - 1] = gs * temp;
for (let j = 1; j <= n; j += 1) {
temp = dmat[j][i - 1];
dmat[j][i - 1] = dmat[j][i];
dmat[j][i] = temp;
}
} else {
work[i - 1] = temp;
nu = gs / (1 + gc);
for (let j = 1; j <= n; j += 1) {
temp = gc * dmat[j][i - 1] + gs * dmat[j][i];
dmat[j][i] = nu * (dmat[j][i - 1] + temp) -
dmat[j][i];
dmat[j][i - 1] = temp;
}
}
}
work[l] = work[nact];
}
} else {
sum = -bvec[nvl];
for (let j = 1; j <= n; j += 1) {
sum += sol[j] * amat[j][nvl];
}
if (nvl > meq) {
work[iwsv + nvl] = sum;
} else {
work[iwsv + nvl] = -Math.abs(sum);
if (sum > 0) {
for (let j = 1; j <= n; j += 1) {
amat[j][nvl] = -amat[j][nvl];
}
bvec[nvl] = -bvec[nvl];
}
}
return 700; // GOTO 700
}
return 0;
}
function fnGoto797() {
l = iwrm + (it1 * (it1 + 1)) / 2 + 1;
l1 = l + it1;
if (work[l1] === 0) {
return 798; // GOTO 798
}
gc = Math.max(Math.abs(work[l1 - 1]), Math.abs(work[l1]));
gs = Math.min(Math.abs(work[l1 - 1]), Math.abs(work[l1]));
if (work[l1 - 1] >= 0) {
temp = Math.abs(gc * Math.sqrt(1 + gs * gs / (gc * gc)));
} else {
temp = -Math.abs(gc * Math.sqrt(1 + gs * gs / (gc * gc)));
}
gc = work[l1 - 1] / temp;
gs = work[l1] / temp;
if (gc === 1) {
return 798; // GOTO 798
}
if (gc === 0) {
for (let i = it1 + 1; i <= nact; i += 1) {
temp = work[l1 - 1];
work[l1 - 1] = work[l1];
work[l1] = temp;
l1 += i;
}
for (let i = 1; i <= n; i += 1) {
temp = dmat[i][it1];
dmat[i][it1] = dmat[i][it1 + 1];
dmat[i][it1 + 1] = temp;
}
} else {
nu = gs / (1 + gc);
for (let i = it1 + 1; i <= nact; i += 1) {
temp = gc * work[l1 - 1] + gs * work[l1];
work[l1] = nu * (work[l1 - 1] + temp) - work[l1];
work[l1 - 1] = temp;
l1 += i;
}
for (let i = 1; i <= n; i += 1) {
temp = gc * dmat[i][it1] + gs * dmat[i][it1 + 1];
dmat[i][it1 + 1] = nu * (dmat[i][it1] + temp) -
dmat[i][it1 + 1];
dmat[i][it1] = temp;
}
}
return 0;
}
function fnGoto798() {
l1 = l - it1;
for (let i = 1; i <= it1; i += 1) {
work[l1] = work[l];
l += 1;
l1 += 1;
}
work[iwuv + it1] = work[iwuv + it1 + 1];
iact[it1] = iact[it1 + 1];
it1 += 1;
if (it1 < nact) {
return 797; // GOTO 797
}
return 0;
}
function fnGoto799() {
work[iwuv + nact] = work[iwuv + nact + 1];
work[iwuv + nact + 1] = 0;
iact[nact] = 0;
nact -= 1;
iter[2] += 1;
return 0;
}
go = 0;
while (true) { // eslint-disable-line no-constant-condition
go = fnGoto50();
if (go === 999) {
return;
}
while (true) { // eslint-disable-line no-constant-condition
go = fnGoto55();
if (go === 0) {
break;
}
if (go === 999) {
return;
}
if (go === 700) {
if (it1 === nact) {
fnGoto799();
} else {
while (true) { // eslint-disable-line no-constant-condition
fnGoto797();
go = fnGoto798();
if (go !== 797) {
break;
}
}
fnGoto799();
}
}
}
}
}
var qpgen2_1 = qpgen2;
function solveQP(Dmat, dvec, Amat, bvec = [], meq = 0, factorized = [0, 0]) {
const crval = [];
const iact = [];
const sol = [];
const lagr = [];
const work = [];
const iter = [];
let message = "";
// In Fortran the array index starts from 1
const n = Dmat.length - 1;
const q = Amat[1].length - 1;
if (!bvec) {
for (let i = 1; i <= q; i += 1) {
bvec[i] = 0;
}
}
if (n !== Dmat[1].length - 1) {
message = "Dmat is not symmetric!";
}
if (n !== dvec.length - 1) {
message = "Dmat and dvec are incompatible!";
}
if (n !== Amat.length - 1) {
message = "Amat and dvec are incompatible!";
}
if (q !== bvec.length - 1) {
message = "Amat and bvec are incompatible!";
}
if ((meq > q) || (meq < 0)) {
message = "Value of meq is invalid!";
}
if (message !== "") {
return {
message
};
}
for (let i = 1; i <= q; i += 1) {
iact[i] = 0;
lagr[i] = 0;
}
const nact = 0;
const r = Math.min(n, q);
for (let i = 1; i <= n; i += 1) {
sol[i] = 0;
}
crval[1] = 0;
for (let i = 1; i <= (2 * n + (r * (r + 5)) / 2 + 2 * q + 1); i += 1) {
work[i] = 0;
}
for (let i = 1; i <= 2; i += 1) {
iter[i] = 0;
}
qpgen2_1(Dmat, dvec, n, n, sol, lagr, crval, Amat, bvec, n, q, meq, iact, nact, iter, work, factorized);
if (factorized[1] === 1) {
message = "constraints are inconsistent, no solution!";
}
if (factorized[1] === 2) {
message = "matrix D in quadratic function is not positive definite!";
}
return {
solution: sol,
Lagrangian: lagr,
value: crval,
unconstrained_solution: dvec, // eslint-disable-line camelcase
iterations: iter,
iact,
message
};
}
var solveQP_1 = solveQP;
var quadprog = {
solveQP: solveQP_1
};
const { solveQP: solveQP$1 } = quadprog;
var wrapper = function(qmat, cvec, amat, bvec, meq = 0, factorized = false) {
const Dmat = [null].concat(qmat.map(row => [null].concat(row)));
const dvec = [null].concat(cvec.map(v => -v));
const Amat = [null].concat(amat.length === 0 ? new Array(qmat.length).fill([null]) : amat[0].map((_, i) => [null].concat(amat.map(row => -row[i]))));
const bvecp = [null].concat(bvec.map(v => -v));
const {
solution,
Lagrangian: lagrangian,
value: boxedVal,
unconstrained_solution: unconstrained,
iterations: iters,
iact,
message
} = solveQP$1(Dmat, dvec, Amat, bvecp, meq, [, +factorized]); // eslint-disable-line no-sparse-arrays
if (message.length > 0) {
throw new Error(message);
} else {
solution.shift();
lagrangian.shift();
unconstrained.shift();
iact.push(0);
const active = iact.slice(1, iact.indexOf(0)).map(v => v - 1);
const [, value] = boxedVal;
const [, iterations, inactive] = iters;
return {
solution,
lagrangian,
unconstrained,
iterations,
inactive,
active,
value
};
}
};
var quadprogJs = wrapper;
// Assign coords to layers by solving a QP
// Compute indices used to index arrays
function indices(layers) {
const inds = {};
let i = 0;
layers.forEach((layer) => layer.forEach((n) => (inds[n.id] = i++)));
return inds;
}
// Compute constraint arrays for layer separation
function sep(layers, inds, separation) {
const n = 1 + Math.max(...Object.values(inds));
const A = [];
const b = [];
layers.forEach((layer) =>
layer.slice(0, layer.length - 1).forEach((first, i) => {
const second = layer[i + 1];
const find = inds[first.id];
const sind = inds[second.id];
const cons = new Array(n).fill(0);
cons[find] = 1;
cons[sind] = -1;
A.push(cons);
b.push(-separation(first, second));
})
);
return [A, b];
}
// Update Q that minimizes edge distance squared
function minDist(Q, pind, cind, coef) {
Q[cind][cind] += coef;
Q[cind][pind] -= coef;
Q[pind][cind] -= coef;
Q[pind][pind] += coef;
}
// Update Q that minimizes curve of edges through a node
function minBend(Q, pind, nind, cind, coef) {
Q[cind][cind] += coef;
Q[cind][nind] -= 2 * coef;
Q[cind][pind] += coef;
Q[nind][cind] -= 2 * coef;
Q[nind][nind] += 4 * coef;
Q[nind][pind] -= 2 * coef;
Q[pind][cind] += coef;
Q[pind][nind] -= 2 * coef;
Q[pind][pind] += coef;
}
// Solve for node positions
function solve(Q, c, A, b, meq = 0) {
// Arbitrarily set the last coordinate to 0, which makes the formula valid
// This is simpler than special casing the last element
c.pop();
Q.pop();
Q.forEach((row) => row.pop());
A.forEach((row) => row.pop());
// Solve
const { solution } = quadprogJs(Q, c, A, b, meq);
// Undo last coordinate removal
solution.push(0);
return solution;
}
// Assign nodes x in [0, 1] based on solution
function layout(layers, inds, solution) {
// Rescale to be in [0, 1]
const min = Math.min(...solution);
const span = Math.max(...solution) - min;
layers.forEach((layer) =>
layer.forEach((n) => (n.x = (solution[inds[n.id]] - min) / span))
);
}
// Assign nodes in each layer an x coordinate in [0, 1] that minimizes curves
function checkWeight(weight) {
if (weight < 0 || weight >= 1) {
throw new Error(`weight must be in [0, 1), but was ${weight}`);
} else {
return weight;
}
}
function minCurve() {
let weight = 0.5;
function coordMinCurve(layers, separation) {
const inds = indices(layers);
const n = Object.keys(inds).length;
const [A, b] = sep(layers, inds, separation);
const c = new Array(n).fill(0);
const Q = new Array(n).fill(null).map(() => new Array(n).fill(0));
layers.forEach((layer) =>
layer.forEach((parent) => {
const pind = inds[parent.id];
parent.children.forEach((child) => {
const cind = inds[child.id];
minDist(Q, pind, cind, 1 - weight);
});
})
);
layers.forEach((layer) =>
layer.forEach((parent) => {
const pind = inds[parent.id];
parent.children.forEach((node) => {
const nind = inds[node.id];
node.children.forEach((child) => {
const cind = inds[child.id];
minBend(Q, pind, nind, cind, weight);
});
});
})
);
const solution = solve(Q, c, A, b);
layout(layers, inds, solution);
return layers;
}
coordMinCurve.weight = function(x) {
return arguments.length
? ((weight = checkWeight(x)), coordMinCurve)
: weight;
};
return coordMinCurve;
}
// Assign nodes in each layer an x coordinate in [0, 1] that minimizes curves
function topological() {
function coordTopological(layers, separation) {
if (
!layers.every((layer) => 1 === layer.reduce((c, n) => c + !!n.data, 0))
) {
throw new Error(
"coordTopological() only works with a topological ordering"
);
}
// This takes advantage that the last "node" is set to 0
const inds = {};
let i = 0;
layers.forEach((layer) =>
layer.forEach((n) => n.data || (inds[n.id] = i++))
);
layers.forEach((layer) =>
layer.forEach((n) => inds[n.id] === undefined && (inds[n.id] = i))
);
const n = ++i;
const [A, b] = sep(layers, inds, separation);
const c = new Array(n).fill(0);
const Q = new Array(n).fill(null).map(() => new Array(n).fill(0));
layers.forEach((layer) =>
layer.forEach((parent) => {
const pind = inds[parent.id];
parent.children.forEach((node) => {
if (!node.data) {
const nind = inds[node.id];
node.children.forEach((child) => {
const cind = inds[child.id];
minBend(Q, pind, nind, cind, 1);
});
}
});
})
);
const solution = solve(Q, c, A, b);
layout(layers, inds, solution);
return layers;
}
return coordTopological;
}
// Assign nodes in each layer an x coordinate in [0, 1] that minimizes curves
function vert() {
function coordVert(layers, separation) {
const inds = indices(layers);
const n = Object.keys(inds).length;
const [A, b] = sep(layers, inds, separation);
const c = new Array(n).fill(0);
const Q = new Array(n).fill(null).map(() => new Array(n).fill(0));
layers.forEach((layer) =>
layer.forEach((parent) => {
const pind = inds[parent.id];
parent.children.forEach((child) => {
const cind = inds[child.id];
if (parent.data) {
minDist(Q, pind, cind, 1);
}
if (child.data) {
minDist(Q, pind, cind, 1);
}
});
})
);
layers.forEach((layer) =>
layer.forEach((parent) => {
const pind = inds[parent.id];
parent.children.forEach((node) => {
if (!node.data) {
const nind = inds[node.id];
node.children.forEach((child) => {
const cind = inds[child.id];
minBend(Q, pind, nind, cind, 1);
});
}
});
})
);
const solution = solve(Q, c, A, b);
layout(layers, inds, solution);
return layers;
}
return coordVert;
}
// Compute x0 and x1 coordinates for nodes that maximizes the spread of nodes in [0, 1].
// It uses columnIndex that has to be present in each node.
// due to the varying height of the nodes, nodes from different layers might be present at the same y coordinate
// therefore, nodes should not be centered in their layer but centering should be considered over all layers
//
function column2CoordRect() {
function coordSpread(layers, columnWidthFunction, columnSeparationFunction) {
// calculate the number of columns
const maxColumns = Math.max(
...layers.map((layer) =>
Math.max(...layer.map((node) => node.columnIndex + 1))
)
);
// call columnWidthFunction for each column index to get an array with the width of each column index:
let columnWidth = Array.from(Array(maxColumns).keys())
.map((_, index) => index)
.map(columnWidthFunction);
// similarly for the separation of the columns, where columnSeparation[0] is the separation between column 0 and 1:
let columnSeparation = Array.from(Array(maxColumns).keys())
.map((_, index) => index)
.map(columnSeparationFunction);
const maxWidth = Math.max(
...layers.map((layer) => {
layer.forEach((node) => {
node.x0 = getColumnStartCoordinate(
columnWidth,
columnSeparation,
node.columnIndex
);
node.x1 = node.x0 + columnWidth[node.columnIndex];
});
return Math.max(...layer.map((node) => node.x1));
})
);
layers.forEach((layer) => {
layer.forEach((node) => {
node.x0 = node.x0 / maxWidth;
node.x1 = node.x1 / maxWidth;
});
});
return layers;
}
return coordSpread;
function getColumnStartCoordinate(
columnWidth,
columnSeparation,
columnIndex
) {
let leadingColumnWidths = columnWidth.filter(
(_, index) => index < columnIndex
);
let leadingColumnSeparations = columnSeparation.filter(
(_, index) => index < columnIndex
);
return leadingColumnWidths
.concat(leadingColumnSeparations)
.reduce((prevVal, currentVal) => prevVal + currentVal, 0);
}
}
function simpleLeft() {
// trivial column assignment (based on node's index in its layer => fill up columns from left to right)
function columnIndexAssignmentLeftToRight(layers) {
layers.forEach((layer) => {
layer.forEach((node, nodeIndex) => (node.columnIndex = nodeIndex));
});
}
return columnIndexAssignmentLeftToRight;
}
function simpleCenter() {
// keeps order of nodes in a layer but spreads nodes in a layer over the middle columns
function columnIndexAssignmentCenter(layers) {
const maxNodesPerLayer = Math.max(...layers.map((layer) => layer.length));
layers.forEach((layer) => {
const nodesInLayer = layer.length;
const startColumnIndex = Math.floor(
(maxNodesPerLayer - nodesInLayer) / 2
);
layer.forEach(
(node, nodeIndex) => (node.columnIndex = startColumnIndex + nodeIndex)
);
});
}
return columnIndexAssignmentCenter;
}
function adjacent() {
let center = false;
function columnIndexAssignmentAdjacent(layers) {
// assigns column indices to the layer with most nodes first.
// afterwards starting from the layer with most nodes, column indices are assigned
// to nodes in adjacent layers. Column indices are assigned with respect to the
// node's parents or children while maintaining the same ordering in the layer.
// overlapping nodes can occur because nodes can be placed in the same column
// although they do not have a children/parents relation with each other
if (layers.length == 0) {
return;
}
// find layer index with most entries:
const maxNodesCount = Math.max(...layers.map((layer) => layer.length));
const maxNodesLayerIndex = layers.findIndex(
(layer) => layer.length === maxNodesCount
);
// layer with most nodes simply assign columnIndex to the node's index:
layers[maxNodesLayerIndex].forEach(
(node, index) => (node.columnIndex = index)
);
// layer with most nodes stays unchanged
// first, visit each layer above the layer with most nodes
for (let i = maxNodesLayerIndex - 1; i >= 0; i--) {
fillLayerBackwards(layers[i]);
}
// then, visit each layer below the layer with most nodes
for (let i = maxNodesLayerIndex + 1; i < layers.length; i++) {
fillLayerForward(layers[i]);
}
function fillLayerBackwards(layer) {
let actualColumnIndices;
if (layer.length === maxNodesCount) {
// leave layer unchanged
actualColumnIndices = layer.map((_, index) => index);
} else {
// map each node to its desired location:
const desiredColumnIndices = layer.map((node, index) => {
if (node.children == null || node.children.length === 0) {
return index;
}
const childrenColumnIndices = node.children.map(
(child) => child.columnIndex
);
if (center) {
// return column index of middle child
return childrenColumnIndices[
Math.floor((childrenColumnIndices.length - 1) / 2)
];
} else {
return Math.min(...childrenColumnIndices);
}
});
// based on the desired column index, the actual column index needs to be assigned
// however, the column indices have to be strictly monotonically increasing and have to
// be greater or equal 0 and smaller than maxNodesCount!
actualColumnIndices = optimizeColumnIndices(desiredColumnIndices);
}
// assign now the column indices to the nodes:
layer.forEach(
(node, index) => (node.columnIndex = actualColumnIndices[index])
);
}
function fillLayerForward(layer) {
let actualColumnIndices;
if (layer.length === maxNodesCount) {
// leave layer unchanged
actualColumnIndices = layer.map((_, index) => index);
} else {
// map each node to its desired location:
const desiredColumnIndices = layer.map((node, index) => {
if (node.parents == null || node.parents.length === 0) {
return index;
}
const parentColumnIndices = node.parents.map(
(parent) => parent.columnIndex
);
if (center) {
// return column index of middle parent
return parentColumnIndices[
Math.floor((parentColumnIndices.length - 1) / 2)
];
} else {
return Math.min(...parentColumnIndices);
}
});
// based on the desired column index, the actual column index needs to be assigned
// however, the column indices have to be strictly monotonically increasing and have to
// be greater or equal 0 and smaller than maxNodesCount!
actualColumnIndices = optimizeColumnIndices(desiredColumnIndices);
}
// assign now the column indices to the nodes:
layer.forEach(
(node, index) => (node.columnIndex = actualColumnIndices[index])
);
}
function optimizeColumnIndices(desiredColumnIndices) {
if (!desiredColumnIndices.every((columnIndex) => isFinite(columnIndex))) {
throw `columnComplex: non-finite column index encountered`;
}
// step 1: reorder indices such that they are strictly monotonically increasing
let largestIndex = -1;
desiredColumnIndices = desiredColumnIndices.map((columnIndex) => {
if (columnIndex <= largestIndex) {
columnIndex = largestIndex + 1;
}
largestIndex = columnIndex;
return columnIndex;
});
// step 2: shift indices such that they are larger or equal 0 and smaller than maxNodesCount
const max = Math.max(...desiredColumnIndices);
const downShift = max - (maxNodesCount - 1);
if (downShift > 0) {
// nodes need to be shifted by that amount
desiredColumnIndices = desiredColumnIndices.map((columnIndex, index) =>
Math.max(columnIndex - downShift, index)
);
}
return desiredColumnIndices;
}
}
columnIndexAssignmentAdjacent.center = function(x) {
return arguments.length
? ((center = x), columnIndexAssignmentAdjacent)
: center;
};
return columnIndexAssignmentAdjacent;
}
function complex() {
let center = false;
function columnIndexAssignmentSubtree(layers) {
// starts at root nodes and assigns column indices based on their subtrees
if (layers.length == 0) {
return;
}
// find all root nodes
let roots = [];
layers.forEach((layer) =>
layer.forEach((node) => {
if (node.parents == null || node.parents.length === 0) {
roots.push(node);
}
})
);
// iterate over each root and assign column indices to each node in its subtree.
// if a node already has a columnIndex, do not change it, this case can occur if the node has more than one predecessor
let startColumnIndex = 0;
roots.forEach((node) => {
const subtreeWidth = getSubtreeWidth(node);
node.columnIndex =
startColumnIndex + (center ? Math.floor((subtreeWidth - 1) / 2) : 0);
assignColumnIndexToChildren(node, startColumnIndex);
startColumnIndex += subtreeWidth;
});
function getSubtreeWidth(node) {
if (node.children.length === 0) {
return 1;
}
return node.children.reduce(
(prevVal, child) => prevVal + getSubtreeWidth(child),
0
);
}
function assignColumnIndexToChildren(node, startColumnIndex) {
const widthPerChild = node.children.map(getSubtreeWidth);
let childColumnIndex = startColumnIndex;
node.children.forEach((child, index) => {
if (child.columnIndex !== undefined) {
// stop recursion, this child was already visited
return;
}
child.columnIndex =
childColumnIndex +
(center ? Math.floor((widthPerChild[index] - 1) / 2) : 0);
assignColumnIndexToChildren(child, childColumnIndex);
childColumnIndex += widthPerChild[index];
});
}
}
columnIndexAssignmentSubtree.center = function(x) {
return arguments.length
? ((center = x), columnIndexAssignmentSubtree)
: center;
};
return columnIndexAssignmentSubtree;
}
// Get an array of all links to children
function childLinks() {
const links = [];
this.eachChildLinks((l) => links.push(l));
return links;
}
// This function sets the value of each descendant to be the number of its descendants including itself
function count() {
this.eachAfter((node) => {
if (node.children.length) {
node._leaves = Object.assign({}, ...node.children.map((c) => c._leaves));
node.value = Object.keys(node._leaves).length;
} else {
node._leaves = { [node.id]: true };
node.value = 1;
}
});
this.each((n) => delete n._leaves);
return this;
}
// Return true if the dag is connected
function connected() {
if (this.id !== undefined) {
return true;
}
const rootsSpan = this.roots().map((r) => r.descendants().map((n) => n.id));
const reached = rootsSpan.map(() => false);
const queue = [reached.length - 1];
while (queue.length) {
const i = queue.pop();
if (reached[i]) {
continue; // already explored
}
const spanMap = {};
reached[i] = true;
rootsSpan[i].forEach((n) => (spanMap[n] = true));
rootsSpan.forEach((span, j) => {
if (span.some((n) => spanMap[n])) {
queue.push(j);
}
});
}
return reached.every((b) => b);
}
// Set each node's value to be zero for leaf nodes and the greatest distance to
// any leaf node for other nodes
function depth() {
this.each((n) => {
n.children.forEach((c) => (c._parents || (c._parents = [])).push(n));
});
this.eachBefore((n) => {
n.value = Math.max(0, ...(n._parents || []).map((c) => 1 + c.value));
});
this.each((n) => delete n._parents);
return this;
}
// Return an array of all descendants
function descendants() {
const descs = [];
this.each((n) => descs.push(n));
return descs;
}
// Call function on each node such that a node is called before any of its parents
function eachAfter(func) {
// TODO Better way to do this?
const all = [];
this.eachBefore((n) => all.push(n));
all.reverse().forEach(func);
return this;
}
// Call a function on each node such that a node is called before any of its children
function eachBefore(func) {
this.each((n) => (n._num_before = 0));
this.each((n) => n.children.forEach((c) => ++c._num_before));
const queue = this.roots();
let node;
let i = 0;
while ((node = queue.pop())) {
func(node, i++);
node.children.forEach((n) => --n._num_before || queue.push(n));
}
this.each((n) => delete n._num_before);
return this;
}
// Call nodes in bread first order
// No guarantees are made on whether the function is called first, or children
// are queued. This is important if the function modifies a node's children.
function eachBreadth(func) {
const seen = {};
let current = [];
let next = this.roots();
let i = 0;
do {
current = next.reverse();
next = [];
let node;
while ((node = current.pop())) {
if (!seen[node.id]) {
seen[node.id] = true;
func(node, i++);
next.push(...node.children);
}
}
} while (next.length);
}
// Call a function on each child link
function eachChildLinks(func) {
if (this.id !== undefined) {
let i = 0;
this.children.forEach((c, j) =>
func(
{
source: this,
target: c,
data: this._childLinkData[j]
},
i++
)
);
}
return this;
}
// Call a function on each node in an arbitrary order
// No guarantees are made with respect to whether the function is called first
// or the children are queued. This is important if the function modifies the
// children of a node.
function eachDepth(func) {
const queue = this.roots();
const seen = {};
let node;
let i = 0;
while ((node = queue.pop())) {
if (!seen[node.id]) {
seen[node.id] = true;
func(node, i++);
queue.push(...node.children);
}
}
return this;
}
// Call a function on each link in the dag
function eachLinks(func) {
let i = 0;
this.each((n) => n.eachChildLinks((l) => func(l, i++)));
return this;
}
// Compare two dag_like objects for equality
function toSet(arr) {
const set = {};
arr.forEach((e) => (set[e] = true));
return set;
}
function info(root) {
const info = {};
root.each(
(node) =>
(info[node.id] = [node.data, toSet(node.children.map((n) => n.id))])
);
return info;
}
function setEqual(a, b) {
return (
Object.keys(a).length === Object.keys(b).length &&
Object.keys(a).every((k) => b[k])
);
}
function equals(that) {
const thisInfo = info(this);
const thatInfo = info(that);
return (
Object.keys(thisInfo).length === Object.keys(thatInfo).length &&
Object.entries(thisInfo).every(([nid, [thisData, thisChildren]]) => {
const val = thatInfo[nid];
if (!val) return false;
const [thatData, thatChildren] = val;
return thisData === thatData && setEqual(thisChildren, thatChildren);
})
);
}
// Return true of function returns true for every node
const sentinel = {};
function every(func) {
try {
this.each((n, i) => {
if (!func(n, i)) {
throw sentinel;
}
});
} catch (err) {
if (err === sentinel) {
return false;
} else {
throw err;
}
}
return true;
}
// Set each node's value to zero for leaf nodes and the greatest distance to
// any leaf for other nodes
function height() {
return this.eachAfter(
(n) => (n.value = Math.max(0, ...n.children.map((c) => 1 + c.value)))
);
}
// Return an array of all of the links in a dag
function links() {
const links = [];
this.eachLinks((l) => links.push(l));
return links;
}
// Reduce over nodes
function reduce(func, start) {
let accum = start;
this.each((n, i) => {
accum = func(accum, n, i);
});
return accum;
}
// Return the roots of the current dag
function roots() {
return this.id === undefined ? this.children.slice() : [this];
}
// Count the number of nodes
function size() {
return this.reduce((a) => a + 1, 0);
}
// Return true if function returns true on at least one node
const sentinel$1 = {};
function some(func) {
try {
this.each((n) => {
if (func(n)) {
throw sentinel$1;
}
});
} catch (err) {
if (err === sentinel$1) {
return true;
} else {
throw err;
}
}
return false;
}
// Call a function on each nodes data and set its value to the sum of the function return and the return value of all descendants
function sum(func) {
this.eachAfter((node, i) => {
const val = +func(node.data, i);
node._descendants = Object.assign(
{ [node.id]: val },
...node.children.map((c) => c._descendants)
);
node.value = Object.values(node._descendants).reduce((a, b) => a + b);
});
this.each((n) => delete n._descendants);
return this;
}
function Node(id, data) {
this.id = id;
this.data = data;
this.children = [];
this._childLinkData = [];
}
// Must be internal for new Node creation
// Copy this dag returning a new DAG pointing to the same data with same structure.
function copy() {
const nodes = [];
const cnodes = [];
const mapping = {};
this.each((node) => {
nodes.push(node);
const cnode = new Node(node.id, node.data);
cnodes.push(cnode);
mapping[cnode.id] = cnode;
});
cnodes.forEach((cnode, i) => {
const node = nodes[i];
cnode.children = node.children.map((c) => mapping[c.id]);
});
if (this.id === undefined) {
const root = new Node(undefined, undefined);
root.children = this.children.map((c) => mapping[c.id]);
} else {
return mapping[this.id];
}
}
// Reverse
function reverse() {
const nodes = [];
const cnodes = [];
const mapping = {};
const root = new Node(undefined, undefined);
this.each((node) => {
nodes.push(node);
const cnode = new Node(node.id, node.data);
cnodes.push(cnode);
mapping[cnode.id] = cnode;
if (!node.children.length) {
root.children.push(cnode);
}
});
cnodes.forEach((cnode, i) => {
const node = nodes[i];
node.children.map((c, j) => {
const cc = mapping[c.id];
cc.children.push(cnode);
cc._childLinkData.push(node._childLinkData[j]);
});
});
return root.children.length > 1 ? root : root.children[0];
}
Node.prototype = {
constructor: Node,
childLinks: childLinks,
copy: copy,
count: count,
connected: connected,
depth: depth,
descendants: descendants,
each: eachDepth,
eachAfter: eachAfter,
eachBefore: eachBefore,
eachBreadth: eachBreadth,
eachChildLinks: eachChildLinks,
eachLinks: eachLinks,
equals: equals,
every: every,
height: height,
links: links,
reduce: reduce,
reverse: reverse,
roots: roots,
size: size,
some: some,
sum: sum
};
// Verify that a dag meets all criteria for validity
// Note, this is written such that root must be a dummy node, i.e. have an undefined id
function verify(root) {
// Test that dummy criteria is met
if (root.id !== undefined) throw new Error("invalid format for verification");
// Test that there are roots
if (!root.children.length) throw new Error("no roots");
// Test that dag is free of cycles
const seen = {};
const past = {};
let rec = undefined;
function visit(node) {
if (seen[node.id]) {
return false;
} else if (past[node.id]) {
rec = node.id;
return [node.id];
} else {
past[node.id] = true;
let result = node.children.reduce((chain, c) => chain || visit(c), false);
delete past[node.id];
seen[node.id] = true;
if (result && rec) result.push(node.id);
if (rec === node.id) rec = undefined;
return result;
}
}
const msg =
root.id === undefined
? root.children.reduce((msg, r) => msg || visit(r), false)
: visit(root);
if (msg)
throw new Error("