UNPKG

d3-dag

Version:

Layout algorithms for visualizing directed acylic graphs.

1,698 lines (1,501 loc) 208 kB
// 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("