UNPKG

@antv/layout

Version:
463 lines (460 loc) 17.7 kB
import { __awaiter } from '../../node_modules/tslib/tslib.es6.js'; import { initNodePosition } from '../../model/data.js'; import '../../node_modules/@antv/expr/dist/index.esm.js'; import { formatFn, formatNumberFn, formatNodeSizeFn } from '../../util/format.js'; import { normalizeViewport } from '../../util/viewport.js'; import { BaseLayoutWithIterations } from '../base-layout.js'; import { forceAttractive } from './attractive.js'; import { forceCentripetal } from './centripetal.js'; import { forceCollide } from './collide.js'; import { forceGravity } from './gravity.js'; import { forceRepulsive } from './repulsive.js'; import { ForceSimulation } from './simulation.js'; import isEmpty from '../../node_modules/@antv/util/esm/lodash/is-empty.js'; const DEFAULTS_LAYOUT_OPTIONS$5 = { nodeSize: 30, dimensions: 2, maxIteration: 500, gravity: 10, factor: 1, edgeStrength: 50, nodeStrength: 1000, coulombDisScale: 0.005, damping: 0.9, maxSpeed: 200, minMovement: 0.4, interval: 0.02, linkDistance: 200, clusterNodeStrength: 20, collideStrength: 1, preventOverlap: true, distanceThresholdMode: 'mean', }; /** * <zh/> 力导向布局 (Force) * * <en/> Force-directed layout (Force) * * @remarks * <zh/> 基于自定义物理模拟的力导向布局,使用库伦定律计算斥力,胡克定律计算引力 * * <en/> Force-directed layout based on custom physics simulation, using Coulomb's law for repulsion and Hooke's law for attraction */ class ForceLayout extends BaseLayoutWithIterations { constructor() { super(...arguments); this.id = 'force'; this.simulation = null; } getDefaultOptions() { return DEFAULTS_LAYOUT_OPTIONS$5; } layout() { var _a; return __awaiter(this, void 0, void 0, function* () { const options = this.parseOptions(this.options); const { width, height, dimensions } = options; this.initializePhysicsData(this.model, options); initNodePosition(this.model, width, height, dimensions); if (!((_a = this.model.nodes()) === null || _a === void 0 ? void 0 : _a.length)) return; const simulation = this.setSimulation(options); simulation.data(this.model); simulation.initialize(options); simulation.restart(); return new Promise((resolve) => { simulation.on('end', () => resolve()); }); }); } /** * Initialize physics properties on model nodes and edges */ initializePhysicsData(model, options) { const { nodeSize, getMass, nodeStrength, edgeStrength, linkDistance } = options; model.forEachNode((node) => { const raw = node._original; node.size = nodeSize(raw); node.mass = getMass(raw); node.nodeStrength = nodeStrength(raw); }); model.forEachEdge((edge) => { const raw = edge._original; edge.edgeStrength = edgeStrength(raw); edge.linkDistance = linkDistance(raw, model.originalNode(edge.source), model.originalNode(edge.target)); }); } /** * Setup simulation and forces */ setSimulation(options) { const simulation = this.simulation || new ForceSimulation(); if (!this.simulation) { this.simulation = simulation.on('tick', () => { var _a; (_a = options.onTick) === null || _a === void 0 ? void 0 : _a.call(options, this); }); } // Setup all forces this.setupRepulsiveForce(simulation, options); this.setupAttractiveForce(simulation, options); this.setupCollideForce(simulation, options); this.setupGravityForce(simulation, options); this.setupCentripetalForce(simulation, options); return simulation; } /** * Setup repulsive force (Coulomb's law) */ setupRepulsiveForce(simulation, options) { const { factor, coulombDisScale, dimensions } = options; let force = simulation.force('repulsive'); if (!force) { force = forceRepulsive(factor, coulombDisScale, dimensions); simulation.force('repulsive', force); } if (force.factor) force.factor(factor); if (force.coulombDisScale) force.coulombDisScale(coulombDisScale); if (force.dimensions) force.dimensions(dimensions); } /** * Setup attractive force (Hooke's law) */ setupAttractiveForce(simulation, options) { const { dimensions, preventOverlap } = options; const edges = this.model.edges() || []; if (edges.length > 0) { let force = simulation.force('attractive'); if (!force) { force = forceAttractive(dimensions); simulation.force('attractive', force); } if (force.dimensions) force.dimensions(dimensions); if (force.preventOverlap) force.preventOverlap(preventOverlap); } else { simulation.force('attractive', null); } } /** * Setup gravity force toward center */ setupGravityForce(simulation, options) { const { center, gravity, getCenter } = options; if (gravity) { let force = simulation.force('gravity'); if (!force) { force = forceGravity(center, gravity); simulation.force('gravity', force); } if (force.center) force.center(center); if (force.gravity) force.gravity(gravity); if (force.getCenter) force.getCenter(getCenter); } else { simulation.force('gravity', null); } } /** * Setup collision force to prevent overlap */ setupCollideForce(simulation, options) { const { preventOverlap, collideStrength = 1, dimensions } = options; if (preventOverlap && collideStrength) { let force = simulation.force('collide'); if (!force) { force = forceCollide(dimensions); simulation.force('collide', force); } if (force.strength) force.strength(collideStrength); if (force.dimensions) force.dimensions(dimensions); } else { simulation.force('collide', null); } } /** * Setup centripetal force (unique to Force) */ setupCentripetalForce(simulation, options) { const { centripetalOptions, width, height } = options; if (centripetalOptions) { let force = simulation.force('centripetal'); if (!force) { force = forceCentripetal(); simulation.force('centripetal', force); } if (force.options) force.options(centripetalOptions); if (force.width) force.width(width); if (force.height) force.height(height); } else { simulation.force('centripetal', null); } } /** * Parse and format options */ parseOptions(options) { const _ = Object.assign(Object.assign({}, options), normalizeViewport(options)); // Format nodeClusterBy (for clustering / leafCluster) if (_.nodeClusterBy) { _.nodeClusterBy = formatFn(_.nodeClusterBy, ['node']); } // Format node mass if (!options.getMass) { _.getMass = (node) => { if (!node) return 1; const massWeight = 1; const degree = this.model.degree(node.id, 'both'); return !degree || degree < 5 ? massWeight : degree * 5 * massWeight; }; } else { _.getMass = formatNumberFn(options.getMass, 1); } // Format per-node center force callback if (options.getCenter) { const params = ['node', 'degree']; _.getCenter = formatFn(options.getCenter, params); } // Format node size const nodeSizeVec = formatNodeSizeFn(options.nodeSize, options.nodeSpacing); _.nodeSize = (node) => { if (!node) return 0; const [w, h, z] = nodeSizeVec(node); return Math.max(w, h, z); }; // Format node / edge strengths _.linkDistance = options.linkDistance ? formatFn(options.linkDistance, ['edge', 'source', 'target']) : (_, source, target) => 1 + _.nodeSize(source) + _.nodeSize(target); _.nodeStrength = formatNumberFn(options.nodeStrength, 1); _.edgeStrength = formatNumberFn(options.edgeStrength, 1, 'edge'); _.clusterNodeStrength = formatNumberFn(options.clusterNodeStrength, 1); // Format centripetal options this.formatCentripetal(_); return _; } /** * Format centripetal options */ formatCentripetal(options) { var _a, _b; const { dimensions, centripetalOptions, center, leafCluster, clustering, nodeClusterBy, } = options; const leafParams = ['node', 'nodes', 'edges']; const leafFn = formatFn(centripetalOptions === null || centripetalOptions === void 0 ? void 0 : centripetalOptions.leaf, leafParams); const singleFn = formatNumberFn(centripetalOptions === null || centripetalOptions === void 0 ? void 0 : centripetalOptions.single, 2); const othersFn = formatNumberFn(centripetalOptions === null || centripetalOptions === void 0 ? void 0 : centripetalOptions.others, 1); const centerRaw = (_a = centripetalOptions === null || centripetalOptions === void 0 ? void 0 : centripetalOptions.center) !== null && _a !== void 0 ? _a : ((_) => { return { x: center[0], y: center[1], z: dimensions === 3 ? center[2] : undefined, }; }); const centerFn = formatFn(centerRaw, [ 'node', 'nodes', 'edges', 'width', 'height', ]); const basicCentripetal = Object.assign(Object.assign({}, centripetalOptions), { leaf: leafFn, single: singleFn, others: othersFn, center: centerFn }); // If user provided centripetalOptions, normalize them even without clustering modes. if (centripetalOptions) { options.centripetalOptions = basicCentripetal; } let sameTypeLeafMap; let clusters; // Leaf cluster mode if (leafCluster && nodeClusterBy) { sameTypeLeafMap = this.getSameTypeLeafMap(nodeClusterBy); clusters = Array.from(new Set((_b = this.model .nodes()) === null || _b === void 0 ? void 0 : _b.map((node) => nodeClusterBy(node._original)))) || []; options.centripetalOptions = Object.assign({}, basicCentripetal, { single: () => 100, leaf: (node) => { const { siblingLeaves, sameTypeLeaves } = sameTypeLeafMap[node.id] || {}; if ((sameTypeLeaves === null || sameTypeLeaves === void 0 ? void 0 : sameTypeLeaves.length) === (siblingLeaves === null || siblingLeaves === void 0 ? void 0 : siblingLeaves.length) || (clusters === null || clusters === void 0 ? void 0 : clusters.length) === 1) { return 1; } return options.clusterNodeStrength(node); }, others: () => 1, center: (node) => { const degree = this.model.degree(node.id, 'both'); if (!degree) { return { x: 100, y: 100, z: 0 }; } let centerPos; if (degree === 1) { const { sameTypeLeaves = [] } = sameTypeLeafMap[node.id] || {}; if (sameTypeLeaves.length === 1) { centerPos = undefined; } else if (sameTypeLeaves.length > 1) { centerPos = this.getAvgNodePosition(sameTypeLeaves); } } else { centerPos = undefined; } return { x: centerPos === null || centerPos === void 0 ? void 0 : centerPos.x, y: centerPos === null || centerPos === void 0 ? void 0 : centerPos.y, z: centerPos === null || centerPos === void 0 ? void 0 : centerPos.z, }; }, }); } // Full clustering mode if (clustering && nodeClusterBy) { if (!sameTypeLeafMap) { sameTypeLeafMap = this.getSameTypeLeafMap(nodeClusterBy); } let clusters = []; if (isEmpty(clusters)) { this.model.forEachNode((node) => { const cluster = nodeClusterBy(node._original); if (cluster && !clusters.includes(cluster)) { clusters.push(cluster); } }); } const centerInfo = {}; clusters.forEach((cluster) => { const sameTypeNodes = this.model .nodes() .filter((node) => nodeClusterBy(node._original) === cluster); centerInfo[cluster] = this.getAvgNodePosition(sameTypeNodes); }); options.centripetalOptions = Object.assign(basicCentripetal, { single: (node) => options.clusterNodeStrength(node), leaf: (node) => options.clusterNodeStrength(node), others: (node) => options.clusterNodeStrength(node), center: (node) => { const centerPos = centerInfo[nodeClusterBy(node._original)]; return { x: centerPos === null || centerPos === void 0 ? void 0 : centerPos.x, y: centerPos === null || centerPos === void 0 ? void 0 : centerPos.y, z: centerPos === null || centerPos === void 0 ? void 0 : centerPos.z, }; }, }); } } /** * Get same type leaf map for clustering */ getSameTypeLeafMap(nodeClusterBy) { const sameTypeLeafMap = {}; this.model.forEachNode((node) => { const degree = this.model.degree(node.id, 'both'); if (degree === 1) { sameTypeLeafMap[node.id] = this.getCoreNodeAndSiblingLeaves(node, nodeClusterBy); } }); return sameTypeLeafMap; } /** * Get core node and sibling leaves */ getCoreNodeAndSiblingLeaves(node, nodeClusterBy) { const inDegree = this.model.degree(node.id, 'in'); const outDegree = this.model.degree(node.id, 'out'); let coreNode = node; let siblingLeaves = []; if (inDegree === 0) { const successors = this.model.successors(node.id); coreNode = this.model.node(successors[0]); siblingLeaves = this.model .neighbors(coreNode.id) .map((id) => this.model.node(id)); } else if (outDegree === 0) { const predecessors = this.model.predecessors(node.id); coreNode = this.model.node(predecessors[0]); siblingLeaves = this.model .neighbors(coreNode.id) .map((id) => this.model.node(id)); } siblingLeaves = siblingLeaves.filter((n) => this.model.degree(n.id, 'in') === 0 || this.model.degree(n.id, 'out') === 0); const typeName = nodeClusterBy(node._original) || ''; const sameTypeLeaves = siblingLeaves.filter((item) => nodeClusterBy(item._original) === typeName && (this.model.degree(item.id, 'in') === 0 || this.model.degree(item.id, 'out') === 0)); return { coreNode, siblingLeaves, sameTypeLeaves }; } /** * Get average position of nodes */ getAvgNodePosition(nodes) { const totalNodes = { x: 0, y: 0 }; nodes.forEach((node) => { totalNodes.x += node.x || 0; totalNodes.y += node.y || 0; }); const n = nodes.length || 1; return { x: totalNodes.x / n, y: totalNodes.y / n, }; } /** * Manually step the simulation */ tick(iterations = 1) { if (this.simulation) { this.simulation.tick(iterations); } return this; } /** * Stop the simulation */ stop() { if (this.simulation) { this.simulation.stop(); } return this; } /** * Restart the simulation */ restart() { if (this.simulation) { this.simulation.restart(); } return this; } /** * Set fixed position for a node */ setFixedPosition(nodeId, position) { if (this.simulation) { this.simulation.setFixedPosition(nodeId, position); } return this; } } export { ForceLayout }; //# sourceMappingURL=index.js.map