如何在D3.js力导向图中增强非关联节点间的作用力?
D3.js力导向图:实现非关联节点的自定义作用力
问题描述
我使用d3.js构建了一个力导向图,部分代码如下:
simulation .force("center", d3.forceCenter(width / 2, height / 2)) .force("nodes", d3.forceManyBody()) .force( "links", d3 .forceLink(links) .id(d => d.id) .distance(d => 5 * (d.source.size + d.target.size)) ) .on("tick", ticked);其中这一行定义了关联节点间的作用力:
.distance(d => 5 * (d.source.size + d.target.size))但我希望实现非关联节点间的作用力(理想情况下该作用力随自由度增加而增强),请问该如何实现?
实现方案
方法1:基于forceManyBody扩展自定义强度
forceManyBody支持传入自定义强度函数,我们可以先预处理关联关系,再针对非关联节点设置随自由度变化的作用力:
- 先构建关联关系的查询Map,方便快速判断节点是否关联:
// 预处理所有节点的关联关系 const linkedNodesMap = new Map(); links.forEach(link => { // 为源节点添加目标节点关联 if (!linkedNodesMap.has(link.source.id)) linkedNodesMap.set(link.source.id, new Set()); linkedNodesMap.get(link.source.id).add(link.target.id); // 为目标节点添加源节点关联 if (!linkedNodesMap.has(link.target.id)) linkedNodesMap.set(link.target.id, new Set()); linkedNodesMap.get(link.target.id).add(link.source.id); });
- 修改
forceManyBody的强度设置:
simulation .force("center", d3.forceCenter(width / 2, height / 2)) .force("nodes", d3.forceManyBody() .strength((node, _, nodes) => { // 计算当前节点的自由度:总节点数 - 关联节点数 - 1(排除自身) const linkedCount = linkedNodesMap.get(node.id)?.size || 0; const freedomDegree = nodes.length - linkedCount - 1; // 仅对非关联节点施加作用力,强度随自由度线性增强 return (otherNode) => { const isUnlinked = !linkedNodesMap.get(node.id)?.has(otherNode.id); return isUnlinked ? freedomDegree * 0.1 : 0; // 0.1为强度系数,可根据画布大小、节点数量自行调整 }; }) ) .force("links", d3.forceLink(links) .id(d => d.id) .distance(d => 5 * (d.source.size + d.target.size)) ) .on("tick", ticked);
方法2:完全自定义力模型
如果forceManyBody的扩展不够灵活,可以直接用d3.force()创建自定义力,更精细控制作用力逻辑:
const customUnlinkedForce = d3.force() .strength(0.1) // 基础强度系数 .initialize(nodes => { // 初始化时构建关联关系Map const linkedMap = new Map(); links.forEach(link => { if (!linkedMap.has(link.source.id)) linkedMap.set(link.source.id, new Set()); if (!linkedMap.has(link.target.id)) linkedMap.set(link.target.id, new Set()); linkedMap.get(link.source.id).add(link.target.id); linkedMap.get(link.target.id).add(link.source.id); }); this.linkedMap = linkedMap; }) .force((alpha, nodes) => { // 遍历所有节点对,处理非关联节点的作用力 for (let i = 0; i < nodes.length; i++) { const nodeA = nodes[i]; const linkedIds = this.linkedMap.get(nodeA.id) || new Set(); const freedomDegree = nodes.length - linkedIds.size - 1; for (let j = i + 1; j < nodes.length; j++) { const nodeB = nodes[j]; if (!linkedIds.has(nodeB.id)) { // 计算节点间距离 const dx = nodeB.x - nodeA.x; const dy = nodeB.y - nodeA.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance === 0) continue; // 计算作用力大小,结合自由度与模拟alpha系数 const force = freedomDegree * alpha * this.strength() / distance; // 施加斥力(如需引力,将force取反即可) nodeA.vx += force * dx; nodeA.vy += force * dy; nodeB.vx -= force * dx; nodeB.vy -= force * dy; } } } }); // 将自定义力加入模拟 simulation .force("center", d3.forceCenter(width / 2, height / 2)) .force("unlinked", customUnlinkedForce) .force("links", d3.forceLink(links) .id(d => d.id) .distance(d => 5 * (d.source.size + d.target.size)) ) .on("tick", ticked);
注意事项
- 自由度的计算逻辑可根据业务需求调整,比如结合节点的其他属性(权重、类型等)
- 强度系数需要根据实际画布尺寸、节点数量调试,避免布局过度分散或拥挤
- 若需要引力而非斥力,只需将作用力的方向取反即可
内容的提问来源于stack exchange,提问作者Alexander Mills
相关产品推荐
相关产品推荐

