中学时最喜欢的学科是物理,大学误打误撞读了计算机。最近在做图计算的相关工作,图的可视化中有一个非常重要的算法:“力引导算法”,这个算法的原理居然就是最简单的粒子间的作用力,真是没想到我喜爱的两个东西在这里结合起来了,也有一个感慨:虽然我们的抽象理论已经这么发达的今天,仍然还是需要这种模拟物理世界的“蛮力”算法。
进入正题,我将按如下顺序带着疑问的由浅入深的讲解一下力引导算法(force-directed),作为自己工作的总结,如果有幸帮助到别人就完美了。
一句话:它很大程度上解决了非常困难的图的布局问题,看下面两张图,你觉得哪一张更清晰,实际上它们是一样的图
是不是第2张图看起来舒服多了,没有那么纠结。
图2的可视化更能展示出了这个图的本质:它是一个环形图。
通常我们通常可以通过以下标准来衡量一个图布局算法的好坏:关于边的交叉,这里不得不提到图论中的一个概念:平面图,所谓平面图就是将一个图画出来,里面没有相交的边,这个过程叫做平面化。需要注意的是不是所有的图都可以平面化的,详情见这里:平面图。
狭义的、一句话说就是:把相连的结点接到一起,并可能把不相连的节点推开。
再详细一点的解释:
图中的结点存在着两种力
相连的结点间存在引力(弹簧),其值通过胡克定律算出
结点相互之前存在着静电斥力,通过库仑定律处出
这两种力合成转化成位移,导致结点位移,反复迭代最终整个系统会稳定下来,达到某个阈值停止。
所以一般的实现步骤是:
1 初始化结点位置;
2 计算结点的各种受力;
3 计算合力并将其转化为位置;
4 反复迭代直到达到平衡状态;
总体来说,力引导算法解决了其它算法很难解决的问题,目前仍然是最好的图布局算法
Show me the code!下面是使用的源码,使用d3.js实现了一个力引导布局,你可以直接将上面的代码复制并保存为一个html文件中,直接点击打开,然后修改修改其中的参数看看效果,大胆尝试吧!
上面的代码主要包含:
7 处理模拟器的tick事件,将模拟器内部的数据可视化出来,每一次tick代表一帧将其反应到界面上;
上面各模块中,最主要的是simulation.js这个模块,它实现了一个完整的模拟器,其它的皆为各种力的实现,所以要看代码要从它开始。
下面我们来深入分析d3.js的实现:
首先我们来看simulation.js这个模块,这个是模拟器的主类,里面实现了力引导的几乎全部流程,除了各种力的具体计算,剩下的其它文件实现了这些力。所有的操作就是从实例化一个simulation对象开始。
我们知道布局的过程是在模拟一个物理过程,反复的计算力然后移动位置,迭代该过程直至达到稳定状态,我们来看这个过程在代码中的体现:
我们一般是这样定义一个模拟器对象的:var simulation = d3.forceSimulation().nodes(nodes_data);,我们知道在js中对象是用函数实现的,进入forceSimulation这个函数(在webstorm中按住ctrl点击它)跳转到exports.forceSimulation = simulation;可以看出forceSimulation是simulation的别名,进入真正的对象simulation,看这个函数的第9行:stepper = timer(step),这里定义了一个定时器,回调函数为step(),继续看它:
function step() {
tick();
event.call("tick", simulation);
if (alpha < alphaMin) {
stepper.stop();
event.call("end", simulation);
}
}
上面的代码实现了反复执行step这个函数,这个函数里面就是一次计算受力及移动结点
function tick() {
var i, n = nodes.length, node;
alpha += (alphaTarget - alpha) * alphaDecay;
forces.each(function(force) {
force(alpha);
});
for (i = 0; i < n; ++i) {
node = nodes[i];
if (node.fx == null) node.x += node.vx *= velocityDecay;
else node.x = node.fx, node.vx = 0;
if (node.fy == null) node.y += node.vy *= velocityDecay;
else node.y = node.fy, node.vy = 0;
}
}
看上面的代码,注意alpha += (alphaTarget - alpha) * alphaDecay;这里由于一般alphaTarget是小于alpha,所以这里的+=其实是在减小alpha,即每迭代一次就减少一点直到alphaMin停止,接着这三句:
for (i = 0; i < n; ++i) {
node = nodes[i];
if (node.fx == null) node.x += node.vx *= velocityDecay;
else node.x = node.fx, node.vx = 0;
if (node.fy == null) node.y += node.vy *= velocityDecay;
else node.y = node.fy, node.vy = 0;
}
迭代所有结点,node.fx == null表示没有设置这个结点的固定位置,fx的意思是“fixed x”,相反如果设置了固定位置则结点不受模拟器内力的影响。
function force(alpha) {
for (var k = 0, n = links.length; k < iterations; ++k) {
for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
link = links[i], source = link.source, target = link.target;
x = target.x + target.vx - source.x - source.vx || jiggle();
y = target.y + target.vy - source.y - source.vy || jiggle();
l = Math.sqrt(x * x + y * y);
l = (l - distances[i]) / l * alpha * strengths[i];
x *= l, y *= l;
target.vx -= x * (b = bias[i]);
target.vy -= y * b;
source.vx += x * (b = 1 - b);
source.vy += y * b;
}
}
}
上面的代码遍历了所有边,对于每一条边,都计算这条边的两个结点的受力,关键代码是下面一行:
参考
D3.js force帮助文档:https://github.com/d3/d3-force/blob/master/README.md
D3.js force源代码:https://github.com/d3/d3-force/tree/master/src
Verlet积分:https://en.wikipedia.org/wiki/Verlet_integration
力引导算法:https://en.wikipedia.org/wiki/Force-directed_graph_drawing
力引导算法的一个实现:https://blog.csdn.net/newworld123made/article/details/51443603