D3学习系列(三) 桑基图

「前言」

网上关于桑基图的例子也有一些,但是对于初入门的新手并不友好、易懂。如果仅用百度搜索,资料更是少得可怜(这里感谢同事推荐shadowsocks进行科学上网●^●)。当然有些语句没有看懂,anyway先实现了再说~

「什么是桑基图」

桑基图(Sankey diagram),即桑基能量分流图,主要是用来描述能量、人口、经济等的流动情况。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。

桑基图主要关注能量、物料或资本等在系统内部的流动和转移情况。Sankey diagram的特点有:

  • 起始流量和结束流量相同
  • 在内部,不同的线条代表了不同的流量分流情况,它的宽度成比例地显示此分支占有的流量
  • 节点不同的宽度代表了特定状态下的流量大小

在数据可视化中,桑基图有利于展现分类维度间的相关性,以流的形式呈现共享同一类别的元素数量。特别适合表达集群的发展,比如展示特定群体的人数分布等。我们可以欣赏下利用桑基图展示的可视化作品,太美了简直!

Paste_Image.png

「绘制桑基图」

绘制画布SVG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 1000 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // 数字转字符串 逗号分隔,0位小数点
format = function(d) {return formatNumber(d) + "m CHF";};
var color = d3.scale.category20();
var svg = d3.select("#chart")//ID选择器
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

在body中定义一个<p>元素,令id="chart",画布SVG作用在元素<p>中,保持有一定的margin。

定义桑基布局

1
2
3
4
5
6
var sankey = d3.sankey()
.nodeWidth(25) // 节点宽度
.nodePadding(20) // 矩形垂直方向的间距
.size([width,height]);
var path = sankey.link();

sankey.link()函数应该是插件sankey.js中定义好的,目的是生成节点相对应的路径。

绑定数据

1
2
3
4
5
6
d3.json("http://benlogan1981.github.io/VerticalSankey/data/ubs.json", function(error, energy) {
sankey
.nodes(energy.nodes) // 绑定节点数据
.links(energy.links) // 绑定路径数据
.layout(32); // iterations ?
};

这里我们通过引用外部JS数据的方式来绑定,之后直接使用energy.的方式调用,方式如下:d3.json("XXX.json", function(error, energy) {};

注意
如果不启动外部服务器,是没有办法加载外部数据的。由于Python自带的包可以建立简单的Web服务器,便直接用Python:

  • 命令行中直接CD到准备做服务器的根目录下,输入命令:python -m SimpleHTTPServer 8080(这里使用的2.X版本,3.X版本稍有不同)
  • 然后就可以在浏览器中输入:http://localhost:8080/路径来访问服务器的资源

JSON数据
这里的JSON数据长这样:

{“nodes”:[
{“name”:”Wealth Management”},
{“name”:”WMA”},

{“name”:”Switzerland”}
],
“links”:[
{“source”:0,”target”:5,”value”:100},
{“source”:1,”target”:5,”value”:1800},
….
{“source”:4,”target”:8,”value”:400}
]}

nodes表示节点数据;links表示连线数据,其中source为起始节点,target表示终点节点,value为量的大小。关于.layout(),查了相关资料,好像是跟计算出来的节点与路径数据的迭代次数(iterations)有关,但是调整参数值并没有发现什么变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var link = svg.append("g").selectAll("path")
.data(energy.links)
.enter()
.append("path")
.attr("class","link")
.attr("d",path) // 路径链接已被sankey封装好
.style("stroke-width",function(d){
return Math.max(1,d.dy);
})
.style("stroke",function(d) {
return d.source.color = color(d.source.name.replace(/ .*/,""));
})
.sort(function(a,b){
return b.dy - a.dy;
})
;
link.append("title")
.text(function(d){
return d.source.name + "->" + d.target.name + "\n" + format(d.value) ;
});

stroke-width参数表示links的宽度,返回的是1和dy中的最大值,但大部分情况都会返回dy。如果换成10(这样每根连线都是一样宽度),看一下效果就知道stroke-width的作用了:

stroke其实是描边的意思,感觉由于header部分设置过了fill:none,所以才默认为填充的颜色。这里设置相同起始节点的连线具有相同的颜色,确保达到你想要颜色分类的效果。同时对每条links添加title,鼠标悬停会显示相应内容。

绘制节点数据nodes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var node = svg.append("g").selectAll("g")
.data(energy.nodes)
.enter()
.append("g")
.attr('class', "node")
.attr('transform', function(d){
return "translate(" + d.x + "," + d.y + ")"; //节点的(x,y)坐标
});
node.append("rect")
.attr("height",sankey.nodeWidth())
.attr("width",function(d) { return d.dy; })
.style("fill",function(d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("stroke",function(d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function(d) {
return d.name + "\n" + format(d.value) ;
});
node.append("text")
.attr("text-anchor","middle")
.attr("x",function(d) {
return d.dy/2;
})
.attr("y",function(d) {
sankey.nodeWidth() / 2;
})
.attr("dy","1em")
.text(function(d) { return d.name; })
.filter(function(d) {
return d.x < width / 2 ;
});

每个rect元素的高度(height)相同,宽度(width)为相应的dy;stroke把外框颜色设置成与rect元素同样的颜色并加深,图片放大可以明显的看出效果),.text添加鼠标悬停显示相应文字,效果如下:

添加交互效果

1.用CSS控制悬浮样式,让连接线在鼠标悬停的时候高亮显示:

1
2
3
4
5
<style >
.link:hover {
stroke-opacity: .8;
}
</style>

2.节点添加拖动事件

  • 定义一个拖动事件,这里仅限于水平方向的移动,移动之后重新布局并生成行的路径

    1
    2
    3
    4
    5
    6
    7
    function dragmove(d) {
    d3.select(this)
    .attr("transform","translate(" + (d.x = Math.max(0,Math.min(width - d.dy, d3.event.x))) + "," + d.y + ")") ;
    sankey.relayout(); // 重新布局
    link.attr("d",path);
    }
  • 对node节点添加事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    node.call(d3.behavior.drag() //这一段只知道大概是什么意思
    .origin(function(d){
    return d;
    })
    .on("dragstart",function() {
    this.parentNode.appendChild(this);
    })
    .on("drag",dragmove)
    );

好了,现在我们就可以做出首页第一张sankey图的效果了,最后再附上自己做的另一张横版sankey图(好像是由于sankey.js插件的问题,导致横竖排版)

Paste_Image.png

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="js/d3.js" charset="utf-8"></script>
<script src="js/d3-sankey-1.js" charset="utf-8"></script>
<style >
body {
background-color: white;
}
#chart {
height: 650px; /* must at least match the svg, to place content after it!*/
}
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
}
.link {
fill: none;
/*stroke: #000;*/
stroke-opacity: .5;
}
.link:hover {
stroke-opacity: .8;
}
</style>
</head>
<body>
<p id="chart"></p>
<script>
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 1000 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // 数字转字符串 逗号分隔,0位小数点
format = function(d) {return formatNumber(d) + "m CHF";};
var color = d3.scale.category20();
var svg = d3.select("#chart")//ID选择器
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
;
// 布局
var sankey = d3.sankey()
.nodeWidth(25) // 节点宽度
.nodePadding(20) // 矩形垂直方向的间距
.size([width,height])
// .nodes(data.nodes)
// .links(data.links)
// .layout(3)
;
var path = sankey.link();
console.log(path);
d3.json("http://benlogan1981.github.io/VerticalSankey/data/ubs.json", function(error, energy) {
sankey
.nodes(energy.nodes)
.links(energy.links)
.layout(32);
var link = svg.append("g").selectAll("path")
.data(energy.links)
.enter()
.append("path")
.attr("class","link")
.attr("d",path) // 路径链接已被sankey封装好
.style("stroke-width",function(d){
return Math.max(1,d.dy);
})
// .style("stroke",function(d) {
// console.log(d.source.name.replace(/ .*/,""));
// })
.style("stroke",function(d) {
return d.source.color = color(d.source.name.replace(/ .*/,""));
})
.sort(function(a,b){
return b.dy - a.dy;
})
;
link.append("title")
.text(function(d){
return d.source.name + "->" + d.target.name + "\n" + format(d.value) ;
});
var node = svg.append("g").selectAll("g")
.data(energy.nodes)
.enter()
.append("g")
.attr('class', "node")
.attr('transform', function(d){
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.behavior.drag()
.origin(function(d){
return d;
})
.on("dragstart",function() {
this.parentNode.appendChild(this);
})
.on("drag",dragmove)
)
;
node.append("rect")
.attr("height",sankey.nodeWidth())
.attr("width",function(d) { return d.dy; })
.style("fill",function(d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("stroke",function(d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function(d) {
return d.name + "\n" + format(d.value) ;
})
;
node.append("text")
.attr("text-anchor","middle")
.attr("x",function(d) {
return d.dy/2;
})
.attr("y",function(d) {
sankey.nodeWidth() / 2;
})
.attr("dy","1em")
.text(function(d) { return d.name; })
.filter(function(d) {
return d.x < width / 2 ;
})
;
function dragmove(d) {
d3.select(this).attr("transform","translate(" + (d.x = Math.max(0,Math.min(width - d.dy, d3.event.x))) + "," + d.y + ")") ;
sankey.relayout();
link.attr("d",path);
}
});
</script>
</body>
</html>

「参考资料」

【D3 Tips and Tricks v4.x】
【D3.js数据可视化实战】—(3)桑基图(sankey)的绘制
【USB 2015 Q1 Results】