R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。

使用 D3.js 制作我们的 Todolist!

注意:本文发布于 2827 天前,文章中的一些内容可能已经过时。

算了一下,跟平儿在一起也有八个月了。作为一个程序员男朋友,自然会想写一些东西来提升我们之间的好感度。之前在网上看到有人做了一个特别炫的页面,其实从技术上来说,只是若干图片算好位置、有个计时器、有个打字的效果就解决了。作为一个偏向技术的人,我肯定要写一个更高大上的效果!

于是就想到了写一个 Todolist,专门记录那些我们之前在聊天记录中提到过但一直没有做的事情,例如看一场恐怖片啦,躺在地上数星星啦……灵光一闪,就想用一堆浮动的圆圈来代表每一件事情,最好是可以用鼠标拖动的,圆圈最好可以自由移动,如果可以加一个避免圆圈之间重叠的功能就更好了!

那么就先给大家看一下效果吧:我们的 Todolist!

太懒不看?好,这是你们要的截图:

1

说干就干!于是我手写了半天的 JavaScript 和 CSS,然而在避免重叠这个地方思维卡住了。

然而,猛然想起了两年前参加软件杯的时候对 D3.js(下文简称 D3)稍有了解,它应该可以实现我想要的功能。于是打开网站上去搜模型,最终搜到了一个类似的:Better force layout node selection,距离我想要的效果最接近了,于是就参考它的代码来写。

我就知道你们还想要截图:

2

先给大家科普一下 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 只要熟悉了之后就非常好用,你只需要告诉它你需要什么样的图就可以了。所以,有现成的库,还是不要自己造轮子了啊!


写了那么长的技术文章,重点只有一句话:平儿么么哒!

晚安,好梦。

Disqus 加载中……如未能加载,请将 disqus.com 和 disquscdn.com 加入白名单。

这是我们共同度过的

第 3071 天