本文详解D3.js力导向图的绘制原理与实现。涵盖向心力、碰撞力、连接力、电荷力及径向力五种核心模型,解析alpha衰减、速度衰减等关键参数对仿真稳定性的影响,并演示如何通过监听tick事件实现节点动态更新与拖拽交互。
力导向图是一种基于物理模拟的绘图算法,其核心采用了velocity Verlet数值积分器来模拟粒子的运动状态。仿真逻辑设定任意单位时间步长Δt为1,且所有粒子的质量常量m均为1。在此假设下,作用于每个粒子的合力F等同于在单位时间Δt内产生的恒定加速度a。通过为每个粒子累加速度并计算其位置,即可实现仿真的动态更新。
在二维或三维空间中配置节点,节点之间通过连线相连,这些连线长度尽量保持相等且避免交叉。节点与连线均受到力的作用,力的大小由节点与连线的相对位置决定。系统根据计算出的力来推导节点和连线的运动轨迹,并逐步降低系统能量,直至达到能量极低的稳定状态。

力
D3 Force是一种基于物理运动原理的绘图算法。初始阶段为所有节点分配任意初始配置,随后根据各节点的属性驱动其运动,这种驱动机制即为节点间的力。
力主要分为斥力与引力两类。节点间的相互作用表现为斥力,而整体图形存在引力约束,类似于地心引力使人无法无限跳跃并最终落回地面。在多种力的交互下,节点发生碰撞并聚拢,逐渐收敛至稳定位置。可通过alpha属性控制整个过程的速度,并设置摩擦力velocityDecay以调整运动速度。
五种力
D3提供了五种力模型,具体实现可参考相关Demo。
向心力
- d3.forceCenter([x, y]):用于创建一个中心作用力。
- center.x:用于设置中心作用力的x坐标。
- center.y:用于设置中心作用力的y坐标。
向心力可将所有节点统一向指定位置<x,y>移动。该力强制修改节点位置但不改变速度,以避免节点在目标位置附近产生抖动。此力有助于保持所有节点位于视口中心。

碰撞力
节点间的相互排斥力,用于防止节点重合。可通过strength参数设置斥力的强弱。
- d3.forceCollide:创建一个圆形区域的碰撞检测力模型。
- collide.radius:设置碰撞半径。
- collide.strength:设置碰撞检测力模型的强度。
- collide.iterations:设置迭代次数。

连接力(弹簧力)
使用d3.forceLink将两个节点添加连线后,可设置连接力。该力根据两节点间的距离拉近或推远节点,力的强弱与距离成正比,类似弹簧,故又称弹簧力。
- d3.forceLink:创建一个
link(弹簧)作用力。 - link.links:设置弹簧作用力的边。
- link.id:设置边元素中节点的查找方式,支持索引或
id字符串。 - link.distance:设置
link的距离。 - link.strength:设置
link的强度。 - link.iterations:设置迭代次数。

电荷力
模拟所有节点间的相互作用力。若值为正,节点相互吸引;若为负,则相互排斥。该力模拟电荷吸引力,其强弱与节点间距离相关。
- d3.forceManyBody:创建一个电荷作用力模型。
- manyBody.strength:设置电荷力模型的强度。
- manyBody.theta:设置
Barnes–Hut算法的精度。 - manyBody.distanceMin:限制节点之间的最小距离。
- manyBody.distanceMax:限制节点之间的最大距离。

径向力
设定一个圆,使所有节点受到指向圆心的力,从而聚集到圆周上。
- d3.forceRadial:创建一个环形布局的作用力。
- radial.strength:设置力强度。
- radial.radius:设置目标半径。
- radial.x:设置环形作用力的目标中心x坐标。
- radial.y:设置环形作用力的目标中心y坐标。

这五种力可以叠加使用。
力向导图
创建力向导图时,需先创建新的力学模型d3.forceSimulation,并指定节点nodes。
1.d3.forceSimulation:创建一个新的力学仿真。
2.simulation.restart:重新启动仿真的定时器。
3.simulation.stop:停止仿真的定时器。
4.simulation.tick:进行一步仿真模拟。
5.simulation.nodes:设置仿真的节点。
每个node必须为对象类型,仿真系统将自动添加以下属性:
index:节点在nodes数组中的索引。x:节点当前的x坐标。y:节点当前的y坐标。vx:节点当前的x方向速度。vy:节点当前的y方向速度。
5.simulation.alpha:设置当前的alpha值。
6.simulation.alphaMin:设置最小alpha阈值。
7.simulation.alphaDecay:设置alpha衰减率。
若设置为0,仿真将永远不会停止。
8.simulation.alphaTarget:设置目标alpha值。
9.simulation.velocityDecay:设置速度衰减率。
10.simulation.force:添加或移除一个力模型。
11.simulation.find:根据指定位置找出最近的节点。
12.simulation.on:添加或移除事件监听器。
tick:仿真内部定时器每次tick之后触发。end:当alpha小于alphaMin时,仿真内部定时器停止。
const simulation = d3.forceSimulation(nodes)
// 连接力
.force('link', d3.forceLink())
// 在 y轴方向上施加一个力
.force('y', d3.forceY().strength(0.025))
// 电荷力
.force('charge', d3.forceManyBody())
// 碰撞力
.force('collision', d3.forceCollide().radius(d => 4))
// 向心力
.force('center', d3.forceCenter(width / 2, height / 2))
接下来绘制文章开头所示的关系图,具体实现可参考Demo。
创建模拟数据
const nodes = [
{name: "张三"}
...
]
const links = [
{ source: 0, target: 1, relation: "关系1"}
...
]创建力模型
let simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).distance(100)) // 连接力
绘制节点和连线
// 画线
function drawLine() {
let lines = svg.append("g")
.selectAll(".force-line")
.data(links)
.enter()
.append("line")
.attr("class", "line")
.attr("stroke", "#999")
.attr("stroke-width", "1px");
return lines;
}
let lines = drawLine();
// 画节点节点盒子
function drawCircle() {
let nodeGroups = svg.append("g")
.attr("class", "nodes-box")
.selectAll(".force-node")
.data(nodes)
.enter()
.append("g")
.attr("class", "force-circle")
nodeGroups.append("circle")
.attr("class", "force-circle")
.attr("r", 20)
.style("fill",(d, i) => color(i));
nodeGroups.append("text")
.attr("class", "force-text")
.attr("dy", ".33em")
.attr("font-size", "12px")
.attr("text-anchor", "middle")
.style("fill", "#eee")
.text(d => d.name);
return nodeGroups;
}
let nodesCircle = drawCircle();
此时仅能在画布左上角原点位置看到circle图形。由于力学模型动态计算节点和连线位置,需动态更新其位置<x, y>。为此,需监听tick事件。

监听 tick
let simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).distance(100));
.on("tick", ticked);
function ticked()
lines
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
// 这里就不适合 去改变circle的圆心位置了,因为有文字存在,改变整个circleGroup的transform
nodesCircle.attr("transform", function (d) {
// d.fx=d.x;d.fy=d.y; 固定位置
return "translate(" + d.x + ", " + d.y + ")";
});
}
可添加向心力,使整个图形显示在画布中心。
添加向心力
let simulation = d3.forceSimulation(nodes)
.force("center", d3.forceCenter(width / 2, height / 2)) // 用指定的x坐标和y坐标创建一个居中力
.force("link", d3.forceLink(links).distance(100)) //
.on("tick", ticked);

此时发现节点出现重合现象,可为节点添加碰撞力使其分离。
添加碰撞力
let simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-200)) // 电荷力 相互之间的作用力
.force("center", d3.forceCenter(width / 2, height / 2)) // 用指定的x坐标和y坐标创建一个居中力
.force("link", d3.forceLink(links).distance(100)) //
.on("tick", ticked);

添加拖拽效果
d3.drag的详细用法将在后续章节介绍,本章暂不深入。
let nodeGroups = svg.append("g")
...
.call(
d3.drag().on("start", started).on("drag", dragged).on("end", ended)
);
// 拖拽
function started(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function ended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
整个拖拽过程表现如下:
- 连接力:拖拽任意节点,其余节点会同向运动。
- 碰撞力:在拖拽过程中,各节点之间不会重合。
- 向心力:拖拽无法到达任意位置,受向心力影响,节点最终仍会向中心靠拢。
以上为D3.js实现力向导图的绘制教程详解,更多关于D3.js力向导图的资料请关注本站其它相关文章!