highlight: a11y-dark
theme: vuepress
力向导图是绘图的一种算法,实现了用以模拟粒子物理运动的 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
。
- d3.forceSimulation – 创建一个新的力学仿真.
- simulation.restart – 重新启动仿真的定时器.
- simulation.stop – 停止仿真的定时器.
- simulation.tick – 进行一步仿真模拟.
- simulation.nodes – 设置仿真的节点.
- 每个 node 必须是一个对象类型,下面的几个属性将会被仿真系统添加:
index
– 节点在 nodes 数组中的索引x
– 节点当前的 x-坐标y
– 节点当前的 y-坐标vx
– 节点当前的 x-方向速度vy
– 节点当前的 y-方向速度
- 每个 node 必须是一个对象类型,下面的几个属性将会被仿真系统添加:
- simulation.alpha – 设置当前的
alpha
值. - simulation.alphaMin – 设置最小
alpha
阈值. - simulation.alphaDecay – 设置
alpha
衰减率.- 为0的话,永远都不会停
- simulation.alphaTarget – 设置目标
alpha
值. - simulation.velocityDecay – 设置速度衰减率.
- simulation.force – 添加或移除一个力模型.
- simulation.find – 根据指定的位置找出最近的节点.
- 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;
}
整个拖拽的过程中:
- 连接力:拖拽任意节点,其余节点都会同向运动
- 碰撞力:在拖拽的过程中,各节点之间不会重合
- 向心力:拖拽不能拖到任意位置,由于向心力的存在,最后还是会向中心靠拢
总结
本章只是简单介绍了几种力的简单应用,如果想要深入学习,还是需要了解一下verlet积分。
暂无评论内容