UNPKG

@antv/g6

Version:

graph visualization frame work

184 lines (167 loc) 5.96 kB
/** * @fileOverview concentric layout * @author shiwu.wyy@antfin.com * this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/ */ const Layout = require('./layout'); const isString = require('@antv/util/lib/type/is-string'); function getDegree(n, nodeIdxMap, edges) { const degrees = []; for (let i = 0; i < n; i++) { degrees[i] = 0; } edges.forEach(e => { degrees[nodeIdxMap.get(e.source)] += 1; degrees[nodeIdxMap.get(e.target)] += 1; }); return degrees; } /** * 同心圆布局 */ Layout.registerLayout('concentric', { getDefaultCfg() { return { center: [ 0, 0 ], // 布局中心 nodeSize: 30, minNodeSpacing: 10, // min spacing between outside of nodes (used for radius adjustment) preventOverlap: false, // prevents node overlap, may overflow boundingBox if not enough space sweep: undefined, // how many radians should be between the first and last node (defaults to full circle) equidistant: false, // whether levels have an equal radial distance betwen them, may cause bounding box overflow startAngle: 3 / 2 * Math.PI, // where nodes start in radians clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false) maxLevelDiff: undefined, // the letiation of concentric values in each level sortBy: 'degree' // 根据 sortBy 指定的属性进行排布,数值高的放在中心。如果是 sortBy 则会计算节点度数,度数最高的放在中心。 }; }, /** * 执行布局 */ execute() { const self = this; const nodes = self.nodes; const edges = self.edges; const n = nodes.length; const center = self.center; if (n === 0) { return; } else if (n === 1) { nodes[0].x = center[0]; nodes[0].y = center[1]; return; } const layoutNodes = []; let maxNodeSize; if (isNaN(self.nodeSize)) { maxNodeSize = Math.max(self.nodeSize[0], self.nodeSize[1]); } else { maxNodeSize = self.nodeSize; } nodes.forEach(node => { layoutNodes.push(node); let nodeSize; if (isNaN(node.size)) { nodeSize = Math.max(node.size[0], node.size[1]); } else { nodeSize = node.size; } maxNodeSize = Math.max(maxNodeSize, nodeSize); }); let width = self.width; if (!width && typeof window !== 'undefined') { width = window.innerWidth; } let height = self.height; if (!height && typeof height !== 'undefined') { height = window.innerHeight; } self.clockwise = self.counterclockwise !== undefined ? !self.counterclockwise : self.clockwise; // layout const nodeMap = new Map(); const nodeIdxMap = new Map(); layoutNodes.forEach((node, i) => { nodeMap.set(node.id, node); nodeIdxMap.set(node.id, i); }); self.nodeMap = nodeMap; // get the node degrees if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) { self.sortBy = 'degree'; if (isNaN(nodes[0].degree)) { const values = getDegree(nodes.length, nodeIdxMap, edges); layoutNodes.forEach((node, i) => { node.degree = values[i]; }); } } // sort nodes by value layoutNodes.sort((n1, n2) => { return n2[self.sortBy] - n1[self.sortBy]; }); self.maxValueNode = layoutNodes[0]; self.maxLevelDiff = self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4; // 0.5; // 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 (diff >= self.maxLevelDiff) { currentLevel = []; levels.push(currentLevel); } } currentLevel.push(node); }); // create positions for levels let minDist = maxNodeSize + 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 => { const sweep = self.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / level.length : self.sweep; 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 r = 0; for (let i = 0; i < levels.length; i++) { const level = levels[ i ]; const rDelta = level.r - r; rDeltaMax = Math.max(rDeltaMax, rDelta); } r = 0; levels.forEach((level, i) => { if (i === 0) { r = level.r; } level.r = r; r += rDeltaMax; }); } // calculate the node positions levels.forEach(level => { const dTheta = level.dTheta; const r = level.r; level.forEach((node, j) => { const theta = self.startAngle + (self.clockwise ? 1 : -1) * dTheta * j; node.x = center[0] + r * Math.cos(theta); node.y = center[1] + r * Math.sin(theta); }); }); } });