First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
Original dagre-d3 copyright: Copyright (c) 2013 Chris Pettitt
Original dagre copyright: Copyright (c) 2012-2014 Chris Pettitt
Original graphlib copyright: Copyright (c) 2012-2014 Chris Pettitt
Copyright (c) 2022-2024 Thibaut Lassalle, David Newell, Alois Klink, Sidharth Vinod and dagre-es contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
<p align="center">
<a href="https://tbo47.github.io/" ><img src="https://img.shields.io/badge/created_by-tbo47-blue.svg" alt="Created by tbo47"></a>
<a href="https://www.npmjs.com/dagre-d3-es"><img src="https://img.shields.io/npm/v/dagre-d3-es.svg?logo=npm&logoColor=fff&label=NPM+package&color=limegreen" alt="dagre-d3-es on npm" /></a>&nbsp;
<a href="https://www.npmjs.com/dagre-d3-es"><img src="https://img.shields.io/npm/dw/dagre-d3-es.svg?logo=npm&logoColor=fff&label=NPM+package&color=limegreen" alt="dagre-d3-es on npm" /></a>
</p>
# dagre-d3-es
[dagre-d3-es](https://www.npmjs.com/package/dagre-d3-es) is a fork of [dagre-d3](https://github.com/dagrejs) using the more modern ES6 javascript syntax.
It uses [ES](https://262.ecma-international.org/6.0/) modules, thus the name [dagre-d3-es](https://www.npmjs.com/package/dagre-d3-es).
[dagre-d3-es](https://www.npmjs.com/package/dagre-d3-es) follows [d3](https://www.npmjs.com/package/d3) versions. Ex: dagre-d3-es version 7 depends on [d3](https://www.npmjs.com/package/d3) version 7.
## Demos
- [simple graph demo](https://codesandbox.io/s/dagre-d3-es-tree-9ywg9) using react
- clone and run [dagre-es-example](https://github.com/tbo47/dagre-es-example) using angular
- [vanilla javascript example](https://tbo47.github.io/dagre-d3-esm_example1/)
- [jsfiddle vanilla javascript example](https://jsfiddle.net/tbo47/fm9ovqeb/)
## Install
```
npm install dagre-d3-es
```
## Code example
Coming from the legacy `dagre-d3`, the main changes in your code will be:
```
import * as dagreD3 from 'dagre-d3-es';
...
const g = new dagreD3.graphlib.Graph().setGraph({});
...
const zoom = d3.zoom().on('zoom', (zoomEvent) => {
inner.attr('transform', zoomEvent.transform);
});
```
+64
View File
@@ -0,0 +1,64 @@
{
"name": "dagre-d3-es",
"version": "7.0.14",
"description": "",
"license": "MIT",
"main": "src/index.js",
"type": "module",
"scripts": {
"bundle": "chmod +x bundle.sh ; ./bundle.sh && cp dist/dagre-d3*.js ../tbo47.github.io",
"generate_types": "find src -name '*.d.ts' -type f -delete ; tsc --project tsconfig.build.json",
"prepack": "npm run generate_types",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
"test": "vitest ",
"my_1_npm_login": "npm login",
"my_2_publish": "echo update_package_version ; git clean -xdf ; npm ci ; npm publish",
"compile_js_as_ts": "tsc src/index.js --AllowJs --checkJs --outDir dist/"
},
"keywords": [
"dagre-d3"
],
"repository": {
"type": "git",
"url": "git+https://github.com/tbo47/dagre-es.git"
},
"dependencies": {
"d3": "^7.9.0",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"@types/lodash-es": "^4.17.12",
"esbuild": "^0.25.10",
"eslint": "^8.57.1",
"eslint-plugin-import": "^2.32.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
"contributors": [
{
"name": "Thibaut Lassalle",
"url": "https://tbo47.github.io/"
},
{
"name": "Alois Klink",
"url": "https://github.com/aloisklink"
},
{
"name": "David Newell",
"url": "https://github.com/rustedgrail"
},
{
"name": "Sidharth Vinod",
"url": "https://github.com/sidharthv96"
}
],
"files": [
"src/**/*",
"!src/**/*.test.js"
],
"types": "./src/index.d.ts"
}
+10
View File
@@ -0,0 +1,10 @@
export namespace arrows {
export { normal };
export { vee };
export { undirected };
}
export function setArrows(value: any): void;
declare function normal(parent: any, id: any, edge: any, type: any): void;
declare function vee(parent: any, id: any, edge: any, type: any): void;
declare function undirected(parent: any, id: any, edge: any, type: any): void;
export {};
+82
View File
@@ -0,0 +1,82 @@
import * as util from './util.js';
export { arrows, setArrows };
var arrows = {
normal,
vee,
undirected,
};
function setArrows(value) {
arrows = value;
}
function normal(parent, id, edge, type) {
var marker = parent
.append('marker')
.attr('id', id)
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 5)
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto');
var path = marker
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
util.applyStyle(path, edge[type + 'Style']);
if (edge[type + 'Class']) {
path.attr('class', edge[type + 'Class']);
}
}
function vee(parent, id, edge, type) {
var marker = parent
.append('marker')
.attr('id', id)
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 5)
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto');
var path = marker
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z')
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
util.applyStyle(path, edge[type + 'Style']);
if (edge[type + 'Class']) {
path.attr('class', edge[type + 'Class']);
}
}
function undirected(parent, id, edge, type) {
var marker = parent
.append('marker')
.attr('id', id)
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 5)
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto');
var path = marker
.append('path')
.attr('d', 'M 0 5 L 10 5')
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
util.applyStyle(path, edge[type + 'Style']);
if (edge[type + 'Class']) {
path.attr('class', edge[type + 'Class']);
}
}
+2
View File
@@ -0,0 +1,2 @@
export function createClusters(selection: any, g: any): any;
export function setCreateClusters(value: any): void;
+49
View File
@@ -0,0 +1,49 @@
import * as d3 from 'd3';
import { addLabel } from './label/add-label.js';
import * as util from './util.js';
export { createClusters, setCreateClusters };
var createClusters = function (selection, g) {
var clusters = g.nodes().filter(function (v) {
return util.isSubgraph(g, v);
});
var svgClusters = selection.selectAll('g.cluster').data(clusters, function (v) {
return v;
});
util.applyTransition(svgClusters.exit(), g).style('opacity', 0).remove();
var enterSelection = svgClusters
.enter()
.append('g')
.attr('class', 'cluster')
.attr('id', function (v) {
var node = g.node(v);
return node.id;
})
.style('opacity', 0)
.each(function (v) {
var node = g.node(v);
var thisGroup = d3.select(this);
d3.select(this).append('rect');
var labelGroup = thisGroup.append('g').attr('class', 'label');
addLabel(labelGroup, node, node.clusterLabelPos);
});
svgClusters = svgClusters.merge(enterSelection);
svgClusters = util.applyTransition(svgClusters, g).style('opacity', 1);
svgClusters.selectAll('rect').each(function (c) {
var node = g.node(c);
var domCluster = d3.select(this);
util.applyStyle(domCluster, node.style);
});
return svgClusters;
};
function setCreateClusters(value) {
createClusters = value;
}
+2
View File
@@ -0,0 +1,2 @@
export function createEdgeLabels(selection: any, g: any): any;
export function setCreateEdgeLabels(value: any): void;
+53
View File
@@ -0,0 +1,53 @@
import * as d3 from 'd3';
import { addLabel } from './label/add-label.js';
import * as util from './util.js';
export { createEdgeLabels, setCreateEdgeLabels };
let createEdgeLabels = function (selection, g) {
var svgEdgeLabels = selection
.selectAll('g.edgeLabel')
.data(g.edges(), function (e) {
return util.edgeToId(e);
})
.classed('update', true);
svgEdgeLabels.exit().remove();
svgEdgeLabels.enter().append('g').classed('edgeLabel', true).style('opacity', 0);
svgEdgeLabels = selection.selectAll('g.edgeLabel');
svgEdgeLabels.each(function (e) {
var root = d3.select(this);
root.select('.label').remove();
var edge = g.edge(e);
var label = addLabel(root, g.edge(e), 0).classed('label', true);
var bbox = label.node().getBBox();
if (edge.labelId) {
label.attr('id', edge.labelId);
}
if (!Object.prototype.hasOwnProperty.call(edge, 'width')) {
edge.width = bbox.width;
}
if (!Object.prototype.hasOwnProperty.call(edge, 'height')) {
edge.height = bbox.height;
}
});
var exitSelection;
if (svgEdgeLabels.exit) {
exitSelection = svgEdgeLabels.exit();
} else {
exitSelection = svgEdgeLabels.selectAll(null); // empty selection
}
util.applyTransition(exitSelection, g).style('opacity', 0).remove();
return svgEdgeLabels;
};
function setCreateEdgeLabels(value) {
createEdgeLabels = value;
}
+2
View File
@@ -0,0 +1,2 @@
export function createEdgePaths(selection: any, g: any, arrows: any): any;
export function setCreateEdgePaths(value: any): void;
+132
View File
@@ -0,0 +1,132 @@
import * as d3 from 'd3';
import * as _ from 'lodash-es';
import { intersectNode } from './intersect/intersect-node.js';
import * as util from './util.js';
export { createEdgePaths, setCreateEdgePaths };
var createEdgePaths = function (selection, g, arrows) {
var previousPaths = selection
.selectAll('g.edgePath')
.data(g.edges(), function (e) {
return util.edgeToId(e);
})
.classed('update', true);
var newPaths = enter(previousPaths, g);
exit(previousPaths, g);
var svgPaths = previousPaths.merge !== undefined ? previousPaths.merge(newPaths) : previousPaths;
util.applyTransition(svgPaths, g).style('opacity', 1);
// Save DOM element in the path group, and set ID and class
svgPaths.each(function (e) {
var domEdge = d3.select(this);
var edge = g.edge(e);
edge.elem = this;
if (edge.id) {
domEdge.attr('id', edge.id);
}
util.applyClass(
domEdge,
edge['class'],
(domEdge.classed('update') ? 'update ' : '') + 'edgePath',
);
});
svgPaths.selectAll('path.path').each(function (e) {
var edge = g.edge(e);
edge.arrowheadId = _.uniqueId('arrowhead');
var domEdge = d3
.select(this)
.attr('marker-end', function () {
return 'url(' + makeFragmentRef(location.href, edge.arrowheadId) + ')';
})
.style('fill', 'none');
util.applyTransition(domEdge, g).attr('d', function (e) {
return calcPoints(g, e);
});
util.applyStyle(domEdge, edge.style);
});
svgPaths.selectAll('defs *').remove();
svgPaths.selectAll('defs').each(function (e) {
var edge = g.edge(e);
var arrowhead = arrows[edge.arrowhead];
arrowhead(d3.select(this), edge.arrowheadId, edge, 'arrowhead');
});
return svgPaths;
};
function setCreateEdgePaths(value) {
createEdgePaths = value;
}
function makeFragmentRef(url, fragmentId) {
var baseUrl = url.split('#')[0];
return baseUrl + '#' + fragmentId;
}
function calcPoints(g, e) {
var edge = g.edge(e);
var tail = g.node(e.v);
var head = g.node(e.w);
var points = edge.points.slice(1, edge.points.length - 1);
points.unshift(intersectNode(tail, points[0]));
points.push(intersectNode(head, points[points.length - 1]));
return createLine(edge, points);
}
function createLine(edge, points) {
// @ts-expect-error
var line = (d3.line || d3.svg.line)()
.x(function (d) {
return d.x;
})
.y(function (d) {
return d.y;
});
(line.curve || line.interpolate)(edge.curve);
return line(points);
}
function getCoords(elem) {
var bbox = elem.getBBox();
var matrix = elem.ownerSVGElement
.getScreenCTM()
.inverse()
.multiply(elem.getScreenCTM())
.translate(bbox.width / 2, bbox.height / 2);
return { x: matrix.e, y: matrix.f };
}
function enter(svgPaths, g) {
var svgPathsEnter = svgPaths.enter().append('g').attr('class', 'edgePath').style('opacity', 0);
svgPathsEnter
.append('path')
.attr('class', 'path')
.attr('d', function (e) {
var edge = g.edge(e);
var sourceElem = g.node(e.v).elem;
var points = _.range(edge.points.length).map(function () {
return getCoords(sourceElem);
});
return createLine(edge, points);
});
svgPathsEnter.append('defs');
return svgPathsEnter;
}
function exit(svgPaths, g) {
var svgPathExit = svgPaths.exit();
util.applyTransition(svgPathExit, g).style('opacity', 0).remove();
}
+2
View File
@@ -0,0 +1,2 @@
export function createNodes(selection: any, g: any, shapes: any): any;
export function setCreateNodes(value: any): void;
+92
View File
@@ -0,0 +1,92 @@
import * as d3 from 'd3';
import { pick } from 'lodash-es';
import { addLabel } from './label/add-label.js';
import * as util from './util.js';
export { createNodes, setCreateNodes };
var createNodes = function (selection, g, shapes) {
var simpleNodes = g.nodes().filter(function (v) {
return !util.isSubgraph(g, v);
});
var svgNodes = selection
.selectAll('g.node')
.data(simpleNodes, function (v) {
return v;
})
.classed('update', true);
svgNodes.exit().remove();
svgNodes.enter().append('g').attr('class', 'node').style('opacity', 0);
svgNodes = selection.selectAll('g.node');
svgNodes.each(function (v) {
var node = g.node(v);
var thisGroup = d3.select(this);
util.applyClass(
thisGroup,
node['class'],
(thisGroup.classed('update') ? 'update ' : '') + 'node',
);
thisGroup.select('g.label').remove();
var labelGroup = thisGroup.append('g').attr('class', 'label');
var labelDom = addLabel(labelGroup, node);
var shape = shapes[node.shape];
var bbox = pick(labelDom.node().getBBox(), 'width', 'height');
node.elem = this;
if (node.id) {
thisGroup.attr('id', node.id);
}
if (node.labelId) {
labelGroup.attr('id', node.labelId);
}
if (Object.prototype.hasOwnProperty.call(node, 'width')) {
bbox.width = node.width;
}
if (Object.prototype.hasOwnProperty.call(node, 'height')) {
bbox.height = node.height;
}
bbox.width += node.paddingLeft + node.paddingRight;
bbox.height += node.paddingTop + node.paddingBottom;
labelGroup.attr(
'transform',
'translate(' +
(node.paddingLeft - node.paddingRight) / 2 +
',' +
(node.paddingTop - node.paddingBottom) / 2 +
')',
);
var root = d3.select(this);
root.select('.label-container').remove();
var shapeSvg = shape(root, bbox, node).classed('label-container', true);
util.applyStyle(shapeSvg, node.style);
var shapeBBox = shapeSvg.node().getBBox();
node.width = shapeBBox.width;
node.height = shapeBBox.height;
});
var exitSelection;
if (svgNodes.exit) {
exitSelection = svgNodes.exit();
} else {
exitSelection = svgNodes.selectAll(null); // empty selection
}
util.applyTransition(exitSelection, g).style('opacity', 0).remove();
return svgNodes;
};
function setCreateNodes(value) {
createNodes = value;
}
+6
View File
@@ -0,0 +1,6 @@
import * as node from './intersect-node.js';
import * as circle from './intersect-circle.js';
import * as ellipse from './intersect-ellipse.js';
import * as polygon from './intersect-polygon.js';
import * as rect from './intersect-rect.js';
export { node, circle, ellipse, polygon, rect };
+7
View File
@@ -0,0 +1,7 @@
import * as node from './intersect-node.js';
import * as circle from './intersect-circle.js';
import * as ellipse from './intersect-ellipse.js';
import * as polygon from './intersect-polygon.js';
import * as rect from './intersect-rect.js';
export { node, circle, ellipse, polygon, rect };
@@ -0,0 +1,4 @@
export function intersectCircle(node: any, rx: any, point: any): {
x: any;
y: any;
};
@@ -0,0 +1,7 @@
import { intersectEllipse } from './intersect-ellipse.js';
export { intersectCircle };
function intersectCircle(node, rx, point) {
return intersectEllipse(node, rx, rx, point);
}
@@ -0,0 +1,4 @@
export function intersectEllipse(node: any, rx: any, ry: any, point: any): {
x: any;
y: any;
};
@@ -0,0 +1,24 @@
export { intersectEllipse };
function intersectEllipse(node, rx, ry, point) {
// Formulae from: http://mathworld.wolfram.com/Ellipse-LineIntersection.html
var cx = node.x;
var cy = node.y;
var px = cx - point.x;
var py = cy - point.y;
var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);
var dx = Math.abs((rx * ry * px) / det);
if (point.x < cx) {
dx = -dx;
}
var dy = Math.abs((rx * ry * py) / det);
if (point.y < cy) {
dy = -dy;
}
return { x: cx + dx, y: cy + dy };
}
@@ -0,0 +1,4 @@
export function intersectLine(p1: any, p2: any, q1: any, q2: any): {
x: number;
y: number;
};
@@ -0,0 +1,70 @@
export { intersectLine };
/*
* Returns the point at which two lines, p and q, intersect or returns
* undefined if they do not intersect.
*/
function intersectLine(p1, p2, q1, q2) {
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
// p7 and p473.
var a1, a2, b1, b2, c1, c2;
var r1, r2, r3, r4;
var denom, offset, num;
var x, y;
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
// b1 y + c1 = 0.
a1 = p2.y - p1.y;
b1 = p1.x - p2.x;
c1 = p2.x * p1.y - p1.x * p2.y;
// Compute r3 and r4.
r3 = a1 * q1.x + b1 * q1.y + c1;
r4 = a1 * q2.x + b1 * q2.y + c1;
// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
return /*DONT_INTERSECT*/;
}
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
a2 = q2.y - q1.y;
b2 = q1.x - q2.x;
c2 = q2.x * q1.y - q1.x * q2.y;
// Compute r1 and r2
r1 = a2 * p1.x + b2 * p1.y + c2;
r2 = a2 * p2.x + b2 * p2.y + c2;
// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
return /*DONT_INTERSECT*/;
}
// Line segments intersect: compute intersection point.
denom = a1 * b2 - a2 * b1;
if (denom === 0) {
return /*COLLINEAR*/;
}
offset = Math.abs(denom / 2);
// The denom/2 is to get rounding instead of truncating. It
// is added or subtracted to the numerator, depending upon the
// sign of the numerator.
num = b1 * c2 - b2 * c1;
x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
num = a2 * c1 - a1 * c2;
y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
return { x: x, y: y };
}
function sameSign(r1, r2) {
return r1 * r2 > 0;
}
@@ -0,0 +1 @@
export function intersectNode(node: any, point: any): any;
@@ -0,0 +1,5 @@
export { intersectNode };
function intersectNode(node, point) {
return node.intersect(point);
}
@@ -0,0 +1 @@
export function intersectPolygon(node: any, polyPoints: any, point: any): any;
@@ -0,0 +1,59 @@
import { intersectLine } from './intersect-line.js';
export { intersectPolygon };
/*
* Returns the point ({x, y}) at which the point argument intersects with the
* node argument assuming that it has the shape specified by polygon.
*/
function intersectPolygon(node, polyPoints, point) {
var x1 = node.x;
var y1 = node.y;
var intersections = [];
var minX = Number.POSITIVE_INFINITY;
var minY = Number.POSITIVE_INFINITY;
polyPoints.forEach(function (entry) {
minX = Math.min(minX, entry.x);
minY = Math.min(minY, entry.y);
});
var left = x1 - node.width / 2 - minX;
var top = y1 - node.height / 2 - minY;
for (var i = 0; i < polyPoints.length; i++) {
var p1 = polyPoints[i];
var p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
var intersect = intersectLine(
node,
point,
{ x: left + p1.x, y: top + p1.y },
{ x: left + p2.x, y: top + p2.y },
);
if (intersect) {
intersections.push(intersect);
}
}
if (!intersections.length) {
console.log('NO INTERSECTION FOUND, RETURN NODE CENTER', node);
return node;
}
if (intersections.length > 1) {
// More intersections, find the one nearest to edge end point
intersections.sort(function (p, q) {
var pdx = p.x - point.x;
var pdy = p.y - point.y;
var distp = Math.sqrt(pdx * pdx + pdy * pdy);
var qdx = q.x - point.x;
var qdy = q.y - point.y;
var distq = Math.sqrt(qdx * qdx + qdy * qdy);
return distp < distq ? -1 : distp === distq ? 0 : 1;
});
}
return intersections[0];
}
@@ -0,0 +1,4 @@
export function intersectRect(node: any, point: any): {
x: any;
y: any;
};
@@ -0,0 +1,32 @@
export { intersectRect };
function intersectRect(node, point) {
var x = node.x;
var y = node.y;
// Rectangle intersection algorithm from:
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
var dx = point.x - x;
var dy = point.y - y;
var w = node.width / 2;
var h = node.height / 2;
var sx, sy;
if (Math.abs(dy) * w > Math.abs(dx) * h) {
// Intersection is top or bottom of rect.
if (dy < 0) {
h = -h;
}
sx = dy === 0 ? 0 : (h * dx) / dy;
sy = h;
} else {
// Intersection is left or right of rect.
if (dx < 0) {
w = -w;
}
sx = w;
sy = dx === 0 ? 0 : (w * dy) / dx;
}
return { x: x + sx, y: y + sy };
}
@@ -0,0 +1 @@
export function addHtmlLabel(root: any, node: any): any;
+35
View File
@@ -0,0 +1,35 @@
import * as util from '../util.js';
export { addHtmlLabel };
function addHtmlLabel(root, node) {
var fo = root.append('foreignObject').attr('width', '100000');
var div = fo.append('xhtml:div');
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
var label = node.label;
switch (typeof label) {
case 'function':
div.insert(label);
break;
case 'object':
// Currently we assume this is a DOM object.
div.insert(function () {
return label;
});
break;
default:
div.html(label);
}
util.applyStyle(div, node.labelStyle);
div.style('display', 'inline-block');
// Fix for firefox
div.style('white-space', 'nowrap');
var client = div.node();
fo.attr('width', client.offsetWidth).attr('height', client.offsetHeight);
return fo;
}
+1
View File
@@ -0,0 +1 @@
export function addLabel(root: any, node: any, location: any): any;
+36
View File
@@ -0,0 +1,36 @@
import { addHtmlLabel } from './add-html-label.js';
import { addSVGLabel } from './add-svg-label.js';
import { addTextLabel } from './add-text-label.js';
export { addLabel };
function addLabel(root, node, location) {
var label = node.label;
var labelSvg = root.append('g');
// Allow the label to be a string, a function that returns a DOM element, or
// a DOM element itself.
if (node.labelType === 'svg') {
addSVGLabel(labelSvg, node);
} else if (typeof label !== 'string' || node.labelType === 'html') {
addHtmlLabel(labelSvg, node);
} else {
addTextLabel(labelSvg, node);
}
var labelBBox = labelSvg.node().getBBox();
var y;
switch (location) {
case 'top':
y = -node.height / 2;
break;
case 'bottom':
y = node.height / 2 - labelBBox.height;
break;
default:
y = -labelBBox.height / 2;
}
labelSvg.attr('transform', 'translate(' + -labelBBox.width / 2 + ',' + y + ')');
return labelSvg;
}
@@ -0,0 +1 @@
export function addSVGLabel(root: any, node: any): any;
+13
View File
@@ -0,0 +1,13 @@
import * as util from '../util.js';
export { addSVGLabel };
function addSVGLabel(root, node) {
var domNode = root;
domNode.node().appendChild(node.label);
util.applyStyle(domNode, node.labelStyle);
return domNode;
}
@@ -0,0 +1 @@
export function addTextLabel(root: any, node: any): any;
+48
View File
@@ -0,0 +1,48 @@
import * as util from '../util.js';
export { addTextLabel };
/*
* Attaches a text label to the specified root. Handles escape sequences.
*/
function addTextLabel(root, node) {
var domNode = root.append('text');
var lines = processEscapeSequences(node.label).split('\n');
for (var i = 0; i < lines.length; i++) {
domNode
.append('tspan')
.attr('xml:space', 'preserve')
.attr('dy', '1em')
.attr('x', '1')
.text(lines[i]);
}
util.applyStyle(domNode, node.labelStyle);
return domNode;
}
function processEscapeSequences(text) {
var newText = '';
var escaped = false;
var ch;
for (var i = 0; i < text.length; ++i) {
ch = text[i];
if (escaped) {
switch (ch) {
case 'n':
newText += '\n';
break;
default:
newText += ch;
}
escaped = false;
} else if (ch === '\\') {
escaped = true;
} else {
newText += ch;
}
}
return newText;
}
+1
View File
@@ -0,0 +1 @@
export function positionClusters(selection: any, g: any): void;
+36
View File
@@ -0,0 +1,36 @@
import * as d3 from 'd3';
import * as util from './util.js';
export { positionClusters };
function positionClusters(selection, g) {
var created = selection.filter(function () {
return !d3.select(this).classed('update');
});
function translate(v) {
var node = g.node(v);
return 'translate(' + node.x + ',' + node.y + ')';
}
created.attr('transform', translate);
util.applyTransition(selection, g).style('opacity', 1).attr('transform', translate);
util
.applyTransition(created.selectAll('rect'), g)
.attr('width', function (v) {
return g.node(v).width;
})
.attr('height', function (v) {
return g.node(v).height;
})
.attr('x', function (v) {
var node = g.node(v);
return -node.width / 2;
})
.attr('y', function (v) {
var node = g.node(v);
return -node.height / 2;
});
}
@@ -0,0 +1 @@
export function positionEdgeLabels(selection: any, g: any): void;
+21
View File
@@ -0,0 +1,21 @@
import * as d3 from 'd3';
import * as util from './util.js';
export { positionEdgeLabels };
function positionEdgeLabels(selection, g) {
var created = selection.filter(function () {
return !d3.select(this).classed('update');
});
function translate(e) {
var edge = g.edge(e);
return Object.prototype.hasOwnProperty.call(edge, 'x')
? 'translate(' + edge.x + ',' + edge.y + ')'
: '';
}
created.attr('transform', translate);
util.applyTransition(selection, g).style('opacity', 1).attr('transform', translate);
}
+1
View File
@@ -0,0 +1 @@
export function positionNodes(selection: any, g: any): void;
+19
View File
@@ -0,0 +1,19 @@
import * as d3 from 'd3';
import * as util from './util.js';
export { positionNodes };
function positionNodes(selection, g) {
var created = selection.filter(function () {
return !d3.select(this).classed('update');
});
function translate(v) {
var node = g.node(v);
return 'translate(' + node.x + ',' + node.y + ')';
}
created.attr('transform', translate);
util.applyTransition(selection, g).style('opacity', 1).attr('transform', translate);
}
+48
View File
@@ -0,0 +1,48 @@
export type Node = {
/**
* - The label of the node.
*/
label: string;
/**
* - The horizontal padding of the node.
*/
paddingX?: number;
/**
* - The vertical padding of the node.
*/
paddingY?: number;
/**
* - The padding of the node for all directions. Overrides `paddingX` and `paddingY`.
*/
padding?: number;
/**
* - The left padding of the node.
*/
paddingLeft?: number;
/**
* - The right padding of the node.
*/
paddingRight?: number;
_prevWidth?: number;
width?: number;
_prevHeight?: number;
height?: number;
};
export function render(): {
(svg: any, g: any): void;
createNodes(value: any, ...args: any[]): (selection: any, g: any, shapes: any) => any;
createClusters(value: any, ...args: any[]): (selection: any, g: any) => any;
createEdgeLabels(value: any, ...args: any[]): (selection: any, g: any) => any;
createEdgePaths(value: any, ...args: any[]): (selection: any, g: any, arrows: any) => any;
shapes(value: any, ...args: any[]): {
rect: (parent: any, bbox: any, node: any) => any;
ellipse: (parent: any, bbox: any, node: any) => any;
circle: (parent: any, bbox: any, node: any) => any;
diamond: (parent: any, bbox: any, node: any) => any;
} | /*elided*/ any;
arrows(value: any, ...args: any[]): {
normal: (parent: any, id: any, edge: any, type: any) => void;
vee: (parent: any, id: any, edge: any, type: any) => void;
undirected: (parent: any, id: any, edge: any, type: any) => void;
} | /*elided*/ any;
};
+195
View File
@@ -0,0 +1,195 @@
import * as d3 from 'd3';
import { defaults } from 'lodash-es';
import { layout } from '../dagre/index.js';
import { arrows, setArrows } from './arrows.js';
import { createClusters, setCreateClusters } from './create-clusters.js';
import { createEdgeLabels, setCreateEdgeLabels } from './create-edge-labels.js';
import { createEdgePaths, setCreateEdgePaths } from './create-edge-paths.js';
import { createNodes, setCreateNodes } from './create-nodes.js';
import { positionClusters } from './position-clusters.js';
import { positionEdgeLabels } from './position-edge-labels.js';
import { positionNodes } from './position-nodes.js';
import { shapes, setShapes } from './shapes.js';
export { render };
// This design is based on http://bost.ocks.org/mike/chart/.
function render() {
var fn = function (svg, g) {
preProcessGraph(g);
var outputGroup = createOrSelectGroup(svg, 'output');
var clustersGroup = createOrSelectGroup(outputGroup, 'clusters');
var edgePathsGroup = createOrSelectGroup(outputGroup, 'edgePaths');
var edgeLabels = createEdgeLabels(createOrSelectGroup(outputGroup, 'edgeLabels'), g);
var nodes = createNodes(createOrSelectGroup(outputGroup, 'nodes'), g, shapes);
layout(g);
positionNodes(nodes, g);
positionEdgeLabels(edgeLabels, g);
createEdgePaths(edgePathsGroup, g, arrows);
var clusters = createClusters(clustersGroup, g);
positionClusters(clusters, g);
postProcessGraph(g);
};
fn.createNodes = function (value) {
if (!arguments.length) return createNodes;
setCreateNodes(value);
return fn;
};
fn.createClusters = function (value) {
if (!arguments.length) return createClusters;
setCreateClusters(value);
return fn;
};
fn.createEdgeLabels = function (value) {
if (!arguments.length) return createEdgeLabels;
setCreateEdgeLabels(value);
return fn;
};
fn.createEdgePaths = function (value) {
if (!arguments.length) return createEdgePaths;
setCreateEdgePaths(value);
return fn;
};
fn.shapes = function (value) {
if (!arguments.length) return shapes;
setShapes(value);
return fn;
};
fn.arrows = function (value) {
if (!arguments.length) return arrows;
setArrows(value);
return fn;
};
return fn;
}
var NODE_DEFAULT_ATTRS = {
paddingLeft: 10,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
rx: 0,
ry: 0,
shape: 'rect',
};
var EDGE_DEFAULT_ATTRS = {
arrowhead: 'normal',
curve: d3.curveLinear,
};
/**
* @typedef {Object} Node
* @property {string} label - The label of the node.
* @property {number} [paddingX] - The horizontal padding of the node.
* @property {number} [paddingY] - The vertical padding of the node.
* @property {number} [padding] - The padding of the node for all directions. Overrides `paddingX` and `paddingY`.
* @property {number} [paddingLeft] - The left padding of the node.
* @property {number} [paddingRight] - The right padding of the node.
* @property {number} [_prevWidth]
* @property {number} [width]
* @property {number} [_prevHeight]
* @property {number} [height]
*/
/**
* Pre-processes the graph by setting default labels and padding for nodes.
* @param {Object} g - The graph object.
*/
function preProcessGraph(g) {
g.nodes().forEach((v) => {
/** @type {Node} */
const node = g.node(v);
if (!Object.prototype.hasOwnProperty.call(node, 'label') && !g.children(v).length) {
node.label = v;
}
if (Object.prototype.hasOwnProperty.call(node, 'paddingX')) {
defaults(node, {
paddingLeft: node.paddingX,
paddingRight: node.paddingX,
});
}
if (Object.prototype.hasOwnProperty.call(node, 'paddingY')) {
defaults(node, {
paddingTop: node.paddingY,
paddingBottom: node.paddingY,
});
}
if (Object.prototype.hasOwnProperty.call(node, 'padding')) {
defaults(node, {
paddingLeft: node.padding,
paddingRight: node.padding,
paddingTop: node.padding,
paddingBottom: node.padding,
});
}
defaults(node, NODE_DEFAULT_ATTRS);
['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom'].forEach((k) => {
node[k] = Number(node[k]);
});
// Save dimensions for restore during post-processing
if (Object.prototype.hasOwnProperty.call(node, 'width')) {
node._prevWidth = node.width;
}
if (Object.prototype.hasOwnProperty.call(node, 'height')) {
node._prevHeight = node.height;
}
});
g.edges().forEach(function (e) {
var edge = g.edge(e);
if (!Object.prototype.hasOwnProperty.call(edge, 'label')) {
edge.label = '';
}
defaults(edge, EDGE_DEFAULT_ATTRS);
});
}
function postProcessGraph(g) {
g.nodes().forEach((v) => {
/** @type {Node} */
var node = g.node(v);
// Restore original dimensions
if (Object.prototype.hasOwnProperty.call(node, '_prevWidth')) {
node.width = node._prevWidth;
} else {
delete node.width;
}
if (Object.prototype.hasOwnProperty.call(node, '_prevHeight')) {
node.height = node._prevHeight;
} else {
delete node.height;
}
delete node._prevWidth;
delete node._prevHeight;
});
}
function createOrSelectGroup(root, name) {
var selection = root.select('g.' + name);
if (selection.empty()) {
selection = root.append('g').attr('class', name);
}
return selection;
}
+12
View File
@@ -0,0 +1,12 @@
export namespace shapes {
export { rect };
export { ellipse };
export { circle };
export { diamond };
}
export function setShapes(value: any): void;
declare function rect(parent: any, bbox: any, node: any): any;
declare function ellipse(parent: any, bbox: any, node: any): any;
declare function circle(parent: any, bbox: any, node: any): any;
declare function diamond(parent: any, bbox: any, node: any): any;
export {};
+94
View File
@@ -0,0 +1,94 @@
import { intersectCircle } from './intersect/intersect-circle.js';
import { intersectEllipse } from './intersect/intersect-ellipse.js';
import { intersectPolygon } from './intersect/intersect-polygon.js';
import { intersectRect } from './intersect/intersect-rect.js';
export { shapes, setShapes };
var shapes = {
rect,
ellipse,
circle,
diamond,
};
function setShapes(value) {
shapes = value;
}
function rect(parent, bbox, node) {
var shapeSvg = parent
.insert('rect', ':first-child')
.attr('rx', node.rx)
.attr('ry', node.ry)
.attr('x', -bbox.width / 2)
.attr('y', -bbox.height / 2)
.attr('width', bbox.width)
.attr('height', bbox.height);
node.intersect = function (point) {
return intersectRect(node, point);
};
return shapeSvg;
}
function ellipse(parent, bbox, node) {
var rx = bbox.width / 2;
var ry = bbox.height / 2;
var shapeSvg = parent
.insert('ellipse', ':first-child')
.attr('x', -bbox.width / 2)
.attr('y', -bbox.height / 2)
.attr('rx', rx)
.attr('ry', ry);
node.intersect = function (point) {
return intersectEllipse(node, rx, ry, point);
};
return shapeSvg;
}
function circle(parent, bbox, node) {
var r = Math.max(bbox.width, bbox.height) / 2;
var shapeSvg = parent
.insert('circle', ':first-child')
.attr('x', -bbox.width / 2)
.attr('y', -bbox.height / 2)
.attr('r', r);
node.intersect = function (point) {
return intersectCircle(node, r, point);
};
return shapeSvg;
}
// Circumscribe an ellipse for the bounding box with a diamond shape. I derived
// the function to calculate the diamond shape from:
// http://mathforum.org/kb/message.jspa?messageID=3750236
function diamond(parent, bbox, node) {
var w = (bbox.width * Math.SQRT2) / 2;
var h = (bbox.height * Math.SQRT2) / 2;
var points = [
{ x: 0, y: -h },
{ x: -w, y: 0 },
{ x: 0, y: h },
{ x: w, y: 0 },
];
var shapeSvg = parent.insert('polygon', ':first-child').attr(
'points',
points
.map(function (p) {
return p.x + ',' + p.y;
})
.join(' '),
);
node.intersect = function (p) {
return intersectPolygon(node, points, p);
};
return shapeSvg;
}
+5
View File
@@ -0,0 +1,5 @@
export function isSubgraph(g: any, v: any): boolean;
export function edgeToId(e: any): string;
export function applyStyle(dom: any, styleFn: any): void;
export function applyClass(dom: any, classFn: any, otherClasses: any): void;
export function applyTransition(selection: any, g: any): any;
+46
View File
@@ -0,0 +1,46 @@
import * as _ from 'lodash-es';
// Public utility functions
export { isSubgraph, edgeToId, applyStyle, applyClass, applyTransition };
/*
* Returns true if the specified node in the graph is a subgraph node. A
* subgraph node is one that contains other nodes.
*/
function isSubgraph(g, v) {
return !!g.children(v).length;
}
function edgeToId(e) {
return escapeId(e.v) + ':' + escapeId(e.w) + ':' + escapeId(e.name);
}
var ID_DELIM = /:/g;
function escapeId(str) {
return str ? String(str).replace(ID_DELIM, '\\:') : '';
}
function applyStyle(dom, styleFn) {
if (styleFn) {
dom.attr('style', styleFn);
}
}
function applyClass(dom, classFn, otherClasses) {
if (classFn) {
dom.attr('class', classFn).attr('class', otherClasses + ' ' + dom.attr('class'));
}
}
function applyTransition(selection, g) {
var graph = g.graph();
if (_.isPlainObject(graph)) {
var transition = graph.transition;
if (_.isFunction(transition)) {
return transition(selection);
}
}
return selection;
}
+2
View File
@@ -0,0 +1,2 @@
export function run(g: any): void;
export function undo(g: any): void;
+60
View File
@@ -0,0 +1,60 @@
import * as _ from 'lodash-es';
import { greedyFAS } from './greedy-fas.js';
export { run, undo };
function run(g) {
var fas = g.graph().acyclicer === 'greedy' ? greedyFAS(g, weightFn(g)) : dfsFAS(g);
_.forEach(fas, function (e) {
var label = g.edge(e);
g.removeEdge(e);
label.forwardName = e.name;
label.reversed = true;
g.setEdge(e.w, e.v, label, _.uniqueId('rev'));
});
function weightFn(g) {
return function (e) {
return g.edge(e).weight;
};
}
}
function dfsFAS(g) {
var fas = [];
var stack = {};
var visited = {};
function dfs(v) {
if (Object.prototype.hasOwnProperty.call(visited, v)) {
return;
}
visited[v] = true;
stack[v] = true;
_.forEach(g.outEdges(v), function (e) {
if (Object.prototype.hasOwnProperty.call(stack, e.w)) {
fas.push(e);
} else {
dfs(e.w);
}
});
delete stack[v];
}
_.forEach(g.nodes(), dfs);
return fas;
}
function undo(g) {
_.forEach(g.edges(), function (e) {
var label = g.edge(e);
if (label.reversed) {
g.removeEdge(e);
var forwardName = label.forwardName;
delete label.reversed;
delete label.forwardName;
g.setEdge(e.w, e.v, label, forwardName);
}
});
}
+1
View File
@@ -0,0 +1 @@
export function addBorderSegments(g: any): void;
+36
View File
@@ -0,0 +1,36 @@
import * as _ from 'lodash-es';
import * as util from './util.js';
export { addBorderSegments };
function addBorderSegments(g) {
function dfs(v) {
var children = g.children(v);
var node = g.node(v);
if (children.length) {
_.forEach(children, dfs);
}
if (Object.prototype.hasOwnProperty.call(node, 'minRank')) {
node.borderLeft = [];
node.borderRight = [];
for (var rank = node.minRank, maxRank = node.maxRank + 1; rank < maxRank; ++rank) {
addBorderNode(g, 'borderLeft', '_bl', v, node, rank);
addBorderNode(g, 'borderRight', '_br', v, node, rank);
}
}
}
_.forEach(g.children(), dfs);
}
function addBorderNode(g, prop, prefix, sg, sgNode, rank) {
var label = { width: 0, height: 0, rank: rank, borderType: prop };
var prev = sgNode[prop][rank - 1];
var curr = util.addDummyNode(g, 'border', label, prefix);
sgNode[prop][rank] = curr;
g.setParent(curr, sg);
if (prev) {
g.setEdge(prev, curr, { weight: 1 });
}
}
+2
View File
@@ -0,0 +1,2 @@
export function adjust(g: any): void;
export function undo(g: any): void;
+75
View File
@@ -0,0 +1,75 @@
import * as _ from 'lodash-es';
export { adjust, undo };
function adjust(g) {
var rankDir = g.graph().rankdir.toLowerCase();
if (rankDir === 'lr' || rankDir === 'rl') {
swapWidthHeight(g);
}
}
function undo(g) {
var rankDir = g.graph().rankdir.toLowerCase();
if (rankDir === 'bt' || rankDir === 'rl') {
reverseY(g);
}
if (rankDir === 'lr' || rankDir === 'rl') {
swapXY(g);
swapWidthHeight(g);
}
}
function swapWidthHeight(g) {
_.forEach(g.nodes(), function (v) {
swapWidthHeightOne(g.node(v));
});
_.forEach(g.edges(), function (e) {
swapWidthHeightOne(g.edge(e));
});
}
function swapWidthHeightOne(attrs) {
var w = attrs.width;
attrs.width = attrs.height;
attrs.height = w;
}
function reverseY(g) {
_.forEach(g.nodes(), function (v) {
reverseYOne(g.node(v));
});
_.forEach(g.edges(), function (e) {
var edge = g.edge(e);
_.forEach(edge.points, reverseYOne);
if (Object.prototype.hasOwnProperty.call(edge, 'y')) {
reverseYOne(edge);
}
});
}
function reverseYOne(attrs) {
attrs.y = -attrs.y;
}
function swapXY(g) {
_.forEach(g.nodes(), function (v) {
swapXYOne(g.node(v));
});
_.forEach(g.edges(), function (e) {
var edge = g.edge(e);
_.forEach(edge.points, swapXYOne);
if (Object.prototype.hasOwnProperty.call(edge, 'x')) {
swapXYOne(edge);
}
});
}
function swapXYOne(attrs) {
var x = attrs.x;
attrs.x = attrs.y;
attrs.y = x;
}
+12
View File
@@ -0,0 +1,12 @@
export class List {
_sentinel: {
_next: /*elided*/ any;
_prev: /*elided*/ any;
};
dequeue(): {
_next: /*elided*/ any;
_prev: /*elided*/ any;
};
enqueue(entry: any): void;
toString(): string;
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Simple doubly linked list implementation derived from Cormen, et al.,
* "Introduction to Algorithms".
*/
export { List };
class List {
constructor() {
var sentinel = {};
sentinel._next = sentinel._prev = sentinel;
this._sentinel = sentinel;
}
dequeue() {
var sentinel = this._sentinel;
var entry = sentinel._prev;
if (entry !== sentinel) {
unlink(entry);
return entry;
}
}
enqueue(entry) {
var sentinel = this._sentinel;
if (entry._prev && entry._next) {
unlink(entry);
}
entry._next = sentinel._next;
sentinel._next._prev = entry;
sentinel._next = entry;
entry._prev = sentinel;
}
toString() {
var strs = [];
var sentinel = this._sentinel;
var curr = sentinel._prev;
while (curr !== sentinel) {
strs.push(JSON.stringify(curr, filterOutLinks));
curr = curr._prev;
}
return '[' + strs.join(', ') + ']';
}
}
function unlink(entry) {
entry._prev._next = entry._next;
entry._next._prev = entry._prev;
delete entry._next;
delete entry._prev;
}
function filterOutLinks(k, v) {
if (k !== '_next' && k !== '_prev') {
return v;
}
}
+2
View File
@@ -0,0 +1,2 @@
export function debugOrdering(g: any): Graph<any, any, any>;
import { Graph } from '../graphlib/index.js';
+32
View File
@@ -0,0 +1,32 @@
import * as _ from 'lodash-es';
import { Graph } from '../graphlib/index.js';
import * as util from './util.js';
export { debugOrdering };
/* istanbul ignore next */
function debugOrdering(g) {
var layerMatrix = util.buildLayerMatrix(g);
var h = new Graph({ compound: true, multigraph: true }).setGraph({});
_.forEach(g.nodes(), function (v) {
h.setNode(v, { label: v });
h.setParent(v, 'layer' + g.node(v).rank);
});
_.forEach(g.edges(), function (e) {
h.setEdge(e.v, e.w, {}, e.name);
});
_.forEach(layerMatrix, function (layer, i) {
var layerV = 'layer' + i;
h.setNode(layerV, { rank: 'same' });
_.reduce(layer, function (u, v) {
h.setEdge(u, v, { style: 'invis' });
return v;
});
});
return h;
}
+1
View File
@@ -0,0 +1 @@
export function greedyFAS(g: any, weightFn: any): any[];
+126
View File
@@ -0,0 +1,126 @@
import * as _ from 'lodash-es';
import { Graph } from '../graphlib/index.js';
import { List } from './data/list.js';
/*
* A greedy heuristic for finding a feedback arc set for a graph. A feedback
* arc set is a set of edges that can be removed to make a graph acyclic.
* The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and
* effective heuristic for the feedback arc set problem." This implementation
* adjusts that from the paper to allow for weighted edges.
*/
export { greedyFAS };
var DEFAULT_WEIGHT_FN = _.constant(1);
function greedyFAS(g, weightFn) {
if (g.nodeCount() <= 1) {
return [];
}
var state = buildState(g, weightFn || DEFAULT_WEIGHT_FN);
var results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx);
// Expand multi-edges
return _.flatten(
_.map(results, function (e) {
return g.outEdges(e.v, e.w);
}),
);
}
function doGreedyFAS(g, buckets, zeroIdx) {
var results = [];
var sources = buckets[buckets.length - 1];
var sinks = buckets[0];
var entry;
while (g.nodeCount()) {
while ((entry = sinks.dequeue())) {
removeNode(g, buckets, zeroIdx, entry);
}
while ((entry = sources.dequeue())) {
removeNode(g, buckets, zeroIdx, entry);
}
if (g.nodeCount()) {
for (var i = buckets.length - 2; i > 0; --i) {
entry = buckets[i].dequeue();
if (entry) {
results = results.concat(removeNode(g, buckets, zeroIdx, entry, true));
break;
}
}
}
}
return results;
}
function removeNode(g, buckets, zeroIdx, entry, collectPredecessors) {
var results = collectPredecessors ? [] : undefined;
_.forEach(g.inEdges(entry.v), function (edge) {
var weight = g.edge(edge);
var uEntry = g.node(edge.v);
if (collectPredecessors) {
results.push({ v: edge.v, w: edge.w });
}
uEntry.out -= weight;
assignBucket(buckets, zeroIdx, uEntry);
});
_.forEach(g.outEdges(entry.v), function (edge) {
var weight = g.edge(edge);
var w = edge.w;
var wEntry = g.node(w);
wEntry['in'] -= weight;
assignBucket(buckets, zeroIdx, wEntry);
});
g.removeNode(entry.v);
return results;
}
function buildState(g, weightFn) {
var fasGraph = new Graph();
var maxIn = 0;
var maxOut = 0;
_.forEach(g.nodes(), function (v) {
fasGraph.setNode(v, { v: v, in: 0, out: 0 });
});
// Aggregate weights on nodes, but also sum the weights across multi-edges
// into a single edge for the fasGraph.
_.forEach(g.edges(), function (e) {
var prevWeight = fasGraph.edge(e.v, e.w) || 0;
var weight = weightFn(e);
var edgeWeight = prevWeight + weight;
fasGraph.setEdge(e.v, e.w, edgeWeight);
maxOut = Math.max(maxOut, (fasGraph.node(e.v).out += weight));
maxIn = Math.max(maxIn, (fasGraph.node(e.w)['in'] += weight));
});
var buckets = _.range(maxOut + maxIn + 3).map(function () {
return new List();
});
var zeroIdx = maxIn + 1;
_.forEach(fasGraph.nodes(), function (v) {
assignBucket(buckets, zeroIdx, fasGraph.node(v));
});
return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx };
}
function assignBucket(buckets, zeroIdx, entry) {
if (!entry.out) {
buckets[0].enqueue(entry);
} else if (!entry['in']) {
buckets[buckets.length - 1].enqueue(entry);
} else {
buckets[entry.out - entry['in'] + zeroIdx].enqueue(entry);
}
}
+5
View File
@@ -0,0 +1,5 @@
import * as acyclic from './acyclic.js';
import * as normalize from './normalize.js';
import { rank } from './rank/index.js';
import { layout } from './layout.js';
export { acyclic, normalize, rank, layout };
+6
View File
@@ -0,0 +1,6 @@
import * as acyclic from './acyclic.js';
import { layout } from './layout.js';
import * as normalize from './normalize.js';
import { rank } from './rank/index.js';
export { acyclic, normalize, rank, layout };
+1
View File
@@ -0,0 +1 @@
export function layout(g: any, opts: any): void;
+406
View File
@@ -0,0 +1,406 @@
import * as _ from 'lodash-es';
import { Graph } from '../graphlib/index.js';
import { addBorderSegments } from './add-border-segments.js';
import * as coordinateSystem from './coordinate-system.js';
import * as acyclic from './acyclic.js';
import * as normalize from './normalize.js';
import { rank } from './rank/index.js';
import * as nestingGraph from './nesting-graph.js';
import { order } from './order/index.js';
import { parentDummyChains } from './parent-dummy-chains.js';
import { position } from './position/index.js';
import * as util from './util.js';
export { layout };
function layout(g, opts) {
var time = opts && opts.debugTiming ? util.time : util.notime;
time('layout', () => {
var layoutGraph = time(' buildLayoutGraph', () => buildLayoutGraph(g));
time(' runLayout', () => runLayout(layoutGraph, time));
time(' updateInputGraph', () => updateInputGraph(g, layoutGraph));
});
}
function runLayout(g, time) {
time(' makeSpaceForEdgeLabels', () => makeSpaceForEdgeLabels(g));
time(' removeSelfEdges', () => removeSelfEdges(g));
time(' acyclic', () => acyclic.run(g));
time(' nestingGraph.run', () => nestingGraph.run(g));
time(' rank', () => rank(util.asNonCompoundGraph(g)));
time(' injectEdgeLabelProxies', () => injectEdgeLabelProxies(g));
time(' removeEmptyRanks', () => util.removeEmptyRanks(g));
time(' nestingGraph.cleanup', () => nestingGraph.cleanup(g));
time(' normalizeRanks', () => util.normalizeRanks(g));
time(' assignRankMinMax', () => assignRankMinMax(g));
time(' removeEdgeLabelProxies', () => removeEdgeLabelProxies(g));
time(' normalize.run', () => normalize.run(g));
time(' parentDummyChains', () => parentDummyChains(g));
time(' addBorderSegments', () => addBorderSegments(g));
time(' order', () => order(g));
time(' insertSelfEdges', () => insertSelfEdges(g));
time(' adjustCoordinateSystem', () => coordinateSystem.adjust(g));
time(' position', () => position(g));
time(' positionSelfEdges', () => positionSelfEdges(g));
time(' removeBorderNodes', () => removeBorderNodes(g));
time(' normalize.undo', () => normalize.undo(g));
time(' fixupEdgeLabelCoords', () => fixupEdgeLabelCoords(g));
time(' undoCoordinateSystem', () => coordinateSystem.undo(g));
time(' translateGraph', () => translateGraph(g));
time(' assignNodeIntersects', () => assignNodeIntersects(g));
time(' reversePoints', () => reversePointsForReversedEdges(g));
time(' acyclic.undo', () => 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) {
var inputLabel = inputGraph.node(v);
var 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) {
var inputLabel = inputGraph.edge(e);
var layoutLabel = layoutGraph.edge(e);
inputLabel.points = layoutLabel.points;
if (Object.prototype.hasOwnProperty.call(layoutLabel, 'x')) {
inputLabel.x = layoutLabel.x;
inputLabel.y = layoutLabel.y;
}
});
inputGraph.graph().width = layoutGraph.graph().width;
inputGraph.graph().height = layoutGraph.graph().height;
}
var graphNumAttrs = ['nodesep', 'edgesep', 'ranksep', 'marginx', 'marginy'];
var graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: 'tb' };
var graphAttrs = ['acyclicer', 'ranker', 'rankdir', 'align'];
var nodeNumAttrs = ['width', 'height'];
var nodeDefaults = { width: 0, height: 0 };
var edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset'];
var edgeDefaults = {
minlen: 1,
weight: 1,
width: 0,
height: 0,
labeloffset: 10,
labelpos: 'r',
};
var 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) {
var g = new Graph({ multigraph: true, compound: true });
var graph = canonicalize(inputGraph.graph());
g.setGraph(
_.merge({}, graphDefaults, selectNumberAttrs(graph, graphNumAttrs), _.pick(graph, graphAttrs)),
);
_.forEach(inputGraph.nodes(), function (v) {
var node = canonicalize(inputGraph.node(v));
g.setNode(v, _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults));
g.setParent(v, inputGraph.parent(v));
});
_.forEach(inputGraph.edges(), function (e) {
var 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) {
var graph = g.graph();
graph.ranksep /= 2;
_.forEach(g.edges(), function (e) {
var 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) {
var edge = g.edge(e);
if (edge.width && edge.height) {
var v = g.node(e.v);
var w = g.node(e.w);
var label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e };
util.addDummyNode(g, 'edge-proxy', label, '_ep');
}
});
}
function assignRankMinMax(g) {
var maxRank = 0;
_.forEach(g.nodes(), function (v) {
var node = g.node(v);
if (node.borderTop) {
node.minRank = g.node(node.borderTop).rank;
node.maxRank = g.node(node.borderBottom).rank;
// @ts-expect-error
maxRank = _.max(maxRank, node.maxRank);
}
});
g.graph().maxRank = maxRank;
}
function removeEdgeLabelProxies(g) {
_.forEach(g.nodes(), function (v) {
var node = g.node(v);
if (node.dummy === 'edge-proxy') {
g.edge(node.e).labelRank = node.rank;
g.removeNode(v);
}
});
}
function translateGraph(g) {
var minX = Number.POSITIVE_INFINITY;
var maxX = 0;
var minY = Number.POSITIVE_INFINITY;
var maxY = 0;
var graphLabel = g.graph();
var marginX = graphLabel.marginx || 0;
var marginY = graphLabel.marginy || 0;
function getExtremes(attrs) {
var x = attrs.x;
var y = attrs.y;
var w = attrs.width;
var 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) {
var edge = g.edge(e);
if (Object.prototype.hasOwnProperty.call(edge, 'x')) {
getExtremes(edge);
}
});
minX -= marginX;
minY -= marginY;
_.forEach(g.nodes(), function (v) {
var node = g.node(v);
node.x -= minX;
node.y -= minY;
});
_.forEach(g.edges(), function (e) {
var edge = g.edge(e);
_.forEach(edge.points, function (p) {
p.x -= minX;
p.y -= minY;
});
if (Object.prototype.hasOwnProperty.call(edge, 'x')) {
edge.x -= minX;
}
if (Object.prototype.hasOwnProperty.call(edge, 'y')) {
edge.y -= minY;
}
});
graphLabel.width = maxX - minX + marginX;
graphLabel.height = maxY - minY + marginY;
}
function assignNodeIntersects(g) {
_.forEach(g.edges(), function (e) {
var edge = g.edge(e);
var nodeV = g.node(e.v);
var nodeW = g.node(e.w);
var p1, p2;
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) {
var edge = g.edge(e);
if (Object.prototype.hasOwnProperty.call(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) {
var edge = g.edge(e);
if (edge.reversed) {
edge.points.reverse();
}
});
}
function removeBorderNodes(g) {
_.forEach(g.nodes(), function (v) {
if (g.children(v).length) {
var node = g.node(v);
var t = g.node(node.borderTop);
var b = g.node(node.borderBottom);
var l = g.node(_.last(node.borderLeft));
var 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) {
var 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) {
var layers = util.buildLayerMatrix(g);
_.forEach(layers, function (layer) {
var orderShift = 0;
_.forEach(layer, function (v, i) {
var 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) {
var node = g.node(v);
if (node.dummy === 'selfedge') {
var selfNode = g.node(node.e.v);
var x = selfNode.x + selfNode.width / 2;
var y = selfNode.y;
var dx = node.x - x;
var 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) {
var newAttrs = {};
_.forEach(attrs, function (v, k) {
newAttrs[k.toLowerCase()] = v;
});
return newAttrs;
}
+2
View File
@@ -0,0 +1,2 @@
export function run(g: any): void;
export function cleanup(g: any): void;
+137
View File
@@ -0,0 +1,137 @@
import * as _ from 'lodash-es';
import * as util from './util.js';
export { run, cleanup };
/*
* A nesting graph creates dummy nodes for the tops and bottoms of subgraphs,
* adds appropriate edges to ensure that all cluster nodes are placed between
* these boundries, and ensures that the graph is connected.
*
* In addition we ensure, through the use of the minlen property, that nodes
* and subgraph border nodes to not end up on the same rank.
*
* Preconditions:
*
* 1. Input graph is a DAG
* 2. Nodes in the input graph has a minlen attribute
*
* Postconditions:
*
* 1. Input graph is connected.
* 2. Dummy nodes are added for the tops and bottoms of subgraphs.
* 3. The minlen attribute for nodes is adjusted to ensure nodes do not
* get placed on the same rank as subgraph border nodes.
*
* The nesting graph idea comes from Sander, "Layout of Compound Directed
* Graphs."
*/
function run(g) {
var root = util.addDummyNode(g, 'root', {}, '_root');
var depths = treeDepths(g);
var height = _.max(_.values(depths)) - 1; // Note: depths is an Object not an array
var nodeSep = 2 * height + 1;
g.graph().nestingRoot = root;
// Multiply minlen by nodeSep to align nodes on non-border ranks.
_.forEach(g.edges(), function (e) {
g.edge(e).minlen *= nodeSep;
});
// Calculate a weight that is sufficient to keep subgraphs vertically compact
var weight = sumWeights(g) + 1;
// Create border nodes and link them up
_.forEach(g.children(), function (child) {
dfs(g, root, nodeSep, weight, height, depths, child);
});
// Save the multiplier for node layers for later removal of empty border
// layers.
g.graph().nodeRankFactor = nodeSep;
}
function dfs(g, root, nodeSep, weight, height, depths, v) {
var children = g.children(v);
if (!children.length) {
if (v !== root) {
g.setEdge(root, v, { weight: 0, minlen: nodeSep });
}
return;
}
var top = util.addBorderNode(g, '_bt');
var bottom = util.addBorderNode(g, '_bb');
var label = g.node(v);
g.setParent(top, v);
label.borderTop = top;
g.setParent(bottom, v);
label.borderBottom = bottom;
_.forEach(children, function (child) {
dfs(g, root, nodeSep, weight, height, depths, child);
var childNode = g.node(child);
var childTop = childNode.borderTop ? childNode.borderTop : child;
var childBottom = childNode.borderBottom ? childNode.borderBottom : child;
var thisWeight = childNode.borderTop ? weight : 2 * weight;
var minlen = childTop !== childBottom ? 1 : height - depths[v] + 1;
g.setEdge(top, childTop, {
weight: thisWeight,
minlen: minlen,
nestingEdge: true,
});
g.setEdge(childBottom, bottom, {
weight: thisWeight,
minlen: minlen,
nestingEdge: true,
});
});
if (!g.parent(v)) {
g.setEdge(root, top, { weight: 0, minlen: height + depths[v] });
}
}
function treeDepths(g) {
var depths = {};
function dfs(v, depth) {
var children = g.children(v);
if (children && children.length) {
_.forEach(children, function (child) {
dfs(child, depth + 1);
});
}
depths[v] = depth;
}
_.forEach(g.children(), function (v) {
dfs(v, 1);
});
return depths;
}
function sumWeights(g) {
return _.reduce(
g.edges(),
function (acc, e) {
return acc + g.edge(e).weight;
},
0,
);
}
function cleanup(g) {
var graphLabel = g.graph();
g.removeNode(graphLabel.nestingRoot);
delete graphLabel.nestingRoot;
_.forEach(g.edges(), function (e) {
var edge = g.edge(e);
if (edge.nestingEdge) {
g.removeEdge(e);
}
});
}
+2
View File
@@ -0,0 +1,2 @@
export function run(g: any): void;
export function undo(g: any): void;
+110
View File
@@ -0,0 +1,110 @@
/**
* TypeScript type imports:
*
* @import { Graph } from '../graphlib/graph.js';
*/
import * as _ from 'lodash-es';
import * as util from './util.js';
export { run, undo };
/*
* Breaks any long edges in the graph into short segments that span 1 layer
* each. This operation is undoable with the denormalize function.
*
* Pre-conditions:
*
* 1. The input graph is a DAG.
* 2. Each node in the graph has a "rank" property.
*
* Post-condition:
*
* 1. All edges in the graph have a length of 1.
* 2. Dummy nodes are added where edges have been split into segments.
* 3. The graph is augmented with a "dummyChains" attribute which contains
* the first dummy in each chain of dummy nodes produced.
*/
function run(g) {
g.graph().dummyChains = [];
_.forEach(g.edges(), function (edge) {
normalizeEdge(g, edge);
});
}
/**
* @param {Graph} g
*/
function normalizeEdge(g, e) {
var v = e.v;
var vRank = g.node(v).rank;
var w = e.w;
var wRank = g.node(w).rank;
var name = e.name;
var edgeLabel = g.edge(e);
var labelRank = edgeLabel.labelRank;
if (wRank === vRank + 1) return;
g.removeEdge(e);
/**
* @typedef {Object} Attrs
* @property {number} width
* @property {number} height
* @property {ReturnType<Graph["node"]>} edgeLabel
* @property {any} edgeObj
* @property {ReturnType<Graph["node"]>["rank"]} rank
* @property {string} [dummy]
* @property {ReturnType<Graph["node"]>["labelpos"]} [labelpos]
*/
/** @type {Attrs | undefined} */
var attrs = undefined;
var dummy, i;
for (i = 0, ++vRank; vRank < wRank; ++i, ++vRank) {
edgeLabel.points = [];
attrs = {
width: 0,
height: 0,
edgeLabel: edgeLabel,
edgeObj: e,
rank: vRank,
};
dummy = util.addDummyNode(g, 'edge', attrs, '_d');
if (vRank === labelRank) {
attrs.width = edgeLabel.width;
attrs.height = edgeLabel.height;
attrs.dummy = 'edge-label';
attrs.labelpos = edgeLabel.labelpos;
}
g.setEdge(v, dummy, { weight: edgeLabel.weight }, name);
if (i === 0) {
g.graph().dummyChains.push(dummy);
}
v = dummy;
}
g.setEdge(v, w, { weight: edgeLabel.weight }, name);
}
function undo(g) {
_.forEach(g.graph().dummyChains, function (v) {
var node = g.node(v);
var origLabel = node.edgeLabel;
var w;
g.setEdge(node.edgeObj, origLabel);
while (node.dummy) {
w = g.successors(v)[0];
g.removeNode(v);
origLabel.points.push({ x: node.x, y: node.y });
if (node.dummy === 'edge-label') {
origLabel.x = node.x;
origLabel.y = node.y;
origLabel.width = node.width;
origLabel.height = node.height;
}
v = w;
node = g.node(v);
}
});
}
@@ -0,0 +1 @@
export function addSubgraphConstraints(g: any, cg: any, vs: any): void;
@@ -0,0 +1,53 @@
import * as _ from 'lodash-es';
export { addSubgraphConstraints };
function addSubgraphConstraints(g, cg, vs) {
var prev = {},
rootPrev;
_.forEach(vs, function (v) {
var child = g.parent(v),
parent,
prevChild;
while (child) {
parent = g.parent(child);
if (parent) {
prevChild = prev[parent];
prev[parent] = child;
} else {
prevChild = rootPrev;
rootPrev = child;
}
if (prevChild && prevChild !== child) {
cg.setEdge(prevChild, child);
return;
}
child = parent;
}
});
/*
function dfs(v) {
var children = v ? g.children(v) : g.children();
if (children.length) {
var min = Number.POSITIVE_INFINITY,
subgraphs = [];
_.each(children, function(child) {
var childMin = dfs(child);
if (g.children(child).length) {
subgraphs.push({ v: child, order: childMin });
}
min = Math.min(min, childMin);
});
_.reduce(_.sortBy(subgraphs, "order"), function(prev, curr) {
cg.setEdge(prev.v, curr.v);
return curr;
});
return min;
}
return g.node(v).order;
}
dfs(undefined);
*/
}
+9
View File
@@ -0,0 +1,9 @@
export function barycenter(g: any, movable: any): ({
v: any;
barycenter?: undefined;
weight?: undefined;
} | {
v: any;
barycenter: number;
weight: any;
})[];
+31
View File
@@ -0,0 +1,31 @@
import * as _ from 'lodash-es';
export { barycenter };
function barycenter(g, movable) {
return _.map(movable, function (v) {
var inV = g.inEdges(v);
if (!inV.length) {
return { v: v };
} else {
var result = _.reduce(
inV,
function (acc, e) {
var edge = g.edge(e),
nodeU = g.node(e.v);
return {
sum: acc.sum + edge.weight * nodeU.order,
weight: acc.weight + edge.weight,
};
},
{ sum: 0, weight: 0 },
);
return {
v: v,
barycenter: result.sum / result.weight,
weight: result.weight,
};
}
});
}
@@ -0,0 +1,2 @@
export function buildLayerGraph(g: any, rank: any, relationship: any): Graph<any, any, any>;
import { Graph } from '../../graphlib/index.js';
+76
View File
@@ -0,0 +1,76 @@
import * as _ from 'lodash-es';
import { Graph } from '../../graphlib/index.js';
export { buildLayerGraph };
/*
* Constructs a graph that can be used to sort a layer of nodes. The graph will
* contain all base and subgraph nodes from the request layer in their original
* hierarchy and any edges that are incident on these nodes and are of the type
* requested by the "relationship" parameter.
*
* Nodes from the requested rank that do not have parents are assigned a root
* node in the output graph, which is set in the root graph attribute. This
* makes it easy to walk the hierarchy of movable nodes during ordering.
*
* Pre-conditions:
*
* 1. Input graph is a DAG
* 2. Base nodes in the input graph have a rank attribute
* 3. Subgraph nodes in the input graph has minRank and maxRank attributes
* 4. Edges have an assigned weight
*
* Post-conditions:
*
* 1. Output graph has all nodes in the movable rank with preserved
* hierarchy.
* 2. Root nodes in the movable layer are made children of the node
* indicated by the root attribute of the graph.
* 3. Non-movable nodes incident on movable nodes, selected by the
* relationship parameter, are included in the graph (without hierarchy).
* 4. Edges incident on movable nodes, selected by the relationship
* parameter, are added to the output graph.
* 5. The weights for copied edges are aggregated as need, since the output
* graph is not a multi-graph.
*/
function buildLayerGraph(g, rank, relationship) {
var root = createRootNode(g),
result = new Graph({ compound: true })
.setGraph({ root: root })
.setDefaultNodeLabel(function (v) {
return g.node(v);
});
_.forEach(g.nodes(), function (v) {
var node = g.node(v),
parent = g.parent(v);
if (node.rank === rank || (node.minRank <= rank && rank <= node.maxRank)) {
result.setNode(v);
result.setParent(v, parent || root);
// This assumes we have only short edges!
_.forEach(g[relationship](v), function (e) {
var u = e.v === v ? e.w : e.v,
edge = result.edge(u, v),
weight = !_.isUndefined(edge) ? edge.weight : 0;
result.setEdge(u, v, { weight: g.edge(e).weight + weight });
});
if (Object.prototype.hasOwnProperty.call(node, 'minRank')) {
result.setNode(v, {
borderLeft: node.borderLeft[rank],
borderRight: node.borderRight[rank],
});
}
}
});
return result;
}
function createRootNode(g) {
var v;
while (g.hasNode((v = _.uniqueId('_root'))));
return v;
}
+1
View File
@@ -0,0 +1 @@
export function crossCount(g: any, layering: any): number;
+79
View File
@@ -0,0 +1,79 @@
import * as _ from 'lodash-es';
export { crossCount };
/*
* A function that takes a layering (an array of layers, each with an array of
* ordererd nodes) and a graph and returns a weighted crossing count.
*
* Pre-conditions:
*
* 1. Input graph must be simple (not a multigraph), directed, and include
* only simple edges.
* 2. Edges in the input graph must have assigned weights.
*
* Post-conditions:
*
* 1. The graph and layering matrix are left unchanged.
*
* This algorithm is derived from Barth, et al., "Bilayer Cross Counting."
*/
function crossCount(g, layering) {
var cc = 0;
for (var i = 1; i < layering.length; ++i) {
cc += twoLayerCrossCount(g, layering[i - 1], layering[i]);
}
return cc;
}
function twoLayerCrossCount(g, northLayer, southLayer) {
// Sort all of the edges between the north and south layers by their position
// in the north layer and then the south. Map these edges to the position of
// their head in the south layer.
var southPos = _.zipObject(
southLayer,
_.map(southLayer, function (v, i) {
return i;
}),
);
var southEntries = _.flatten(
_.map(northLayer, function (v) {
return _.sortBy(
_.map(g.outEdges(v), function (e) {
return { pos: southPos[e.w], weight: g.edge(e).weight };
}),
'pos',
);
}),
);
// Build the accumulator tree
var firstIndex = 1;
while (firstIndex < southLayer.length) firstIndex <<= 1;
var treeSize = 2 * firstIndex - 1;
firstIndex -= 1;
var tree = _.map(new Array(treeSize), function () {
return 0;
});
// Calculate the weighted crossings
var cc = 0;
_.forEach(
// @ts-expect-error
southEntries.forEach(function (entry) {
var index = entry.pos + firstIndex;
tree[index] += entry.weight;
var weightSum = 0;
while (index > 0) {
if (index % 2) {
weightSum += tree[index + 1];
}
index = (index - 1) >> 1;
tree[index] += entry.weight;
}
cc += entry.weight * weightSum;
}),
);
return cc;
}
+1
View File
@@ -0,0 +1 @@
export function order(g: any): void;
+77
View File
@@ -0,0 +1,77 @@
import * as _ from 'lodash-es';
import { Graph } from '../../graphlib/index.js';
import * as util from '../util.js';
import { addSubgraphConstraints } from './add-subgraph-constraints.js';
import { buildLayerGraph } from './build-layer-graph.js';
import { crossCount } from './cross-count.js';
import { initOrder } from './init-order.js';
import { sortSubgraph } from './sort-subgraph.js';
export { order };
/*
* Applies heuristics to minimize edge crossings in the graph and sets the best
* order solution as an order attribute on each node.
*
* Pre-conditions:
*
* 1. Graph must be DAG
* 2. Graph nodes must be objects with a "rank" attribute
* 3. Graph edges must have the "weight" attribute
*
* Post-conditions:
*
* 1. Graph nodes will have an "order" attribute based on the results of the
* algorithm.
*/
function order(g) {
var maxRank = util.maxRank(g),
downLayerGraphs = buildLayerGraphs(g, _.range(1, maxRank + 1), 'inEdges'),
upLayerGraphs = buildLayerGraphs(g, _.range(maxRank - 1, -1, -1), 'outEdges');
var layering = initOrder(g);
assignOrder(g, layering);
var bestCC = Number.POSITIVE_INFINITY,
best;
for (var i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) {
sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2);
layering = util.buildLayerMatrix(g);
var cc = crossCount(g, layering);
if (cc < bestCC) {
lastBest = 0;
best = _.cloneDeep(layering);
bestCC = cc;
}
}
assignOrder(g, best);
}
function buildLayerGraphs(g, ranks, relationship) {
return _.map(ranks, function (rank) {
return buildLayerGraph(g, rank, relationship);
});
}
function sweepLayerGraphs(layerGraphs, biasRight) {
var cg = new Graph();
_.forEach(layerGraphs, function (lg) {
var root = lg.graph().root;
var sorted = sortSubgraph(lg, root, cg, biasRight);
_.forEach(sorted.vs, function (v, i) {
lg.node(v).order = i;
});
addSubgraphConstraints(lg, cg, sorted.vs);
});
}
function assignOrder(g, layering) {
_.forEach(layering, function (layer) {
_.forEach(layer, function (v, i) {
g.node(v).order = i;
});
});
}
+1
View File
@@ -0,0 +1 @@
export function initOrder(g: any): any[][];
+42
View File
@@ -0,0 +1,42 @@
import * as _ from 'lodash-es';
/*
* Assigns an initial order value for each node by performing a DFS search
* starting from nodes in the first rank. Nodes are assigned an order in their
* rank as they are first visited.
*
* This approach comes from Gansner, et al., "A Technique for Drawing Directed
* Graphs."
*
* Returns a layering matrix with an array per layer and each layer sorted by
* the order of its nodes.
*/
export function initOrder(g) {
var visited = {};
var simpleNodes = _.filter(g.nodes(), function (v) {
return !g.children(v).length;
});
var maxRank = _.max(
_.map(simpleNodes, function (v) {
return g.node(v).rank;
}),
);
var layers = _.map(_.range(maxRank + 1), function () {
return [];
});
function dfs(v) {
if (_.has(visited, v)) return;
visited[v] = true;
var node = g.node(v);
layers[node.rank].push(v);
_.forEach(g.successors(v), dfs);
}
var orderedVs = _.sortBy(simpleNodes, function (v) {
return g.node(v).rank;
});
_.forEach(orderedVs, dfs);
return layers;
}
@@ -0,0 +1 @@
export function resolveConflicts(entries: any, cg: any): Pick<any, "weight" | "barycenter" | "vs" | "i">[];
+128
View File
@@ -0,0 +1,128 @@
import * as _ from 'lodash-es';
export { resolveConflicts };
/*
* Given a list of entries of the form {v, barycenter, weight} and a
* constraint graph this function will resolve any conflicts between the
* constraint graph and the barycenters for the entries. If the barycenters for
* an entry would violate a constraint in the constraint graph then we coalesce
* the nodes in the conflict into a new node that respects the contraint and
* aggregates barycenter and weight information.
*
* This implementation is based on the description in Forster, "A Fast and
* Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it
* differs in some specific details.
*
* Pre-conditions:
*
* 1. Each entry has the form {v, barycenter, weight}, or if the node has
* no barycenter, then {v}.
*
* Returns:
*
* A new list of entries of the form {vs, i, barycenter, weight}. The list
* `vs` may either be a singleton or it may be an aggregation of nodes
* ordered such that they do not violate constraints from the constraint
* graph. The property `i` is the lowest original index of any of the
* elements in `vs`.
*/
function resolveConflicts(entries, cg) {
var mappedEntries = {};
_.forEach(entries, function (entry, i) {
var tmp = (mappedEntries[entry.v] = {
indegree: 0,
in: [],
out: [],
vs: [entry.v],
i: i,
});
if (!_.isUndefined(entry.barycenter)) {
// @ts-expect-error
tmp.barycenter = entry.barycenter;
// @ts-expect-error
tmp.weight = entry.weight;
}
});
_.forEach(cg.edges(), function (e) {
var entryV = mappedEntries[e.v];
var entryW = mappedEntries[e.w];
if (!_.isUndefined(entryV) && !_.isUndefined(entryW)) {
entryW.indegree++;
entryV.out.push(mappedEntries[e.w]);
}
});
var sourceSet = _.filter(mappedEntries, function (entry) {
// @ts-expect-error
return !entry.indegree;
});
return doResolveConflicts(sourceSet);
}
function doResolveConflicts(sourceSet) {
var entries = [];
function handleIn(vEntry) {
return function (uEntry) {
if (uEntry.merged) {
return;
}
if (
_.isUndefined(uEntry.barycenter) ||
_.isUndefined(vEntry.barycenter) ||
uEntry.barycenter >= vEntry.barycenter
) {
mergeEntries(vEntry, uEntry);
}
};
}
function handleOut(vEntry) {
return function (wEntry) {
wEntry['in'].push(vEntry);
if (--wEntry.indegree === 0) {
sourceSet.push(wEntry);
}
};
}
while (sourceSet.length) {
var entry = sourceSet.pop();
entries.push(entry);
_.forEach(entry['in'].reverse(), handleIn(entry));
_.forEach(entry.out, handleOut(entry));
}
return _.map(
_.filter(entries, function (entry) {
return !entry.merged;
}),
function (entry) {
return _.pick(entry, ['vs', 'i', 'barycenter', 'weight']);
},
);
}
function mergeEntries(target, source) {
var sum = 0;
var weight = 0;
if (target.weight) {
sum += target.barycenter * target.weight;
weight += target.weight;
}
if (source.weight) {
sum += source.barycenter * source.weight;
weight += source.weight;
}
target.vs = source.vs.concat(target.vs);
target.barycenter = sum / weight;
target.weight = weight;
target.i = Math.min(source.i, target.i);
source.merged = true;
}
+3
View File
@@ -0,0 +1,3 @@
export function sortSubgraph(g: any, v: any, cg: any, biasRight: any): {
vs: any[];
};
+78
View File
@@ -0,0 +1,78 @@
import * as _ from 'lodash-es';
import { barycenter } from './barycenter.js';
import { resolveConflicts } from './resolve-conflicts.js';
import { sort } from './sort.js';
export { sortSubgraph };
function sortSubgraph(g, v, cg, biasRight) {
var movable = g.children(v);
var node = g.node(v);
var bl = node ? node.borderLeft : undefined;
var br = node ? node.borderRight : undefined;
var subgraphs = {};
if (bl) {
movable = _.filter(movable, function (w) {
return w !== bl && w !== br;
});
}
var barycenters = barycenter(g, movable);
_.forEach(barycenters, function (entry) {
if (g.children(entry.v).length) {
var subgraphResult = sortSubgraph(g, entry.v, cg, biasRight);
subgraphs[entry.v] = subgraphResult;
if (Object.prototype.hasOwnProperty.call(subgraphResult, 'barycenter')) {
mergeBarycenters(entry, subgraphResult);
}
}
});
var entries = resolveConflicts(barycenters, cg);
expandSubgraphs(entries, subgraphs);
var result = sort(entries, biasRight);
if (bl) {
result.vs = _.flatten([bl, result.vs, br]);
if (g.predecessors(bl).length) {
var blPred = g.node(g.predecessors(bl)[0]),
brPred = g.node(g.predecessors(br)[0]);
if (!Object.prototype.hasOwnProperty.call(result, 'barycenter')) {
result.barycenter = 0;
result.weight = 0;
}
result.barycenter =
(result.barycenter * result.weight + blPred.order + brPred.order) / (result.weight + 2);
result.weight += 2;
}
}
return result;
}
function expandSubgraphs(entries, subgraphs) {
_.forEach(entries, function (entry) {
entry.vs = _.flatten(
entry.vs.map(function (v) {
if (subgraphs[v]) {
return subgraphs[v].vs;
}
return v;
}),
);
});
}
function mergeBarycenters(target, other) {
if (!_.isUndefined(target.barycenter)) {
target.barycenter =
(target.barycenter * target.weight + other.barycenter * other.weight) /
(target.weight + other.weight);
target.weight += other.weight;
} else {
target.barycenter = other.barycenter;
target.weight = other.weight;
}
}
+3
View File
@@ -0,0 +1,3 @@
export function sort(entries: any, biasRight: any): {
vs: any[];
};
+59
View File
@@ -0,0 +1,59 @@
import * as _ from 'lodash-es';
import * as util from '../util.js';
export { sort };
function sort(entries, biasRight) {
var parts = util.partition(entries, function (entry) {
return Object.prototype.hasOwnProperty.call(entry, 'barycenter');
});
var sortable = parts.lhs,
unsortable = _.sortBy(parts.rhs, function (entry) {
return -entry.i;
}),
vs = [],
sum = 0,
weight = 0,
vsIndex = 0;
sortable.sort(compareWithBias(!!biasRight));
vsIndex = consumeUnsortable(vs, unsortable, vsIndex);
_.forEach(sortable, function (entry) {
vsIndex += entry.vs.length;
vs.push(entry.vs);
sum += entry.barycenter * entry.weight;
weight += entry.weight;
vsIndex = consumeUnsortable(vs, unsortable, vsIndex);
});
var result = { vs: _.flatten(vs) };
if (weight) {
result.barycenter = sum / weight;
result.weight = weight;
}
return result;
}
function consumeUnsortable(vs, unsortable, index) {
var last;
while (unsortable.length && (last = _.last(unsortable)).i <= index) {
unsortable.pop();
vs.push(last.vs);
index++;
}
return index;
}
function compareWithBias(bias) {
return function (entryV, entryW) {
if (entryV.barycenter < entryW.barycenter) {
return -1;
} else if (entryV.barycenter > entryW.barycenter) {
return 1;
}
return !bias ? entryV.i - entryW.i : entryW.i - entryV.i;
};
}
+1
View File
@@ -0,0 +1 @@
export function parentDummyChains(g: any): void;
+86
View File
@@ -0,0 +1,86 @@
import * as _ from 'lodash-es';
export { parentDummyChains };
function parentDummyChains(g) {
var postorderNums = postorder(g);
_.forEach(g.graph().dummyChains, function (v) {
var node = g.node(v);
var edgeObj = node.edgeObj;
var pathData = findPath(g, postorderNums, edgeObj.v, edgeObj.w);
var path = pathData.path;
var lca = pathData.lca;
var pathIdx = 0;
var pathV = path[pathIdx];
var ascending = true;
while (v !== edgeObj.w) {
node = g.node(v);
if (ascending) {
while ((pathV = path[pathIdx]) !== lca && g.node(pathV).maxRank < node.rank) {
pathIdx++;
}
if (pathV === lca) {
ascending = false;
}
}
if (!ascending) {
while (
pathIdx < path.length - 1 &&
g.node((pathV = path[pathIdx + 1])).minRank <= node.rank
) {
pathIdx++;
}
pathV = path[pathIdx];
}
g.setParent(v, pathV);
v = g.successors(v)[0];
}
});
}
// Find a path from v to w through the lowest common ancestor (LCA). Return the
// full path and the LCA.
function findPath(g, postorderNums, v, w) {
var vPath = [];
var wPath = [];
var low = Math.min(postorderNums[v].low, postorderNums[w].low);
var lim = Math.max(postorderNums[v].lim, postorderNums[w].lim);
var parent;
var lca;
// Traverse up from v to find the LCA
parent = v;
do {
parent = g.parent(parent);
vPath.push(parent);
} while (parent && (postorderNums[parent].low > low || lim > postorderNums[parent].lim));
lca = parent;
// Traverse from w to LCA
parent = w;
while ((parent = g.parent(parent)) !== lca) {
wPath.push(parent);
}
return { path: vPath.concat(wPath.reverse()), lca: lca };
}
function postorder(g) {
var result = {};
var lim = 0;
function dfs(v) {
var low = lim;
_.forEach(g.children(v), dfs);
result[v] = { low: low, lim: lim++ };
}
_.forEach(g.children(), dfs);
return result;
}
+46
View File
@@ -0,0 +1,46 @@
export function positionX(g: any): {
[x: string]: any;
};
export function findType1Conflicts(g: any, layering: any): {
[nodeId: string]: {
[nodeId: string]: true;
[nodeId: number]: true;
};
[nodeId: number]: {
[nodeId: string]: true;
[nodeId: number]: true;
};
};
export function findType2Conflicts(g: any, layering: any): {
[nodeId: string]: {
[nodeId: string]: true;
[nodeId: number]: true;
};
[nodeId: number]: {
[nodeId: string]: true;
[nodeId: number]: true;
};
};
/**
* Sets `conflicts[v][w] = true`, creating objects if needed.
*
* @param {{[nodeId: string | number]: {[nodeId: string | number]: true}}} conflicts - Object to set.
* @param {string | number} v - First Node ID
* @param {string | number} w - Second Node ID
*/
export function addConflict(conflicts: {
[nodeId: string | number]: {
[nodeId: string | number]: true;
};
}, v: string | number, w: string | number): void;
export function hasConflict(conflicts: any, v: any, w: any): any;
export function verticalAlignment(g: any, layering: any, conflicts: any, neighborFn: any): {
root: {};
align: {};
};
export function horizontalCompaction(g: any, layering: any, root: any, align: any, reverseSep: any): Record<string, number>;
export function alignCoordinates(xss: any, alignTo: any): void;
export function findSmallestWidthAlignment(g: any, xss: any): any;
export function balance(xss: any, align: any): {
[x: string]: any;
};
+444
View File
@@ -0,0 +1,444 @@
import * as _ from 'lodash-es';
import { Graph } from '../../graphlib/index.js';
import * as util from '../util.js';
/*
* This module provides coordinate assignment based on Brandes and Köpf, "Fast
* and Simple Horizontal Coordinate Assignment."
*/
export {
positionX,
findType1Conflicts,
findType2Conflicts,
addConflict,
hasConflict,
verticalAlignment,
horizontalCompaction,
alignCoordinates,
findSmallestWidthAlignment,
balance,
};
/*
* Marks all edges in the graph with a type-1 conflict with the "type1Conflict"
* property. A type-1 conflict is one where a non-inner segment crosses an
* inner segment. An inner segment is an edge with both incident nodes marked
* with the "dummy" property.
*
* This algorithm scans layer by layer, starting with the second, for type-1
* conflicts between the current layer and the previous layer. For each layer
* it scans the nodes from left to right until it reaches one that is incident
* on an inner segment. It then scans predecessors to determine if they have
* edges that cross that inner segment. At the end a final scan is done for all
* nodes on the current rank to see if they cross the last visited inner
* segment.
*
* This algorithm (safely) assumes that a dummy node will only be incident on a
* single node in the layers being scanned.
*/
function findType1Conflicts(g, layering) {
/** @type {{[nodeId: string | number]: {[nodeId: string | number]: true}}} */
var conflicts = {};
function visitLayer(prevLayer, layer) {
var // last visited node in the previous layer that is incident on an inner
// segment.
k0 = 0,
// Tracks the last node in this layer scanned for crossings with a type-1
// segment.
scanPos = 0,
prevLayerLength = prevLayer.length,
lastNode = _.last(layer);
_.forEach(layer, function (v, i) {
var w = findOtherInnerSegmentNode(g, v),
k1 = w ? g.node(w).order : prevLayerLength;
if (w || v === lastNode) {
_.forEach(layer.slice(scanPos, i + 1), function (scanNode) {
_.forEach(g.predecessors(scanNode), function (u) {
var uLabel = g.node(u),
uPos = uLabel.order;
if ((uPos < k0 || k1 < uPos) && !(uLabel.dummy && g.node(scanNode).dummy)) {
addConflict(conflicts, u, scanNode);
}
});
});
// @ts-expect-error
scanPos = i + 1;
k0 = k1;
}
});
return layer;
}
_.reduce(layering, visitLayer);
return conflicts;
}
function findType2Conflicts(g, layering) {
/** @type {{[nodeId: string | number]: {[nodeId: string | number]: true}}} */
var conflicts = {};
function scan(south, southPos, southEnd, prevNorthBorder, nextNorthBorder) {
var v;
_.forEach(_.range(southPos, southEnd), function (i) {
v = south[i];
if (g.node(v).dummy) {
_.forEach(g.predecessors(v), function (u) {
var uNode = g.node(u);
if (uNode.dummy && (uNode.order < prevNorthBorder || uNode.order > nextNorthBorder)) {
addConflict(conflicts, u, v);
}
});
}
});
}
function visitLayer(north, south) {
var prevNorthPos = -1,
nextNorthPos,
southPos = 0;
_.forEach(south, function (v, southLookahead) {
if (g.node(v).dummy === 'border') {
var predecessors = g.predecessors(v);
if (predecessors.length) {
nextNorthPos = g.node(predecessors[0]).order;
scan(south, southPos, southLookahead, prevNorthPos, nextNorthPos);
// @ts-expect-error
southPos = southLookahead;
prevNorthPos = nextNorthPos;
}
}
scan(south, southPos, south.length, nextNorthPos, north.length);
});
return south;
}
_.reduce(layering, visitLayer);
return conflicts;
}
function findOtherInnerSegmentNode(g, v) {
if (g.node(v).dummy) {
return _.find(g.predecessors(v), function (u) {
return g.node(u).dummy;
});
}
}
/**
* Sets `conflicts[v][w] = true`, creating objects if needed.
*
* @param {{[nodeId: string | number]: {[nodeId: string | number]: true}}} conflicts - Object to set.
* @param {string | number} v - First Node ID
* @param {string | number} w - Second Node ID
*/
function addConflict(conflicts, v, w) {
if (v > w) {
var tmp = v;
v = w;
w = tmp;
}
if (!Object.prototype.hasOwnProperty.call(conflicts, v)) {
// can't use conflicts[v] = {} since it's unsafe if v = `__proto__`
Object.defineProperty(conflicts, v, {
enumerable: true,
configurable: true,
value: {},
writable: true,
});
}
var conflictsV = conflicts[v];
Object.defineProperty(conflictsV, w, {
enumerable: true,
configurable: true,
value: true,
writable: true,
});
}
function hasConflict(conflicts, v, w) {
if (v > w) {
var tmp = v;
v = w;
w = tmp;
}
return !!conflicts[v] && Object.prototype.hasOwnProperty.call(conflicts[v], w);
}
/*
* Try to align nodes into vertical "blocks" where possible. This algorithm
* attempts to align a node with one of its median neighbors. If the edge
* connecting a neighbor is a type-1 conflict then we ignore that possibility.
* If a previous node has already formed a block with a node after the node
* we're trying to form a block with, we also ignore that possibility - our
* blocks would be split in that scenario.
*/
function verticalAlignment(g, layering, conflicts, neighborFn) {
var root = {},
align = {},
pos = {};
// We cache the position here based on the layering because the graph and
// layering may be out of sync. The layering matrix is manipulated to
// generate different extreme alignments.
_.forEach(layering, function (layer) {
_.forEach(layer, function (v, order) {
root[v] = v;
align[v] = v;
pos[v] = order;
});
});
_.forEach(layering, function (layer) {
var prevIdx = -1;
_.forEach(layer, function (v) {
var ws = neighborFn(v);
if (ws.length) {
ws = _.sortBy(ws, function (w) {
return pos[w];
});
var mp = (ws.length - 1) / 2;
for (var i = Math.floor(mp), il = Math.ceil(mp); i <= il; ++i) {
var w = ws[i];
if (align[v] === v && prevIdx < pos[w] && !hasConflict(conflicts, v, w)) {
align[w] = v;
align[v] = root[v] = root[w];
prevIdx = pos[w];
}
}
}
});
});
return { root: root, align: align };
}
function horizontalCompaction(g, layering, root, align, reverseSep) {
// This portion of the algorithm differs from BK due to a number of problems.
// Instead of their algorithm we construct a new block graph and do two
// sweeps. The first sweep places blocks with the smallest possible
// coordinates. The second sweep removes unused space by moving blocks to the
// greatest coordinates without violating separation.
/** @type {Record<import('../../graphlib/graph.js').NodeID, number>} */
var xs = {},
blockG = buildBlockGraph(g, layering, root, reverseSep),
borderType = reverseSep ? 'borderLeft' : 'borderRight';
function iterate(setXsFunc, nextNodesFunc) {
var stack = blockG.nodes();
var elem = stack.pop();
var visited = {};
while (elem) {
if (visited[elem]) {
setXsFunc(elem);
} else {
visited[elem] = true;
stack.push(elem);
stack = stack.concat(nextNodesFunc(elem));
}
elem = stack.pop();
}
}
// First pass, assign smallest coordinates
function pass1(elem) {
xs[elem] = blockG.inEdges(elem).reduce(function (acc, e) {
return Math.max(acc, xs[e.v] + blockG.edge(e));
}, 0);
}
// Second pass, assign greatest coordinates
function pass2(elem) {
var min = blockG.outEdges(elem).reduce(function (acc, e) {
return Math.min(acc, xs[e.w] - blockG.edge(e));
}, Number.POSITIVE_INFINITY);
var node = g.node(elem);
if (min !== Number.POSITIVE_INFINITY && node.borderType !== borderType) {
xs[elem] = Math.max(xs[elem], min);
}
}
iterate(pass1, blockG.predecessors.bind(blockG));
iterate(pass2, blockG.successors.bind(blockG));
// Assign x coordinates to all nodes
_.forEach(align, function (v) {
xs[v] = xs[root[v]];
});
return xs;
}
function buildBlockGraph(g, layering, root, reverseSep) {
var blockGraph = new Graph(),
graphLabel = g.graph(),
sepFn = sep(graphLabel.nodesep, graphLabel.edgesep, reverseSep);
_.forEach(layering, function (layer) {
var u;
_.forEach(layer, function (v) {
var vRoot = root[v];
blockGraph.setNode(vRoot);
if (u) {
var uRoot = root[u],
prevMax = blockGraph.edge(uRoot, vRoot);
blockGraph.setEdge(uRoot, vRoot, Math.max(sepFn(g, v, u), prevMax || 0));
}
u = v;
});
});
return blockGraph;
}
/*
* Returns the alignment that has the smallest width of the given alignments.
*/
function findSmallestWidthAlignment(g, xss) {
return _.minBy(_.values(xss), function (xs) {
var max = Number.NEGATIVE_INFINITY;
var min = Number.POSITIVE_INFINITY;
_.forIn(xs, function (x, v) {
var halfWidth = width(g, v) / 2;
max = Math.max(x + halfWidth, max);
min = Math.min(x - halfWidth, min);
});
return max - min;
});
}
/*
* Align the coordinates of each of the layout alignments such that
* left-biased alignments have their minimum coordinate at the same point as
* the minimum coordinate of the smallest width alignment and right-biased
* alignments have their maximum coordinate at the same point as the maximum
* coordinate of the smallest width alignment.
*/
function alignCoordinates(xss, alignTo) {
var alignToVals = _.values(alignTo),
alignToMin = _.min(alignToVals),
alignToMax = _.max(alignToVals);
_.forEach(['u', 'd'], function (vert) {
_.forEach(['l', 'r'], function (horiz) {
var alignment = vert + horiz,
xs = xss[alignment],
delta;
if (xs === alignTo) return;
var xsVals = _.values(xs);
delta = horiz === 'l' ? alignToMin - _.min(xsVals) : alignToMax - _.max(xsVals);
if (delta) {
xss[alignment] = _.mapValues(xs, function (x) {
return x + delta;
});
}
});
});
}
function balance(xss, align) {
return _.mapValues(xss.ul, function (ignore, v) {
if (align) {
return xss[align.toLowerCase()][v];
} else {
var xs = _.sortBy(_.map(xss, v));
return (xs[1] + xs[2]) / 2;
}
});
}
function positionX(g) {
var layering = util.buildLayerMatrix(g);
var conflicts = _.merge(findType1Conflicts(g, layering), findType2Conflicts(g, layering));
var xss = {};
var adjustedLayering;
_.forEach(['u', 'd'], function (vert) {
adjustedLayering = vert === 'u' ? layering : _.values(layering).reverse();
_.forEach(['l', 'r'], function (horiz) {
if (horiz === 'r') {
adjustedLayering = _.map(adjustedLayering, function (inner) {
return _.values(inner).reverse();
});
}
var neighborFn = (vert === 'u' ? g.predecessors : g.successors).bind(g);
var align = verticalAlignment(g, adjustedLayering, conflicts, neighborFn);
var xs = horizontalCompaction(g, adjustedLayering, align.root, align.align, horiz === 'r');
if (horiz === 'r') {
xs = _.mapValues(xs, function (x) {
return -x;
});
}
xss[vert + horiz] = xs;
});
});
var smallestWidth = findSmallestWidthAlignment(g, xss);
alignCoordinates(xss, smallestWidth);
return balance(xss, g.graph().align);
}
function sep(nodeSep, edgeSep, reverseSep) {
return function (g, v, w) {
var vLabel = g.node(v);
var wLabel = g.node(w);
var sum = 0;
var delta;
sum += vLabel.width / 2;
if (Object.prototype.hasOwnProperty.call(vLabel, 'labelpos')) {
switch (vLabel.labelpos.toLowerCase()) {
case 'l':
delta = -vLabel.width / 2;
break;
case 'r':
delta = vLabel.width / 2;
break;
}
}
if (delta) {
sum += reverseSep ? delta : -delta;
}
delta = 0;
sum += (vLabel.dummy ? edgeSep : nodeSep) / 2;
sum += (wLabel.dummy ? edgeSep : nodeSep) / 2;
sum += wLabel.width / 2;
if (Object.prototype.hasOwnProperty.call(wLabel, 'labelpos')) {
switch (wLabel.labelpos.toLowerCase()) {
case 'l':
delta = wLabel.width / 2;
break;
case 'r':
delta = -wLabel.width / 2;
break;
}
}
if (delta) {
sum += reverseSep ? delta : -delta;
}
delta = 0;
return sum;
};
}
function width(g, v) {
return g.node(v).width;
}
+1
View File
@@ -0,0 +1 @@
export function position(g: any): void;
+31
View File
@@ -0,0 +1,31 @@
import * as _ from 'lodash-es';
import * as util from '../util.js';
import { positionX } from './bk.js';
export { position };
function position(g) {
g = util.asNonCompoundGraph(g);
positionY(g);
_.forOwn(positionX(g), function (x, v) {
g.node(v).x = x;
});
}
function positionY(g) {
var layering = util.buildLayerMatrix(g);
var rankSep = g.graph().ranksep;
var prevY = 0;
_.forEach(layering, function (layer) {
var maxHeight = _.max(
_.map(layer, function (v) {
return g.node(v).height;
}),
);
_.forEach(layer, function (v) {
g.node(v).y = prevY + maxHeight / 2;
});
prevY += maxHeight + rankSep;
});
}
+2
View File
@@ -0,0 +1,2 @@
export function feasibleTree(g: any): Graph<any, any, any>;
import { Graph } from '../../graphlib/index.js';
+87
View File
@@ -0,0 +1,87 @@
import * as _ from 'lodash-es';
import { Graph } from '../../graphlib/index.js';
import { slack } from './util.js';
export { feasibleTree };
/*
* Constructs a spanning tree with tight edges and adjusted the input node's
* ranks to achieve this. A tight edge is one that is has a length that matches
* its "minlen" attribute.
*
* The basic structure for this function is derived from Gansner, et al., "A
* Technique for Drawing Directed Graphs."
*
* Pre-conditions:
*
* 1. Graph must be a DAG.
* 2. Graph must be connected.
* 3. Graph must have at least one node.
* 5. Graph nodes must have been previously assigned a "rank" property that
* respects the "minlen" property of incident edges.
* 6. Graph edges must have a "minlen" property.
*
* Post-conditions:
*
* - Graph nodes will have their rank adjusted to ensure that all edges are
* tight.
*
* Returns a tree (undirected graph) that is constructed using only "tight"
* edges.
*/
function feasibleTree(g) {
var t = new Graph({ directed: false });
// Choose arbitrary node from which to start our tree
var start = g.nodes()[0];
var size = g.nodeCount();
t.setNode(start, {});
var edge, delta;
while (tightTree(t, g) < size) {
edge = findMinSlackEdge(t, g);
delta = t.hasNode(edge.v) ? slack(g, edge) : -slack(g, edge);
shiftRanks(t, g, delta);
}
return t;
}
/*
* Finds a maximal tree of tight edges and returns the number of nodes in the
* tree.
*/
function tightTree(t, g) {
function dfs(v) {
_.forEach(g.nodeEdges(v), function (e) {
var edgeV = e.v,
w = v === edgeV ? e.w : edgeV;
if (!t.hasNode(w) && !slack(g, e)) {
t.setNode(w, {});
t.setEdge(v, w, {});
dfs(w);
}
});
}
_.forEach(t.nodes(), dfs);
return t.nodeCount();
}
/*
* Finds the edge with the smallest slack that is incident on tree and returns
* it.
*/
function findMinSlackEdge(t, g) {
return _.minBy(g.edges(), function (e) {
if (t.hasNode(e.v) !== t.hasNode(e.w)) {
return slack(g, e);
}
});
}
function shiftRanks(t, g, delta) {
_.forEach(t.nodes(), function (v) {
g.node(v).rank += delta;
});
}
+1
View File
@@ -0,0 +1 @@
export function rank(g: any): void;
+52
View File
@@ -0,0 +1,52 @@
import { feasibleTree } from './feasible-tree.js';
import { networkSimplex } from './network-simplex.js';
import { longestPath } from './util.js';
export { rank };
/*
* Assigns a rank to each node in the input graph that respects the "minlen"
* constraint specified on edges between nodes.
*
* This basic structure is derived from Gansner, et al., "A Technique for
* Drawing Directed Graphs."
*
* Pre-conditions:
*
* 1. Graph must be a connected DAG
* 2. Graph nodes must be objects
* 3. Graph edges must have "weight" and "minlen" attributes
*
* Post-conditions:
*
* 1. Graph nodes will have a "rank" attribute based on the results of the
* algorithm. Ranks can start at any index (including negative), we'll
* fix them up later.
*/
function rank(g) {
switch (g.graph().ranker) {
case 'network-simplex':
networkSimplexRanker(g);
break;
case 'tight-tree':
tightTreeRanker(g);
break;
case 'longest-path':
longestPathRanker(g);
break;
default:
networkSimplexRanker(g);
}
}
// A fast and simple ranker, but results are far from optimal.
var longestPathRanker = longestPath;
function tightTreeRanker(g) {
longestPath(g);
feasibleTree(g);
}
function networkSimplexRanker(g) {
networkSimplex(g);
}
+16
View File
@@ -0,0 +1,16 @@
export function networkSimplex(g: any): void;
export namespace networkSimplex {
export { initLowLimValues };
export { initCutValues };
export { calcCutValue };
export { leaveEdge };
export { enterEdge };
export { exchangeEdges };
}
declare function initLowLimValues(tree: any, root: any, ...args: any[]): void;
declare function initCutValues(t: any, g: any): void;
declare function calcCutValue(t: any, g: any, child: any): number;
declare function leaveEdge(tree: any): any;
declare function enterEdge(t: any, g: any, edge: any): any;
declare function exchangeEdges(t: any, g: any, e: any, f: any): void;
export {};
+236
View File
@@ -0,0 +1,236 @@
import * as _ from 'lodash-es';
import * as alg from '../../graphlib/alg/index.js';
import { simplify } from '../util.js';
import { feasibleTree } from './feasible-tree.js';
import { longestPath, slack } from './util.js';
export { networkSimplex };
// Expose some internals for testing purposes
networkSimplex.initLowLimValues = initLowLimValues;
networkSimplex.initCutValues = initCutValues;
networkSimplex.calcCutValue = calcCutValue;
networkSimplex.leaveEdge = leaveEdge;
networkSimplex.enterEdge = enterEdge;
networkSimplex.exchangeEdges = exchangeEdges;
/*
* 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.
*/
function networkSimplex(g) {
g = simplify(g);
longestPath(g);
var t = feasibleTree(g);
initLowLimValues(t);
initCutValues(t, g);
var e, f;
while ((e = leaveEdge(t))) {
f = enterEdge(t, g, e);
exchangeEdges(t, g, e, f);
}
}
/*
* Initializes cut values for all edges in the tree.
*/
function initCutValues(t, g) {
var vs = alg.postorder(t, t.nodes());
vs = vs.slice(0, vs.length - 1);
_.forEach(vs, function (v) {
assignCutValue(t, g, v);
});
}
function assignCutValue(t, g, child) {
var childLab = t.node(child);
var parent = childLab.parent;
t.edge(child, parent).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.
*/
function calcCutValue(t, g, child) {
var childLab = t.node(child);
var parent = childLab.parent;
// True if the child is on the tail end of the edge in the directed graph
var childIsTail = true;
// The graph's view of the tree edge we're inspecting
var graphEdge = g.edge(child, parent);
// The accumulated cut value for the edge between this node and its parent
var cutValue = 0;
if (!graphEdge) {
childIsTail = false;
graphEdge = g.edge(parent, child);
}
cutValue = graphEdge.weight;
_.forEach(g.nodeEdges(child), function (e) {
var isOutEdge = e.v === child,
other = isOutEdge ? e.w : e.v;
if (other !== parent) {
var pointsToHead = isOutEdge === childIsTail,
otherWeight = g.edge(e).weight;
cutValue += pointsToHead ? otherWeight : -otherWeight;
if (isTreeEdge(t, child, other)) {
var otherCutValue = t.edge(child, other).cutvalue;
cutValue += pointsToHead ? -otherCutValue : otherCutValue;
}
}
});
return cutValue;
}
function initLowLimValues(tree, root) {
if (arguments.length < 2) {
root = tree.nodes()[0];
}
dfsAssignLowLim(tree, {}, 1, root);
}
function dfsAssignLowLim(tree, visited, nextLim, v, parent) {
var low = nextLim;
var label = tree.node(v);
visited[v] = true;
_.forEach(tree.neighbors(v), function (w) {
if (!Object.prototype.hasOwnProperty.call(visited, w)) {
nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v);
}
});
label.low = low;
label.lim = nextLim++;
if (parent) {
label.parent = parent;
} else {
// TODO should be able to remove this when we incrementally update low lim
delete label.parent;
}
return nextLim;
}
function leaveEdge(tree) {
return _.find(tree.edges(), function (e) {
return tree.edge(e).cutvalue < 0;
});
}
function enterEdge(t, g, edge) {
var v = edge.v;
var w = edge.w;
// 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.hasEdge(v, w)) {
v = edge.w;
w = edge.v;
}
var vLabel = t.node(v);
var wLabel = t.node(w);
var tailLabel = vLabel;
var 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.lim > wLabel.lim) {
tailLabel = wLabel;
flip = true;
}
var candidates = _.filter(g.edges(), function (edge) {
return (
flip === isDescendant(t, t.node(edge.v), tailLabel) &&
flip !== isDescendant(t, t.node(edge.w), tailLabel)
);
});
return _.minBy(candidates, function (edge) {
return slack(g, edge);
});
}
function exchangeEdges(t, g, e, f) {
var v = e.v;
var w = e.w;
t.removeEdge(v, w);
t.setEdge(f.v, f.w, {});
initLowLimValues(t);
initCutValues(t, g);
updateRanks(t, g);
}
function updateRanks(t, g) {
var root = _.find(t.nodes(), function (v) {
return !g.node(v).parent;
});
var vs = alg.preorder(t, root);
vs = vs.slice(1);
_.forEach(vs, function (v) {
var parent = t.node(v).parent,
edge = g.edge(v, parent),
flipped = false;
if (!edge) {
edge = g.edge(parent, v);
flipped = true;
}
g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen);
});
}
/*
* Returns true if the edge is in the tree.
*/
function isTreeEdge(tree, u, v) {
return tree.hasEdge(u, v);
}
/*
* Returns true if the specified node is descendant of the root node per the
* assigned low and lim attributes in the tree.
*/
function isDescendant(tree, vLabel, rootLabel) {
return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim;
}
+2
View File
@@ -0,0 +1,2 @@
export function longestPath(g: any): void;
export function slack(g: any, e: any): number;
+63
View File
@@ -0,0 +1,63 @@
import * as _ from 'lodash-es';
export { longestPath, slack };
/*
* Initializes ranks for the input graph using the longest path algorithm. This
* algorithm scales well and is fast in practice, it yields rather poor
* solutions. Nodes are pushed to the lowest layer possible, leaving the bottom
* ranks wide and leaving edges longer than necessary. However, due to its
* speed, this algorithm is good for getting an initial ranking that can be fed
* into other algorithms.
*
* This algorithm does not normalize layers because it will be used by other
* algorithms in most cases. If using this algorithm directly, be sure to
* run normalize at the end.
*
* Pre-conditions:
*
* 1. Input graph is a DAG.
* 2. Input graph node labels can be assigned properties.
*
* Post-conditions:
*
* 1. Each node will be assign an (unnormalized) "rank" property.
*/
function longestPath(g) {
var visited = {};
function dfs(v) {
var label = g.node(v);
if (Object.prototype.hasOwnProperty.call(visited, v)) {
return label.rank;
}
visited[v] = true;
var rank = _.min(
_.map(g.outEdges(v), function (e) {
return dfs(e.w) - g.edge(e).minlen;
}),
);
if (
rank === Number.POSITIVE_INFINITY || // return value of _.map([]) for Lodash 3
rank === undefined || // return value of _.map([]) for Lodash 4
rank === null
) {
// return value of _.map([null])
rank = 0;
}
return (label.rank = rank);
}
_.forEach(g.sources(), dfs);
}
/*
* Returns the amount of slack for the given edge. The slack is defined as the
* difference between the length of the edge and its minimum length.
*/
function slack(g, e) {
return g.node(e.w).rank - g.node(e.v).rank - g.edge(e).minlen;
}
+21
View File
@@ -0,0 +1,21 @@
export function addDummyNode(g: any, type: any, attrs: any, name: any): string;
export function simplify(g: any): Graph<any, any, any>;
export function asNonCompoundGraph(g: any): Graph<any, any, any>;
export function successorWeights(g: any): import("lodash").Dictionary<{}>;
export function predecessorWeights(g: any): import("lodash").Dictionary<{}>;
export function intersectRect(rect: any, point: any): {
x: any;
y: any;
};
export function buildLayerMatrix(g: any): any[][];
export function normalizeRanks(g: any): void;
export function removeEmptyRanks(g: any): void;
export function addBorderNode(g: any, prefix: any, rank: any, order: any, ...args: any[]): string;
export function maxRank(g: any): any;
export function partition(collection: any, fn: any): {
lhs: any[];
rhs: any[];
};
export function time(name: any, fn: any): any;
export function notime(name: any, fn: any): any;
import { Graph } from '../graphlib/index.js';

Some files were not shown because too many files have changed in this diff Show More