You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

468 lines
17 KiB

import { Graph } from '@antv/graphlib';
import { isNil } from '@antv/util';
import { run as runAcyclic, undo as undoAcyclic } from './acyclic';
import { addBorderSegments } from './add-border-segments';
import { adjust as adjustCoordinateSystem, undo as undoCoordinateSystem, } from './coordinate-system';
import { cleanup as cleanupNestingGraph, run as runNestingGraph, } from './nesting-graph';
import { run as runNormalize, undo as undoNormalize } from './normalize';
import { order } from './order';
import { initDataOrder } from './order/init-data-order';
import { parentDummyChains } from './parent-dummy-chains';
import { position } from './position';
import { rank } from './rank';
import { addDummyNode, asNonCompoundGraph, buildLayerMatrix, intersectRect, normalizeRanks, removeEmptyRanks, } from './util';
// const graphNumAttrs = ["nodesep", "edgesep", "ranksep", "marginx", "marginy"];
// const graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: "tb" };
// const graphAttrs = ["acyclicer", "ranker", "rankdir", "align"];
export const layout = (g, options) => {
const { edgeLabelSpace, keepNodeOrder, prevGraph, rankdir, ranksep } = options;
// 如果在原图基础上修改,继承原图的order结果
if (!keepNodeOrder && prevGraph) {
inheritOrder(g, prevGraph);
}
const layoutGraph = buildLayoutGraph(g);
// 控制是否为边的label留位置(这会影响是否在边中间添加dummy node)
if (!!edgeLabelSpace) {
options.ranksep = makeSpaceForEdgeLabels(layoutGraph, {
rankdir,
ranksep,
});
}
let dimension;
// TODO: 暂时处理层级设置不正确时的异常报错,提示设置正确的层级
try {
dimension = runLayout(layoutGraph, options);
}
catch (e) {
if (e.message === 'Not possible to find intersection inside of the rectangle') {
console.error("The following error may be caused by improper layer setting, please make sure your manual layer setting does not violate the graph's structure:\n", e);
return;
}
throw e;
}
updateInputGraph(g, layoutGraph);
return dimension;
};
const runLayout = (g, options) => {
const { acyclicer, ranker, rankdir = 'tb', nodeOrder, keepNodeOrder, align, nodesep = 50, edgesep = 20, ranksep = 50, } = options;
removeSelfEdges(g);
runAcyclic(g, acyclicer);
const { nestingRoot, nodeRankFactor } = runNestingGraph(g);
rank(asNonCompoundGraph(g), ranker);
injectEdgeLabelProxies(g);
removeEmptyRanks(g, nodeRankFactor);
cleanupNestingGraph(g, nestingRoot);
normalizeRanks(g);
assignRankMinMax(g);
removeEdgeLabelProxies(g);
const dummyChains = [];
runNormalize(g, dummyChains);
parentDummyChains(g, dummyChains);
addBorderSegments(g);
if (keepNodeOrder) {
initDataOrder(g, nodeOrder);
}
order(g, keepNodeOrder);
insertSelfEdges(g);
adjustCoordinateSystem(g, rankdir);
position(g, {
align,
nodesep,
edgesep,
ranksep,
});
positionSelfEdges(g);
removeBorderNodes(g);
undoNormalize(g, dummyChains);
fixupEdgeLabelCoords(g);
undoCoordinateSystem(g, rankdir);
const { width, height } = translateGraph(g);
assignNodeIntersects(g);
reversePointsForReversedEdges(g);
undoAcyclic(g);
return { width, height };
};
/**
* 继承上一个布局中的order,防止翻转
* TODO: 暂时没有考虑涉及层级变动的布局,只保证原来布局层级和相对顺序不变
*/
const inheritOrder = (currG, prevG) => {
currG.getAllNodes().forEach((n) => {
const node = currG.getNode(n.id);
if (prevG.hasNode(n.id)) {
const prevNode = prevG.getNode(n.id);
node.data.fixorder = prevNode.data._order;
delete prevNode.data._order;
}
else {
delete node.data.fixorder;
}
});
};
/*
* 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.
*/
const updateInputGraph = (inputGraph, layoutGraph) => {
inputGraph.getAllNodes().forEach((v) => {
var _a;
const inputLabel = inputGraph.getNode(v.id);
if (inputLabel) {
const layoutLabel = layoutGraph.getNode(v.id);
inputLabel.data.x = layoutLabel.data.x;
inputLabel.data.y = layoutLabel.data.y;
inputLabel.data._order = layoutLabel.data.order;
inputLabel.data._rank = layoutLabel.data.rank;
if ((_a = layoutGraph.getChildren(v.id)) === null || _a === void 0 ? void 0 : _a.length) {
inputLabel.data.width = layoutLabel.data.width;
inputLabel.data.height = layoutLabel.data.height;
}
}
});
inputGraph.getAllEdges().forEach((e) => {
const inputLabel = inputGraph.getEdge(e.id);
const layoutLabel = layoutGraph.getEdge(e.id);
inputLabel.data.points = layoutLabel ? layoutLabel.data.points : [];
if (layoutLabel && layoutLabel.data.hasOwnProperty('x')) {
inputLabel.data.x = layoutLabel.data.x;
inputLabel.data.y = layoutLabel.data.y;
}
});
// inputGraph.graph().width = layoutGraph.graph().width;
// inputGraph.graph().height = layoutGraph.graph().height;
};
const nodeNumAttrs = ['width', 'height', 'layer', 'fixorder']; // 需要传入layer, fixOrder作为参数参考
const nodeDefaults = { width: 0, height: 0 };
const edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset'];
const edgeDefaults = {
minlen: 1,
weight: 1,
width: 0,
height: 0,
labeloffset: 10,
labelpos: 'r',
};
const 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.
*/
const buildLayoutGraph = (inputGraph) => {
const g = new Graph({ tree: [] });
inputGraph.getAllNodes().forEach((v) => {
const node = canonicalize(inputGraph.getNode(v.id).data);
const defaultNode = Object.assign(Object.assign({}, nodeDefaults), node);
const defaultAttrs = selectNumberAttrs(defaultNode, nodeNumAttrs);
if (!g.hasNode(v.id)) {
g.addNode({
id: v.id,
data: Object.assign({}, defaultAttrs),
});
}
const parent = inputGraph.hasTreeStructure('combo')
? inputGraph.getParent(v.id, 'combo')
: inputGraph.getParent(v.id);
if (!isNil(parent)) {
if (!g.hasNode(parent.id)) {
g.addNode(Object.assign({}, parent));
}
g.setParent(v.id, parent.id);
}
});
inputGraph.getAllEdges().forEach((e) => {
const edge = canonicalize(inputGraph.getEdge(e.id).data);
const pickedProperties = {};
edgeAttrs === null || edgeAttrs === void 0 ? void 0 : edgeAttrs.forEach((key) => {
if (edge[key] !== undefined)
pickedProperties[key] = edge[key];
});
g.addEdge({
id: e.id,
source: e.source,
target: e.target,
data: Object.assign({}, edgeDefaults, selectNumberAttrs(edge, edgeNumAttrs), pickedProperties),
});
});
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.
*/
const makeSpaceForEdgeLabels = (g, options) => {
const { ranksep = 0, rankdir } = options;
g.getAllNodes().forEach((node) => {
if (!isNaN(node.data.layer)) {
if (!node.data.layer)
node.data.layer = 0;
}
});
g.getAllEdges().forEach((edge) => {
var _a;
edge.data.minlen *= 2;
if (((_a = edge.data.labelpos) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== 'c') {
if (rankdir === 'TB' || rankdir === 'BT') {
edge.data.width += edge.data.labeloffset;
}
else {
edge.data.height += edge.data.labeloffset;
}
}
});
return ranksep / 2;
};
/*
* 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.
*/
const injectEdgeLabelProxies = (g) => {
g.getAllEdges().forEach((e) => {
if (e.data.width && e.data.height) {
const v = g.getNode(e.source);
const w = g.getNode(e.target);
const label = {
e,
rank: (w.data.rank - v.data.rank) / 2 + v.data.rank,
};
addDummyNode(g, 'edge-proxy', label, '_ep');
}
});
};
const assignRankMinMax = (g) => {
let maxRank = 0;
g.getAllNodes().forEach((node) => {
var _a, _b;
if (node.data.borderTop) {
node.data.minRank = (_a = g.getNode(node.data.borderTop)) === null || _a === void 0 ? void 0 : _a.data.rank;
node.data.maxRank = (_b = g.getNode(node.data.borderBottom)) === null || _b === void 0 ? void 0 : _b.data.rank;
maxRank = Math.max(maxRank, node.data.maxRank || -Infinity);
}
});
return maxRank;
};
const removeEdgeLabelProxies = (g) => {
g.getAllNodes().forEach((node) => {
if (node.data.dummy === 'edge-proxy') {
g.getEdge(node.data.e.id).data.labelRank = node.data.rank;
g.removeNode(node.id);
}
});
};
const translateGraph = (g, options) => {
let minX;
let maxX = 0;
let minY;
let maxY = 0;
const { marginx: marginX = 0, marginy: marginY = 0 } = options || {};
const getExtremes = (attrs) => {
if (!attrs.data)
return;
const x = attrs.data.x;
const y = attrs.data.y;
const w = attrs.data.width;
const h = attrs.data.height;
if (!isNaN(x) && !isNaN(w)) {
if (minX === undefined) {
minX = x - w / 2;
}
minX = Math.min(minX, x - w / 2);
maxX = Math.max(maxX, x + w / 2);
}
if (!isNaN(y) && !isNaN(h)) {
if (minY === undefined) {
minY = y - h / 2;
}
minY = Math.min(minY, y - h / 2);
maxY = Math.max(maxY, y + h / 2);
}
};
g.getAllNodes().forEach((v) => {
getExtremes(v);
});
g.getAllEdges().forEach((e) => {
if (e === null || e === void 0 ? void 0 : e.data.hasOwnProperty('x')) {
getExtremes(e);
}
});
minX -= marginX;
minY -= marginY;
g.getAllNodes().forEach((node) => {
node.data.x -= minX;
node.data.y -= minY;
});
g.getAllEdges().forEach((edge) => {
var _a;
(_a = edge.data.points) === null || _a === void 0 ? void 0 : _a.forEach((p) => {
p.x -= minX;
p.y -= minY;
});
if (edge.data.hasOwnProperty('x')) {
edge.data.x -= minX;
}
if (edge.data.hasOwnProperty('y')) {
edge.data.y -= minY;
}
});
return {
width: maxX - minX + marginX,
height: maxY - minY + marginY,
};
};
const assignNodeIntersects = (g) => {
g.getAllEdges().forEach((e) => {
const nodeV = g.getNode(e.source);
const nodeW = g.getNode(e.target);
let p1;
let p2;
if (!e.data.points) {
e.data.points = [];
p1 = { x: nodeW.data.x, y: nodeW.data.y };
p2 = { x: nodeV.data.x, y: nodeV.data.y };
}
else {
p1 = e.data.points[0];
p2 = e.data.points[e.data.points.length - 1];
}
e.data.points.unshift(intersectRect(nodeV.data, p1));
e.data.points.push(intersectRect(nodeW.data, p2));
});
};
const fixupEdgeLabelCoords = (g) => {
g.getAllEdges().forEach((edge) => {
if (edge.data.hasOwnProperty('x')) {
if (edge.data.labelpos === 'l' || edge.data.labelpos === 'r') {
edge.data.width -= edge.data.labeloffset;
}
switch (edge.data.labelpos) {
case 'l':
edge.data.x -= edge.data.width / 2 + edge.data.labeloffset;
break;
case 'r':
edge.data.x += edge.data.width / 2 + edge.data.labeloffset;
break;
}
}
});
};
const reversePointsForReversedEdges = (g) => {
g.getAllEdges().forEach((edge) => {
var _a;
if (edge.data.reversed) {
(_a = edge.data.points) === null || _a === void 0 ? void 0 : _a.reverse();
}
});
};
const removeBorderNodes = (g) => {
g.getAllNodes().forEach((v) => {
var _a, _b, _c;
if ((_a = g.getChildren(v.id)) === null || _a === void 0 ? void 0 : _a.length) {
const node = g.getNode(v.id);
const t = g.getNode(node.data.borderTop);
const b = g.getNode(node.data.borderBottom);
const l = g.getNode(node.data.borderLeft[((_b = node.data.borderLeft) === null || _b === void 0 ? void 0 : _b.length) - 1]);
const r = g.getNode(node.data.borderRight[((_c = node.data.borderRight) === null || _c === void 0 ? void 0 : _c.length) - 1]);
node.data.width = Math.abs((r === null || r === void 0 ? void 0 : r.data.x) - (l === null || l === void 0 ? void 0 : l.data.x)) || 10;
node.data.height = Math.abs((b === null || b === void 0 ? void 0 : b.data.y) - (t === null || t === void 0 ? void 0 : t.data.y)) || 10;
node.data.x = ((l === null || l === void 0 ? void 0 : l.data.x) || 0) + node.data.width / 2;
node.data.y = ((t === null || t === void 0 ? void 0 : t.data.y) || 0) + node.data.height / 2;
}
});
g.getAllNodes().forEach((n) => {
if (n.data.dummy === 'border') {
g.removeNode(n.id);
}
});
};
const removeSelfEdges = (g) => {
g.getAllEdges().forEach((e) => {
if (e.source === e.target) {
const node = g.getNode(e.source);
if (!node.data.selfEdges) {
node.data.selfEdges = [];
}
node.data.selfEdges.push(e);
g.removeEdge(e.id);
}
});
};
const insertSelfEdges = (g) => {
const layers = buildLayerMatrix(g);
layers === null || layers === void 0 ? void 0 : layers.forEach((layer) => {
let orderShift = 0;
layer === null || layer === void 0 ? void 0 : layer.forEach((v, i) => {
var _a;
const node = g.getNode(v);
node.data.order = i + orderShift;
(_a = node.data.selfEdges) === null || _a === void 0 ? void 0 : _a.forEach((selfEdge) => {
addDummyNode(g, 'selfedge', {
width: selfEdge.data.width,
height: selfEdge.data.height,
rank: node.data.rank,
order: i + ++orderShift,
e: selfEdge,
}, '_se');
});
delete node.data.selfEdges;
});
});
};
const positionSelfEdges = (g) => {
g.getAllNodes().forEach((v) => {
const node = g.getNode(v.id);
if (node.data.dummy === 'selfedge') {
const selfNode = g.getNode(node.data.e.source);
const x = selfNode.data.x + selfNode.data.width / 2;
const y = selfNode.data.y;
const dx = node.data.x - x;
const dy = selfNode.data.height / 2;
if (g.hasEdge(node.data.e.id)) {
g.updateEdgeData(node.data.e.id, node.data.e.data);
}
else {
g.addEdge({
id: node.data.e.id,
source: node.data.e.source,
target: node.data.e.target,
data: node.data.e.data,
});
}
g.removeNode(v.id);
node.data.e.data.points = [
{ x: x + (2 * dx) / 3, y: y - dy },
{ x: x + (5 * dx) / 6, y: y - dy },
{ y, x: x + dx },
{ x: x + (5 * dx) / 6, y: y + dy },
{ x: x + (2 * dx) / 3, y: y + dy },
];
node.data.e.data.x = node.data.x;
node.data.e.data.y = node.data.y;
}
});
};
const selectNumberAttrs = (obj, attrs) => {
const pickedProperties = {};
attrs === null || attrs === void 0 ? void 0 : attrs.forEach((key) => {
if (obj[key] === undefined)
return;
pickedProperties[key] = +obj[key];
});
return pickedProperties;
};
const canonicalize = (attrs = {}) => {
const newAttrs = {};
Object.keys(attrs).forEach((k) => {
newAttrs[k.toLowerCase()] = attrs[k];
});
return newAttrs;
};
//# sourceMappingURL=layout.js.map