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.

210 lines
8.6 KiB

4 months ago
import { __awaiter } from "tslib";
import { isFunction, isNumber, isObject, isString } from '@antv/util';
import { cloneFormatData, isArray } from './util';
import { handleSingleNodeGraph } from './util/common';
import { parseSize } from './util/size';
const DEFAULTS_LAYOUT_OPTIONS = {
nodeSize: 30,
nodeSpacing: 10,
preventOverlap: false,
sweep: undefined,
equidistant: false,
startAngle: (3 / 2) * Math.PI,
clockwise: true,
maxLevelDiff: undefined,
sortBy: 'degree',
};
/**
* <zh/> 同心圆布局
*
* <en/> Concentric layout
*/
export class ConcentricLayout {
constructor(options = {}) {
this.options = options;
this.id = 'concentric';
this.options = Object.assign(Object.assign({}, DEFAULTS_LAYOUT_OPTIONS), options);
}
/**
* Return the positions of nodes and edges(if needed).
*/
execute(graph, options) {
return __awaiter(this, void 0, void 0, function* () {
return this.genericConcentricLayout(false, graph, options);
});
}
/**
* To directly assign the positions to the nodes.
*/
assign(graph, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.genericConcentricLayout(true, graph, options);
});
}
genericConcentricLayout(assign, graph, options) {
return __awaiter(this, void 0, void 0, function* () {
const mergedOptions = Object.assign(Object.assign({}, this.options), options);
const { center: propsCenter, width: propsWidth, height: propsHeight, sortBy: propsSortBy, maxLevelDiff: propsMaxLevelDiff, sweep: propsSweep, clockwise, equidistant, preventOverlap, startAngle = (3 / 2) * Math.PI, nodeSize, nodeSpacing, } = mergedOptions;
const nodes = graph.getAllNodes();
const edges = graph.getAllEdges();
const width = !propsWidth && typeof window !== 'undefined'
? window.innerWidth
: propsWidth;
const height = !propsHeight && typeof window !== 'undefined'
? window.innerHeight
: propsHeight;
const center = (!propsCenter ? [width / 2, height / 2] : propsCenter);
if (!(nodes === null || nodes === void 0 ? void 0 : nodes.length) || nodes.length === 1) {
return handleSingleNodeGraph(graph, assign, center);
}
const layoutNodes = [];
let maxNodeSize;
let maxNodeSpacing = 0;
if (isArray(nodeSize)) {
maxNodeSize = Math.max(nodeSize[0], nodeSize[1]);
}
else if (isFunction(nodeSize)) {
maxNodeSize = -Infinity;
nodes.forEach((node) => {
const currentSize = Math.max(...parseSize(nodeSize(node)));
if (currentSize > maxNodeSize)
maxNodeSize = currentSize;
});
}
else {
maxNodeSize = nodeSize;
}
if (isArray(nodeSpacing)) {
maxNodeSpacing = Math.max(nodeSpacing[0], nodeSpacing[1]);
}
else if (isNumber(nodeSpacing)) {
maxNodeSpacing = nodeSpacing;
}
nodes.forEach((node) => {
const cnode = cloneFormatData(node);
layoutNodes.push(cnode);
let nodeSize = maxNodeSize;
const { data } = cnode;
if (isArray(data.size)) {
nodeSize = Math.max(data.size[0], data.size[1]);
}
else if (isNumber(data.size)) {
nodeSize = data.size;
}
else if (isObject(data.size)) {
nodeSize = Math.max(data.size.width, data.size.height);
}
maxNodeSize = Math.max(maxNodeSize, nodeSize);
if (isFunction(nodeSpacing)) {
maxNodeSpacing = Math.max(nodeSpacing(node), maxNodeSpacing);
}
});
// layout
const nodeIdxMap = {};
layoutNodes.forEach((node, i) => {
nodeIdxMap[node.id] = i;
});
// get the node degrees
let sortBy = propsSortBy;
if (!isString(sortBy) ||
layoutNodes[0].data[sortBy] === undefined) {
sortBy = 'degree';
}
if (sortBy === 'degree') {
layoutNodes.sort((n1, n2) => graph.getDegree(n2.id, 'both') - graph.getDegree(n1.id, 'both'));
}
else {
// sort nodes by value
layoutNodes.sort((n1, n2) => n2.data[sortBy] - n1.data[sortBy]);
}
const maxValueNode = layoutNodes[0];
const maxLevelDiff = (propsMaxLevelDiff ||
(sortBy === 'degree'
? graph.getDegree(maxValueNode.id, 'both')
: maxValueNode.data[sortBy])) / 4;
// put the values into levels
const levels = [{ nodes: [] }];
let currentLevel = levels[0];
layoutNodes.forEach((node) => {
if (currentLevel.nodes.length > 0) {
const diff = sortBy === 'degree'
? Math.abs(graph.getDegree(currentLevel.nodes[0].id, 'both') -
graph.getDegree(node.id, 'both'))
: Math.abs(currentLevel.nodes[0].data[sortBy] -
node.data[sortBy]);
if (maxLevelDiff && diff >= maxLevelDiff) {
currentLevel = { nodes: [] };
levels.push(currentLevel);
}
}
currentLevel.nodes.push(node);
});
// create positions for levels
let minDist = maxNodeSize + maxNodeSpacing; // min dist between nodes
if (!preventOverlap) {
// then strictly constrain to bb
const firstLvlHasMulti = levels.length > 0 && levels[0].nodes.length > 1;
const maxR = Math.min(width, height) / 2 - minDist;
const rStep = maxR / (levels.length + (firstLvlHasMulti ? 1 : 0));
minDist = Math.min(minDist, rStep);
}
// find the metrics for each level
let r = 0;
levels.forEach((level) => {
const sweep = propsSweep === undefined
? 2 * Math.PI - (2 * Math.PI) / level.nodes.length
: propsSweep;
level.dTheta = sweep / Math.max(1, level.nodes.length - 1);
// calculate the radius
if (level.nodes.length > 1 && preventOverlap) {
// but only if more than one node (can't overlap)
const dcos = Math.cos(level.dTheta) - Math.cos(0);
const dsin = Math.sin(level.dTheta) - Math.sin(0);
const rMin = Math.sqrt((minDist * minDist) / (dcos * dcos + dsin * dsin)); // s.t. no nodes overlapping
r = Math.max(rMin, r);
}
level.r = r;
r += minDist;
});
if (equidistant) {
let rDeltaMax = 0;
let rr = 0;
for (let i = 0; i < levels.length; i++) {
const level = levels[i];
const rDelta = (level.r || 0) - rr;
rDeltaMax = Math.max(rDeltaMax, rDelta);
}
rr = 0;
levels.forEach((level, i) => {
if (i === 0) {
rr = level.r || 0;
}
level.r = rr;
rr += rDeltaMax;
});
}
// calculate the node positions
levels.forEach((level) => {
const dTheta = level.dTheta || 0;
const rr = level.r || 0;
level.nodes.forEach((node, j) => {
const theta = startAngle + (clockwise ? 1 : -1) * dTheta * j;
node.data.x = center[0] + rr * Math.cos(theta);
node.data.y = center[1] + rr * Math.sin(theta);
});
});
if (assign) {
layoutNodes.forEach((node) => graph.mergeNodeData(node.id, {
x: node.data.x,
y: node.data.y,
}));
}
const result = {
nodes: layoutNodes,
edges,
};
return result;
});
}
}
//# sourceMappingURL=concentric.js.map