@antv/g6
Version:
graph visualization frame work
197 lines (191 loc) • 5.93 kB
JavaScript
/**
* @fileOverview random layout
* @author shiwu.wyy@antfin.com
*/
const d3Force = require('d3-force');
const Layout = require('./layout');
const Util = require('../util');
const isArray = require('@antv/util/lib/type/is-array');
const isNumber = require('@antv/util/lib/type/is-number');
const isFunction = require('@antv/util/lib/type/is-function');
/**
* 经典力导布局 force-directed
*/
Layout.registerLayout('force', {
getDefaultCfg() {
return {
center: [ 0, 0 ], // 向心力作用点
nodeStrength: null, // 节点作用力
preventOverlap: false, // 是否防止节点相互覆盖
nodeSize: undefined, // 节点大小 / 直径,用于防止重叠时的碰撞检测
nodeSpacing: undefined, // 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离)
edgeStrength: null, // 边的作用力, 默认为根据节点的入度出度自适应
linkDistance: 50, // 默认边长度
forceSimulation: null, // 自定义 force 方法
alphaDecay: 0.028, // 迭代阈值的衰减率 [0, 1],0.028 对应最大迭代数为 300
alphaMin: 0.001, // 停止迭代的阈值
alpha: 0.3, // 当前阈值
collideStrength: 1, // 防止重叠的力强度
tick() {},
onLayoutEnd() {}, // 布局完成回调
onTick() {} // 每一迭代布局回调
};
},
/**
* 初始化
* @param {object} data 数据
*/
init(data) {
const self = this;
self.nodes = data.nodes;
self.edges = data.edges;
self.ticking = false;
},
/**
* 执行布局
*/
execute() {
const self = this;
const nodes = self.nodes;
const edges = self.edges;
// 如果正在布局,忽略布局请求
if (self.ticking) {
return;
}
let simulation = self.forceSimulation;
const alphaMin = self.alphaMin;
const alphaDecay = self.alphaDecay;
const alpha = self.alpha;
if (!simulation) {
try {
// 定义节点的力
const nodeForce = d3Force.forceManyBody();
if (self.nodeStrength) {
nodeForce.strength(self.nodeStrength);
}
simulation = d3Force.forceSimulation()
.nodes(nodes)
.force('center', d3Force.forceCenter(self.center[0], self.center[1]))
.force('charge', nodeForce)
.alpha(alpha)
.alphaDecay(alphaDecay)
.alphaMin(alphaMin)
.on('tick', () => {
self.tick();
})
.on('end', () => {
self.ticking = false;
self.onLayoutEnd && self.onLayoutEnd();
});
if (self.preventOverlap) {
self.overlapProcess(simulation);
}
// 如果有边,定义边的力
if (edges) {
// d3 的 forceLayout 会重新生成边的数据模型,为了避免污染源数据
const d3Edges = edges.map(edge => {
return {
id: edge.id,
source: edge.source,
target: edge.target
};
});
const edgeForce = d3Force.forceLink().id(function(d) { return d.id; }).links(d3Edges);
if (self.edgeStrength) {
edgeForce.strength(self.edgeStrength);
}
if (self.linkDistance) {
edgeForce.distance(self.linkDistance);
}
simulation.force('link', edgeForce);
}
self.forceSimulation = simulation;
self.ticking = true;
} catch (e) {
self.ticking = false;
console.warn(e);
}
} else {
if (self.preventOverlap) {
self.overlapProcess(simulation);
}
simulation
.alpha(alpha)
.restart();
this.ticking = true;
}
},
/**
* 防止重叠
* @param {object} simulation 力模拟模型
*/
overlapProcess(simulation) {
const self = this;
const nodeSize = self.nodeSize;
let nodeSizeFunc;
const nodeSpacing = self.nodeSpacing;
let nodeSpacingFunc;
const collideStrength = self.collideStrength;
if (isNumber(nodeSpacing)) {
nodeSpacingFunc = () => {
return nodeSpacing;
};
} else if (typeof nodeSpacing === 'function') {
nodeSpacingFunc = nodeSpacing;
} else {
nodeSpacingFunc = () => {
return 0;
};
}
if (!nodeSize) {
nodeSizeFunc = d => {
if (d.size) {
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
return res / 2 + nodeSpacingFunc(d);
}
return d.size / 2 + nodeSpacingFunc(d);
}
return 10 + nodeSpacingFunc(d);
};
} else if (isFunction(nodeSize)) {
nodeSizeFunc = nodeSize;
} else if (!isNaN(nodeSize)) {
const radius = nodeSize / 2;
nodeSizeFunc = d => {
return radius + nodeSpacingFunc(d);
};
} else if (isArray(nodeSize)) {
const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
const radius = larger / 2;
nodeSizeFunc = d => {
return radius + nodeSpacingFunc(d);
};
}
// forceCollide's parameter is a radius
simulation.force('collisionForce', d3Force.forceCollide(nodeSizeFunc).strength(collideStrength));
},
/**
* 更新布局配置,但不执行布局
* @param {object} cfg 需要更新的配置项
*/
updateCfg(cfg) {
const self = this;
if (self.ticking) {
self.forceSimulation.stop();
self.ticking = false;
}
self.forceSimulation = null;
Util.mix(self, cfg);
},
destroy() {
const self = this;
if (self.ticking) {
self.forceSimulation.stop();
self.ticking = false;
}
self.nodes = null;
self.edges = null;
self.destroyed = true;
}
});