UNPKG

@antv/layout

Version:
358 lines (355 loc) 12.7 kB
import '../../node_modules/@antv/expr/dist/index.esm.js'; import { normalizeViewport } from '../../util/viewport.js'; import { BaseSimulation } from '../base-simulation.js'; import isNil from '../../node_modules/@antv/util/esm/lodash/is-nil.js'; const SPEED_DIVISOR$1 = 800; /** * Fruchterman Simulation */ class Simulation extends BaseSimulation { constructor() { super(...arguments); this.displacements = null; this.clusterMap = null; } data(model) { this.model = model; return this; } initialize(options) { super.initialize(options); this.recomputeConstants(); this.displacements = null; this.clusterMap = null; this.initDisplacements(); } recomputeConstants() { const { model, options } = this; const { width, height } = normalizeViewport(options); const area = width * height; this.k2 = area / (model.nodeCount() + 1); this.k = Math.sqrt(this.k2); this.maxDisplace = Math.sqrt(area) / 10; } runOneStep() { this.syncFixedPositions(); this.initDisplacements(); this.calculateRepulsive(); this.calculateAttractive(); this.applyClusterGravity(); this.applyGlobalGravity(); return this.updatePositions(); } /** * Fixes the position of the node with the given id to the specified position. */ setFixedPosition(id, position) { const node = this.model.node(id); if (!node) return; const keys = ['fx', 'fy', 'fz']; if (position === null) { // Unset fixed position keys.forEach((key) => { delete node[key]; }); return; } position.forEach((value, index) => { if (index < keys.length && (typeof value === 'number' || value === null)) { node[keys[index]] = value; } }); } /** * Determines whether a node is fixed (has fx and fy defined). */ isNodeFixed(node) { return !isNil(node.fx) && !isNil(node.fy); } /** * Synchronizes fixed node positions (fx/fy -> x/y) */ syncFixedPositions() { const { model, options } = this; const is3D = options.dimensions === 3; model.forEachNode((node) => { if (this.isNodeFixed(node)) { node.x = node.fx; node.y = node.fy; if (is3D && node.fz !== undefined) { node.z = node.fz; } } }); } initDisplacements() { if (!this.displacements) { this.displacements = new Map(); this.model.forEachNode((node) => { this.displacements.set(node.id, { x: 0, y: 0, z: 0 }); }); } this.displacements.forEach((displacement) => { displacement.x = 0; displacement.y = 0; displacement.z = 0; }); } /** * Calculates repulsive forces */ calculateRepulsive() { const { model, options } = this; const is3D = options.dimensions === 3; const nodes = model.nodes(); for (let i = 0; i < nodes.length; i++) { const nodeV = nodes[i]; const dispV = this.displacements.get(nodeV.id); const vFixed = this.isNodeFixed(nodeV); for (let j = i + 1; j < nodes.length; j++) { const nodeU = nodes[j]; const dispU = this.displacements.get(nodeU.id); const uFixed = this.isNodeFixed(nodeU); let vecX = nodeV.x - nodeU.x; let vecY = nodeV.y - nodeU.y; let vecZ = is3D ? nodeV.z - nodeU.z : 0; let lengthSqr = vecX * vecX + vecY * vecY + vecZ * vecZ; if (lengthSqr === 0) { lengthSqr = 1; vecX = 0.01; vecY = 0.01; vecZ = 0.01; } const common = this.k2 / lengthSqr; const dispX = vecX * common; const dispY = vecY * common; const dispZ = vecZ * common; if (!vFixed && !uFixed) { // 两个都不固定:正常分配 dispV.x += dispX; dispV.y += dispY; dispU.x -= dispX; dispU.y -= dispY; if (is3D) { dispV.z += dispZ; dispU.z -= dispZ; } } else if (vFixed && !uFixed) { // V 固定,U 不固定:U 承受双倍位移 dispU.x -= dispX * 2; dispU.y -= dispY * 2; if (is3D) { dispU.z -= dispZ * 2; } } else if (!vFixed && uFixed) { // U 固定,V 不固定:V 承受双倍位移 dispV.x += dispX * 2; dispV.y += dispY * 2; if (is3D) { dispV.z += dispZ * 2; } } // 如果两个都固定,则都不移动(不添加位移) } } } calculateAttractive() { const { model, options } = this; const is3D = options.dimensions === 3; model.forEachEdge((edge) => { const { source, target } = edge; if (!source || !target || source === target) { return; } const u = model.node(source); const v = model.node(target); const dispSource = this.displacements.get(source); const dispTarget = this.displacements.get(target); const fixedU = this.isNodeFixed(u); const fixedV = this.isNodeFixed(v); const vecX = v.x - u.x; const vecY = v.y - u.y; const vecZ = is3D ? v.z - u.z : 0; const length = Math.sqrt(vecX * vecX + vecY * vecY + vecZ * vecZ); if (length === 0) return; const common = length / this.k; const dispX = vecX * common; const dispY = vecY * common; const dispZ = vecZ * common; if (!fixedU && !fixedV) { // 两个都不固定:正常分配 dispSource.x += dispX; dispSource.y += dispY; dispTarget.x -= dispX; dispTarget.y -= dispY; if (is3D) { dispSource.z += dispZ; dispTarget.z -= dispZ; } } else if (fixedU && !fixedV) { // V 固定,U 不固定:U 承受双倍位移 dispTarget.x -= dispX * 2; dispTarget.y -= dispY * 2; if (is3D) { dispTarget.z -= dispZ * 2; } } else if (!fixedU && fixedV) { // U 固定,V 不固定:V 承受双倍位移 dispSource.x += dispX * 2; dispSource.y += dispY * 2; if (is3D) { dispSource.z += dispZ * 2; } } // 如果两个都固定,则都不移动(不添加位移) }); } applyClusterGravity() { const { model, options } = this; const { nodeClusterBy, clusterGravity, dimensions, clustering } = options; if (!clustering) return; if (!this.clusterMap) { this.clusterMap = new Map(); model.forEachNode((node) => { const clusterKey = nodeClusterBy(model.originalNode(node.id)); if (!this.clusterMap.has(clusterKey)) { this.clusterMap.set(clusterKey, { name: clusterKey, cx: 0, cy: 0, cz: 0, count: 0, }); } }); } if (this.clusterMap.size === 0) return; const is3D = dimensions === 3; this.clusterMap.forEach((cluster) => { cluster.cx = 0; cluster.cy = 0; cluster.cz = 0; cluster.count = 0; }); model.forEachNode((node) => { const clusterKey = nodeClusterBy(model.originalNode(node.id)); const cluster = this.clusterMap.get(clusterKey); if (!cluster) return; cluster.cx += node.x; cluster.cy += node.y; if (is3D) cluster.cz += node.z; cluster.count++; }); this.clusterMap.forEach((cluster) => { if (cluster.count > 0) { cluster.cx /= cluster.count; cluster.cy /= cluster.count; cluster.cz /= cluster.count; } }); model.forEachNode((node) => { const { id } = node; // 固定节点不应用聚类重力 if (this.isNodeFixed(node)) return; const clusterKey = nodeClusterBy(model.originalNode(id)); const cluster = this.clusterMap.get(clusterKey); if (!cluster) return; const disp = this.displacements.get(id); const vecX = node.x - cluster.cx; const vecY = node.y - cluster.cy; const vecZ = is3D ? node.z - cluster.cz : 0; const distLength = Math.sqrt(vecX * vecX + vecY * vecY + vecZ * vecZ); if (distLength === 0) return; const gravityForce = this.k * clusterGravity; disp.x -= (gravityForce * vecX) / distLength; disp.y -= (gravityForce * vecY) / distLength; if (is3D) { disp.z -= (gravityForce * vecZ) / distLength; } }); } applyGlobalGravity() { const { model, options } = this; const { gravity, center, dimensions } = options; const is3D = dimensions === 3; const gravityForce = 0.01 * this.k * gravity; model.forEachNode((node) => { const { id } = node; // 固定节点不应用全局重力 if (this.isNodeFixed(node)) return; const disp = this.displacements.get(id); disp.x -= gravityForce * (node.x - center[0]); disp.y -= gravityForce * (node.y - center[1]); if (is3D) { disp.z -= gravityForce * (node.z - (center[2] || 0)); } }); } /** * Updates node positions based on calculated displacements */ updatePositions() { const { model, options } = this; const { speed, dimensions, distanceThresholdMode = 'max' } = options; const is3D = dimensions === 3; let max = 0; let min = Infinity; let sum = 0; let count = 0; model.forEachNode((node) => { const { id } = node; if (this.isNodeFixed(node)) return; const disp = this.displacements.get(id); const distLength = Math.sqrt(disp.x * disp.x + disp.y * disp.y + (is3D ? disp.z * disp.z : 0)); if (distLength === 0) return; const limitedDist = Math.min(this.maxDisplace * (speed / SPEED_DIVISOR$1), distLength); const ratio = limitedDist / distLength; node.x += disp.x * ratio; node.y += disp.y * ratio; if (is3D) { node.z = node.z + disp.z * ratio; } // ---- distance statistics ---- max = Math.max(max, limitedDist); min = Math.min(min, limitedDist); sum += limitedDist; count++; }); if (count === 0) return 0; switch (distanceThresholdMode) { case 'min': return min === Infinity ? 0 : min; case 'mean': return sum / count; case 'max': default: return max; } } destroy() { var _a, _b; this.stop(); (_a = this.displacements) === null || _a === void 0 ? void 0 : _a.clear(); (_b = this.clusterMap) === null || _b === void 0 ? void 0 : _b.clear(); } } export { Simulation }; //# sourceMappingURL=simulation.js.map