算了一下,跟平儿在一起也有八个月了。作为一个程序员男朋友,自然会想写一些东西来提升我们之间的好感度。之前在网上看到有人做了一个特别炫的页面,其实从技术上来说,只是若干图片算好位置、有个计时器、有个打字的效果就解决了。作为一个偏向技术的人,我肯定要写一个更高大上的效果!
于是就想到了写一个 Todolist,专门记录那些我们之前在聊天记录中提到过但一直没有做的事情,例如看一场恐怖片啦,躺在地上数星星啦……灵光一闪,就想用一堆浮动的圆圈来代表每一件事情,最好是可以用鼠标拖动的,圆圈最好可以自由移动,如果可以加一个避免圆圈之间重叠的功能就更好了!
那么就先给大家看一下效果吧:我们的 Todolist!
太懒不看?好,这是你们要的截图:
说干就干!于是我手写了半天的 JavaScript 和 CSS,然而在避免重叠这个地方思维卡住了。
然而,猛然想起了两年前参加软件杯的时候对 D3.js(下文简称 D3)稍有了解,它应该可以实现我想要的功能。于是打开网站上去搜模型,最终搜到了一个类似的:Better force layout node selection,距离我想要的效果最接近了,于是就参考它的代码来写。
我就知道你们还想要截图:
先给大家科普一下 D3 的世界观。D3 是一个专门为了实现各种数据图表而设计的语言,写起来更像一个描述型语言,你只需要写清楚需要什么,剩下的它来帮你完成。D3 的写法是用一连串流水线操作的方式,特别舒服。而且 D3 使用的是 SVG,个人感觉比百度的 Echarts(使用了 Canvas)好一些,毕竟可以无限放大嘛。那么接下来就是准备我们的数据和代码了!
官方样例中给的数据是 这个,看起来是一个图,里面有若干点和边,每个点还有自己的分组。然而我并不需要那么麻烦,我只需要没有分组的点就够了。于是把数据改成这样:
[
{ "todo": "去海边看海" },
{ "todo": "赤脚去踩水" },
{ "todo": "躺在草地上数星星" },
{ "todo": "看一场恐怖片" },
{ "todo": "爬一趟泰山" },
{ "todo": "去张家界玻璃桥体验心跳" },
{ "todo": "买许许多多的零食" },
{ "todo": "看对方的毕业典礼" }
]
那么接下来就是我们的代码了。我们的思路大概是这样:显示文字,圆圈大小随文字而变,不显示边和中间的黑点,点之间的距离增大。首先先定义我们的 SVG 元素。
var width = window.innerWidth,
height = window.innerHeight;
var color = d3.scale.category20();
var svg = d3.select('.todos')
.append('svg')
.attr('width', width)
.attr('height', height);
其中,d3.select
相当于 jQuery 的选择器,append
就是往这里面追加一个元素,然后设置宽高填满窗口。
var force = d3.layout.force()
.charge(-400)
.friction(0.95)
.linkDistance(20)
.size([width, height]);
这个就好说了,力学图可以模拟若干个存在相互作用力的物体之间的位置,下面的几个参数是可以调整的:电荷数、摩擦系数、连接线距离、图像宽高。此外还有好多其它参数,然而这里应该用不到。这些参数在后来我调了好久,就是为了一个相对完美的效果。
我想到的计算圆圈大小的方法就是用内容长度的平方根上取整,因为在圆里,所以每一行应该是 [math]\lceil\sqrt{xy}\rceil[/math] 个字符,一共有 [math]\lfloor\sqrt{xy}\rfloor[/math] 行(其实统一上取整也没有错),于是先写这么个函数备用(其实这个函数是后来整理出来的):
function calcSize(length) {
return Math.ceil(Math.sqrt(length));
}
接下来就是添加数据了!
// 使用 JSON 类型的数据,data 就是我们要的结果
d3.json('data.json', function(err, data) {
if (err) throw err;
// 设置节点,selectAll 可以为之后的元素预留空间
var node = svg.selectAll('.node')
.data(data)
// enter 之后,我们的数据就跟元素绑定了起来,然后我们往里面放 g 节点
.enter().append('g')
// 设置 g 节点的属性,别忘了把 class 设置成 node
.attr('title', name)
.attr('class', 'node')
// 允许鼠标拖动
.call(force.drag);
// 往 g 节点里面放圆
node.append('circle')
// 半径大小为文字长度的 calcSize 乘以 12,假设中英文平均单个字符大小为 12px
.attr('r', function (d) {
return calcSize(d.todo.length) * 12;
});
接下来是设置文字,以及文字垂直居中(即向上移动 [math]\lceil8 linesCount + 4\rceil[/math] 个像素):
var text = node.append('text')
.attr('class', 'text')
.attr('font-size', 14)
.attr('style', function (d) {
// 垂直居中
var len = Math.ceil(d.todo.length / calcSize(d.todo.length));
return 'transform:translateY(-' + (len * 8 + 4) + 'px)';
});
然后是用 tspan 来给文字设置断行(其实就是一个 tspan 中一段文字),只要注意好每一行多少个文字就好了。然后是设置水平居中:
text.selectAll('tspan')
// 绑定数据,当然就是每一行的文字啦
.data(function (d) {
var len = calcSize(d.todo.length);
var strs = [];
// 掌握每一行的文字个数
for (var i = 0; i < d.todo.length; i++) {
var index = parseInt(i / len);
if (!strs[index]) strs[index] = '';
strs[index] += d.todo[i];
}
return strs;
})
.enter()
.append('tspan')
// 水平居中
.attr('x', function (d) {
return -(d.length - text.attr('width')) * 7;
})
.attr('width', text.attr('width'))
.attr('dy', '18px')
// 为 tspan 设置显示的 text,跟绑定的数据是两回事
.text(function(d){
return d;
})
// 可以拖动
.call(force.drag);
最后给整个图设置参数:点、边(空的)、开始模拟、设置模拟过程中的点的位置:
force
.nodes(data)
.links([])
.start()
.on('tick', function() {
node.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
});
大功告成!最后稍微设置一下样式就可以了。
其实 D3 只要熟悉了之后就非常好用,你只需要告诉它你需要什么样的图就可以了。所以,有现成的库,还是不要自己造轮子了啊!
写了那么长的技术文章,重点只有一句话:平儿么么哒!
晚安,好梦。