219 lines
7.9 KiB
JavaScript
219 lines
7.9 KiB
JavaScript
/**
|
|
* @fileOverview concentric layout
|
|
* @author shiwu.wyy@antfin.com
|
|
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
|
|
*/
|
|
import { isString, isArray, isNumber, getDegree, isObject, isFunction } from "../util";
|
|
import { Base } from "./base";
|
|
/**
|
|
* 同心圆布局
|
|
*/
|
|
export class ConcentricLayout extends Base {
|
|
constructor(options) {
|
|
super();
|
|
this.nodeSize = 30;
|
|
/** min spacing between outside of nodes (used for radius adjustment) */
|
|
this.minNodeSpacing = 10;
|
|
/** same as minNodeSpacing. min spacing between outside of nodes (used for radius adjustment) */
|
|
this.nodeSpacing = 10;
|
|
/** prevents node overlap, may overflow boundingBox if not enough space */
|
|
this.preventOverlap = false;
|
|
/** whether levels have an equal radial distance betwen them, may cause bounding box overflow */
|
|
this.equidistant = false;
|
|
/** where nodes start in radians */
|
|
this.startAngle = (3 / 2) * Math.PI;
|
|
/** whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false) */
|
|
this.clockwise = true;
|
|
/** 根据 sortBy 指定的属性进行排布,数值高的放在中心,如果是 sortBy 则会计算节点度数,度数最高的放在中心 */
|
|
this.sortBy = "degree";
|
|
this.nodes = [];
|
|
this.edges = [];
|
|
this.width = 300;
|
|
this.height = 300;
|
|
/** 迭代结束的回调函数 */
|
|
this.onLayoutEnd = () => { };
|
|
this.updateCfg(options);
|
|
}
|
|
getDefaultCfg() {
|
|
return {
|
|
nodeSize: 30,
|
|
minNodeSpacing: 10,
|
|
nodeSpacing: 10,
|
|
preventOverlap: false,
|
|
sweep: undefined,
|
|
equidistant: false,
|
|
startAngle: (3 / 2) * Math.PI,
|
|
clockwise: true,
|
|
maxLevelDiff: undefined,
|
|
sortBy: "degree"
|
|
};
|
|
}
|
|
/**
|
|
* 执行布局
|
|
*/
|
|
execute() {
|
|
var _a, _b;
|
|
const self = this;
|
|
const { nodes, edges } = self;
|
|
const n = nodes.length;
|
|
if (n === 0) {
|
|
(_a = self.onLayoutEnd) === null || _a === void 0 ? void 0 : _a.call(self);
|
|
return;
|
|
}
|
|
if (!self.width && typeof window !== "undefined") {
|
|
self.width = window.innerWidth;
|
|
}
|
|
if (!self.height && typeof window !== "undefined") {
|
|
self.height = window.innerHeight;
|
|
}
|
|
if (!self.center) {
|
|
self.center = [self.width / 2, self.height / 2];
|
|
}
|
|
const center = self.center;
|
|
if (n === 1) {
|
|
nodes[0].x = center[0];
|
|
nodes[0].y = center[1];
|
|
(_b = self.onLayoutEnd) === null || _b === void 0 ? void 0 : _b.call(self);
|
|
return;
|
|
}
|
|
const { nodeSize, nodeSpacing } = self;
|
|
const layoutNodes = [];
|
|
let maxNodeSize;
|
|
let maxNodeSpacing = 0;
|
|
if (isArray(nodeSize)) {
|
|
maxNodeSize = Math.max(nodeSize[0], nodeSize[1]);
|
|
}
|
|
else {
|
|
maxNodeSize = nodeSize;
|
|
}
|
|
if (isArray(nodeSpacing)) {
|
|
maxNodeSpacing = Math.max(nodeSpacing[0], nodeSpacing[1]);
|
|
}
|
|
else if (isNumber(nodeSpacing)) {
|
|
maxNodeSpacing = nodeSpacing;
|
|
}
|
|
nodes.forEach((node) => {
|
|
layoutNodes.push(node);
|
|
let nodeSize = maxNodeSize;
|
|
if (isArray(node.size)) {
|
|
nodeSize = Math.max(node.size[0], node.size[1]);
|
|
}
|
|
else if (isNumber(node.size)) {
|
|
nodeSize = node.size;
|
|
}
|
|
else if (isObject(node.size)) {
|
|
nodeSize = Math.max(node.size.width, node.size.height);
|
|
}
|
|
maxNodeSize = Math.max(maxNodeSize, nodeSize);
|
|
if (isFunction(nodeSpacing)) {
|
|
maxNodeSpacing = Math.max(nodeSpacing(node), maxNodeSpacing);
|
|
}
|
|
});
|
|
self.clockwise =
|
|
self.counterclockwise !== undefined
|
|
? !self.counterclockwise
|
|
: self.clockwise;
|
|
// layout
|
|
const nodeMap = {};
|
|
const indexMap = {};
|
|
layoutNodes.forEach((node, i) => {
|
|
nodeMap[node.id] = node;
|
|
indexMap[node.id] = i;
|
|
});
|
|
// get the node degrees
|
|
if (self.sortBy === "degree" ||
|
|
!isString(self.sortBy) ||
|
|
layoutNodes[0][self.sortBy] === undefined) {
|
|
self.sortBy = "degree";
|
|
if (!isNumber(nodes[0].degree)) {
|
|
const values = getDegree(nodes.length, indexMap, edges);
|
|
layoutNodes.forEach((node, i) => {
|
|
node.degree = values[i].all;
|
|
});
|
|
}
|
|
}
|
|
// sort nodes by value
|
|
layoutNodes.sort((n1, n2) => n2[self.sortBy] - n1[self.sortBy]);
|
|
self.maxValueNode = layoutNodes[0];
|
|
self.maxLevelDiff =
|
|
self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4;
|
|
// put the values into levels
|
|
const levels = [[]];
|
|
let currentLevel = levels[0];
|
|
layoutNodes.forEach((node) => {
|
|
if (currentLevel.length > 0) {
|
|
const diff = Math.abs(currentLevel[0][self.sortBy] - node[self.sortBy]);
|
|
if (self.maxLevelDiff && diff >= self.maxLevelDiff) {
|
|
currentLevel = [];
|
|
levels.push(currentLevel);
|
|
}
|
|
}
|
|
currentLevel.push(node);
|
|
});
|
|
// create positions for levels
|
|
let minDist = maxNodeSize + (maxNodeSpacing || self.minNodeSpacing); // min dist between nodes
|
|
if (!self.preventOverlap) {
|
|
// then strictly constrain to bb
|
|
const firstLvlHasMulti = levels.length > 0 && levels[0].length > 1;
|
|
const maxR = Math.min(self.width, self.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) => {
|
|
let sweep = self.sweep;
|
|
if (sweep === undefined) {
|
|
sweep = 2 * Math.PI - (2 * Math.PI) / level.length;
|
|
}
|
|
const dTheta = (level.dTheta = sweep / Math.max(1, level.length - 1));
|
|
// calculate the radius
|
|
if (level.length > 1 && self.preventOverlap) {
|
|
// but only if more than one node (can't overlap)
|
|
const dcos = Math.cos(dTheta) - Math.cos(0);
|
|
const dsin = Math.sin(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 (self.equidistant) {
|
|
let rDeltaMax = 0;
|
|
let rr = 0;
|
|
for (let i = 0; i < levels.length; i++) {
|
|
const level = levels[i];
|
|
const rDelta = level.r - rr;
|
|
rDeltaMax = Math.max(rDeltaMax, rDelta);
|
|
}
|
|
rr = 0;
|
|
levels.forEach((level, i) => {
|
|
if (i === 0) {
|
|
rr = level.r;
|
|
}
|
|
level.r = rr;
|
|
rr += rDeltaMax;
|
|
});
|
|
}
|
|
// calculate the node positions
|
|
levels.forEach((level) => {
|
|
const dTheta = level.dTheta;
|
|
const rr = level.r;
|
|
level.forEach((node, j) => {
|
|
const theta = self.startAngle + (self.clockwise ? 1 : -1) * dTheta * j;
|
|
node.x = center[0] + rr * Math.cos(theta);
|
|
node.y = center[1] + rr * Math.sin(theta);
|
|
});
|
|
});
|
|
if (self.onLayoutEnd)
|
|
self.onLayoutEnd();
|
|
return {
|
|
nodes,
|
|
edges
|
|
};
|
|
}
|
|
getType() {
|
|
return "concentric";
|
|
}
|
|
}
|
|
//# sourceMappingURL=concentric.js.map
|