布局 捆图

捆图(Bundle)是 D3 中比较奇特的一个布局,只有两个函数,而且需要与其它布局配合使用。本文讲述捆图的制作方法。

有关捆图的例子极少,很容易找到的是:http://bl.ocks.org/mbostock/1044242

但是此例有些复杂,不太容易弄懂,而且也没有相应的说明文字(D3官网上的例大多没有相应文字解说),根本无法作为参考。本文提供一个简单的例子,讲解捆图到底是什么,应该怎么制作。

捆图只有两个函数:

捆图的布局之所以函数少,是因为它需要与其它层级布局一起使用。所谓层级布局,是指采用嵌套结构(父子节点关系)来描述节点信息,根据层级布局扩展出来的布局即:集群图、打包图、分区图、树状图、矩阵树图。最常见的是与集群图一起使用,使用集群图布局计算节点的位置,再用捆图布局计算连线路径。也就是说,捆图布局只干一件事:

计算连线的路径。

下面举一个例子。中国的高铁已经在很多城市开通,如北京到上海,北京到桂林等。现要制作一图来表示经过哪一座城市的高铁最密集。

数据

有9座城市:

var cities = {
     name: "",
     children:[
        {name: "北京"},{name: "上海"},{name: "杭州"},
        {name: "广州"},{name: "桂林"},{name: "昆明"},
        {name: "成都"},{name: "西安"},{name: "太原"}
     ]
 };

这9座城市所属的节点有一个公共的父节点,父节点名称为空,稍后并不绘制此父节点。另外还有连接各城市高铁的数组,如下:

var railway = [
            {source: "北京", target: "上海"},
            {source: "北京", target: "广州"},
            {source: "北京", target: "杭州"},
            {source: "北京", target: "西安"},
            {source: "北京", target: "成都"},
            {source: "北京", target: "太原"},
            {source: "北京", target: "桂林"},
            {source: "北京", target: "昆明"},
            {source: "北京", target: "成都"},
            {source: "上海", target: "杭州"},
            {source: "昆明", target: "成都"},
            {source: "西安", target: "太原"}
];  //(该数据为假设,并没有经过调查)

source 和 target 分别表示高铁的两端。

布局(数据转换)

前面提到,捆图布局要和其他布局联合使用,在这里与集群图布局联合。分别创建一个集群图布局和一个捆图布局。

var cluster = d3.layout.cluster()
                .size([360, width/2 - 50])
                .separation(function(a, b) { 
return (a.parent == b.parent ? 1 : 2) / a.depth; 
});

var bundle = d3.layout.bundle();

从集群图布局的参数可以看出,接下来节点将要分布呈圆形。捆图布局没有参数可以设置,只创建即可,保存在变量 bundle 中。

先使用集群图布局计算节点:

var nodes = cluster.nodes(cities);
console.log(nodes);

将计算后的节点数组保存在 nodes 中,并输出该数组,结果如下图所示,第一个节点有 9 个子节点,其他的节点都有且只有一个父节点,没有子节点。这是接下来捆图要使用的节点数组,但是却是用集群图布局计算而来的。

下一步是重点,要使用数组 railway。由于 railway 中存储的 source 和 target 都只有城市名称,因此先要将其对应成 nodes 中的节点对象。写一个函数,按城市名将 railway 中的 source 和 target 替换成节点对象。

function map( nodes, links ){
var hash = [];
    for(var i = 0; i < nodes.length; i++){
        hash[nodes[i].name] = nodes[i];
    }
    var resultLinks = [];
    for(var i = 0; i < links.length; i++){
        resultLinks.push({      source: hash[ links[i].source ], 
                                target: hash[ links[i].target ]
                            });
    }
    return resultLinks;
}

使用该函数返回的数组,即可作为捆图布局bundle的参数使用:

var oLinks = map(nodes, railway);
    console.log(oLinks);
        
var links = bundle(oLinks);
console.log(links);

map() 返回的结果保存在 oLinks,bundle() 返回的结果保存在 links 中,输出结果分别下图所示,即捆图布局的连线数组转换前后的变换。

上图是转换前的数据,连线数组的每一项都只有两个变量:source 和 target,内容是节点对象。对于第一个连线,是从“北京”到“上海”。

下图是转换后的数据,source 和 target 不见了,取而代之的是 0、1、2,变成了一个数组。很明显,该数组的第一项和 source 的内容一样,最后一项和 target 的内容一样,但中间多出了一项(下图的红框内)。多出的这一项是根据source和target公共的父节点计算出来的。于是,该数组表示了一条路径。

其实,捆图布局根据各连线的source和target为我们计算了一条条连线路径,我们可以把捆图布局的作用简单地解释为:使用这些路径绘制的线条能更美观地表示“经过哪座城市的高铁最多”

绘制

经过捆图布局转换后的数据很适合用 d3.svg.line() 和 d3.svg.line.radial() 来绘制,前者是线段生成器,后者是放射式线段生成器。在 line.interpolate() 所预定义的插值模式中,有一种就叫做 bundle,正是为捆图准备的。

由于本例中用集群图布局计算节点数组使用的使用圆形的,因此要用放射式的线段生成器。先创建一个:

var line = d3.svg.line.radial()
                    .interpolate("bundle")
                    .tension(.85)
                    .radius(function(d) { return d.y; })
                    .angle(function(d) { return d.x / 180 * Math.PI; });

此线段生成器是用来获取连线路径的。接下来,添加一个分组元素<g>,用来放所有与捆图相关的元素。

gBundle = svg.append("g")
                .attr("transform", 
"translate(" + (width/2) + "," + (height/2) + ")");

var color = d3.scale.category20c(); //颜色比例尺

然后,先在gBundle中添加连线路径:

var link = gBundle.selectAll(".link")
              .data(links)
              .enter()
              .append("path")
              .attr("class", "link")
              .attr("d", line); //使用线段生成器

在该连线的样式中,添加透明度能够在连线汇聚处更能显示出“捆”的效果。例如样式设定为:

.link {
        fill: none;
        stroke: black;
        stroke-opacity: .5;
        stroke-width: 8px;
    }

连线的绘制结果如下图所示。

最后,向图中添加节点。节点用一个圈,里面写上城市的名称来表示。首先,绑定节点数组,并添加与之对应的<g>元素。

var node = gBundle.selectAll(".node")
              .data( nodes.filter(function(d) { return !d.children; }) )
              .enter()
              .append("g")
              .attr("class", "node")
              .attr("transform", function(d) {
                    return "rotate(" + (d.x- 90) + ")translate(" 
+ d.y + ")" + "rotate("+ (90 - d.x) +")"; 
              });

要注意,被绑定的数组是经过过滤后的nodes数组。此处的filter是JavaScript数组对象自身的函数,粗体字的部分表示只绑定没有子节点的节点,也就是说9座城市的公共父节点不绘制。然后只要在该分组元素<g>中分别加入<circle>和<text>即可。

node.append("circle")
              .attr("r", 20)
              .style("fill",function(d,i){ return color(i); });
            
    node.append("text")
            .attr("dy",".2em")
            .style("text-anchor", "middle")
            .text(function(d) { return d.name; });

结果如图5所示。由于经过北京的高铁线路最多,连线在北京的圆圈处最密集,就好像将很多条绳子“捆”在这里一样。

当节点和连线变得很多很复杂时,就变成了本文开头处的例子。就像本文开头说的一样,那个例子完成度太高,根本不能提供参考。希望本文的例子能给大家作为捆图的参考范例。

源代码

下载地址:bundle.zip

在线演示

委托名片直接发送邮件(需邮箱客户端)点击复制邮箱地址14721230383项目委托、技术支持,欢迎与我们联系。休息日7:00 ~ 11:0012:00 ~ 22:00工作日11:00 ~ 11:4016:00 ~ 22:00

Copyright 2014-2016, DecemberCafe, All Rights Reserved