d3词语的原项目地址为:
https://github.com/jasondavies/d3-cloud
使用效果为:
实现原理
本质上就是找到中心点,按词语的大小进行排序,然后利用广度优先算法向四周扩散词语的中心点。
而这里的广度优先算法使用的是阿基米德螺旋线,其实不使用该螺旋线也可以,比如米字型射线等等。
以下是源码中求解阿基米德螺旋线的代码:
function archimedeanSpiral(size) {
var e = size[0] / size[1];
return function(t) {
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
};
}
而根据该算法得到的螺旋线其实是一条连续的实线,所以我们需要取变化量delta来获取一个节点的下一个节点(这种方式有点不像广度优先算法了,有点像深度优先)。在d3词云的放置函数中,就可以发现这一点:
function place(board, tag, bounds) {
var perimeter = [{
x: 0, y: 0}, {
x: size[0], y: size[1]}],
startX = tag.x,
startY = tag.y,
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
s = spiral(size),
dt = random() < .5 ? 1 : -1,
t = -dt,
dxdy,
dx,
dy;
while (dxdy = s(t += dt)) {
dx = ~~dxdy[0];
dy = ~~dxdy[1];
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
tag.x = startX + dx;
tag.y = startY + dy;
... ...
代码中是放置函数的一部分,而这个maxDelta就是能取的最大变化量。
那么我们其实就可以推断出,这个词云算法就是通过寻找阿基米德螺旋线中的节点作为待绘制词语的中心,逐词进行绘制的。利用广度优先的思想,应该将已经使用过的顶点给抹去(d3源码中每次都用所有的顶点作为目标顶点,这里可以考虑使用一个集合来缓存),避免浪费计算力。
按照上面这个思路,大概的实现结果如下:
文字之间不能有重叠
这个时候,就需要我们做像素处理,首先,对于一个500*500的canvas画布,可以使用一个二维数组dp[500][500]来表达,而对于dp[i][j]来说,表达的就是第i行j列的这个像素的情况,我们可以使用0代表该像素点没有被占用,1代表该像素点被占用了。
但是此时我们可以发现,这样的25万个元素的数组,放到浏览器中是非常影响浏览器的性能的,对用户来说体验会很差,所以需要考虑使用状态压缩算法。
状态压缩算法
状态压缩算法本质上就是利用二进制位运算来解决实际问题,那么什么是状态压缩算法呢?
在以前打ACM的时候,有学习过状态压缩DP或者插头DP(DP即动态规划),当我们面对的问题是以指数级的方式存在的时候,例如Np问题,复杂的组合问题,就可以考虑使用一个长整型来标识一个64长度的二进制数组,而同时我们可以利用整型的位运算来判断两个二进制数组的关系(与,或,非,异或等等),甚至可以精确到某一位上。
详细了解状态压缩可以看下https://zhuanlan.zhihu.com/p/131585177
由于在Javascript中对Long型的定义不太对,所以我们可以使用32位长度的普通数字来做状态压缩。也就是说,我们可以将25万个元素组成的二进制数组改造成以32位整型构成的整形数组,那么,最终这个整型数组的长度为:
ceil(500*500/32) = 7813
这一步,在d3词云源码中也有体现
cloud.start = function() {
var contextAndRatio = getContext(canvas()),
board = zeroArray((size[0] >> 5) * size[1]),
bounds = null,
n = words.length,
i = -1,
tags = [],
data = words.map(function(d, i) {
d.text = text.call(this, d, i);
d.font = font.call(this, d, i);
d.style = fontStyle.call(this, d, i);
d.weight = fontWeight.call(this, d, i);
d.rotate = rotate.call(this, d, i);
d.size = ~~fontSize.call(this, d, i);
d.padding = padding.call(this, d, i);
return d;
}).sort(function(a, b) {
return b.size - a.size; });
... ...
一开始,代码就填充了
board = zeroArray((size[0] >> 5) * size[1]),
作为背景像素占用情况的原始数组,虽然代码中的写法是
(size[0] >> 5) * size[1]
但是其实本质上长宽的某一个值还是没有关系,>> 5 是右移运算符,意思就是除以2的五次方就是32,所以写成
size[0] * size[1]/32
也没有任何问题,只不过右移运算比除法运算要快一些。
有了状态压缩算法只是第一步,我们还需要对这些文本进行占用像素点计算