D3学习系列(二) 弦图绘制

「什么时候用弦图」

Chord Diagram主要是用来表示多个节点之间的关系,假设我们要表示5个节点之间的关系,那么输入的矩阵是下面这个样子,且必须是方阵。节点A的长度是元素A所在行的总和,就是(A, A)、(A, B)、(A, C)、(A, D)、(A, E)的和。图中C与D之间的弦表示C和D相关,与C相接的弧长实际上是(C, D)的值。

Paste_Image.png

「绘制弦图」

导入初始数据

1
2
3
4
5
6
7
8
var city_name = [ "北京" , "上海" , "广州" , "深圳" , "香港" ];
var population = [
[ 1000, 3045 , 4567 , 1234 , 3714 ],
[ 3214, 2000 , 2060 , 124 , 3234 ],
[ 8761, 6545 , 3000 , 8045 , 647 ],
[ 3211, 1067 , 3214 , 4000 , 1006 ],
[ 2146, 1034 , 6745 , 4764 , 5000 ]
];

布局(转换数据)

1
2
3
4
5
6
7
8
// 弦布局
var chord_layout = d3.layout.chord()
.padding(0.03)
.sortSubgroups(d3.descending)
.matrix(population);
// 布局转化数据
var groups = chord_layout.groups();
var chords = chord_layout.chords();

padding(0.03)表示弧与弧之间的间隔,population是之前输入的人口数据。经过布局之后会生成两块,一块是groups,表示节点;另一块是chords,表示弦(连线),chords里面还会分source与target,表示连线的两端。

绘制画布SVG

1
2
3
4
5
6
7
8
9
10
// svg画布
var width = 600;
var height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr('height', height)
.append("g")
.attr('transform', 'translate(' + width/2 + "," + height/2 + ")");
var color20 = d3.scale.category20();

先创建一个SVG元素,里面添加一个g元素同时设置平移属性(用来确定弦图的中心)。然后再在g元素添加2个g元素,分别用来装节点与弦,结构如下所示:

<svg>
 --<g>
 ---- <g>``</g>
 ---- <g>``</g>
 --</g>
</svg>

绘制节点(弧)

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
// 弧生成器
var innerRadius = width/2 * 0.7;
var outerRadius = innerRadius * 1.1;
var outer_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
// 绘制节点
var g_outer = svg.append("g");
g_outer.selectAll("path")
.data(groups)
.enter()
.append("path")
.style("fill",function(d) {
return color20(d.index);
})
.style("stroke",function(d) {
color20(d.index);
})
.attr("d",outer_arc) // 此处调用了弧生成器
;
// 节点文字
g_outer.selectAll("text")
.data(groups)
.enter()
.append("text")
.each(function(d,i) { // 对每个绑定的数据添加两个变量
d.angle = (d.startAngle + d.endAngle) / 2;
d.name = city_name[i];
})
.attr("dy",".35em")
.attr('transform', function(d) { // 平移属性
var result = "rotate(" + (d.angle*180/Math.PI) + ")";
result += "translate(0," + -1 * (outerRadius + 10) + ")";
if (d.angle > Math.PI * 3 / 4 && d.angle < Math.PI * 5 / 4 )
result += "rotate(180)";
return result;
})
.text(function(d) {
return d.name;
});

在标记文字的地方要注意:

  1. each():表示对任何一个绑定数据的元素,都执行后面的无名函数 function(d,i) ,计算文字的角度与内容
  2. transform():不仅需要考虑文字的旋转角度与平移距离,还要考虑如果文字在下方是会是倒写的情况。

生成如下图:

绘制连线(弦)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 弦生成器
var inner_chord = d3.svg.chord()
.radius(innerRadius);
// 绘制内部弦
var g_inner = svg.append("g")
.attr("class","chord");
g_inner.selectAll("path")
.data(chords)
.enter()
.append("path")
.attr("d",inner_chord) // 调用弦的路径值
.style("fill",function(d) {
return color20(d.source.index);
})
.style("opacity",1)
;

这样就生成了首页的弦图,但是当数据多了之后,会看不清节点与节点之间的关系,我们可以添加一些交互式的操作解决。如当鼠标移到该节点,只会显示与该节点相接的弦,其他的会被隐藏。这里我们定义一个fade函数,并在节点(弧)上通过mouseovermouseout添加动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fade(opacity){
return function(g,i){
g_inner.selectAll("path")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity",opacity);
}
}
g_outer.selectAll("path")
.on("mouseover",fade(0.0)) // 0.0完全透明
.on("mouseout",fade(1.0)) // 1.0完全不透明
;

效果如图:

「源代码」

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style>
.chord path{
fill-opacity: 0.67;
stroke: #000;
stroke-width: 0.5px;
}
</style>
</head>
<body>
<script>
// 初始数据
var city_name = [ "北京" , "上海" , "广州" , "深圳" , "香港" ];
var population = [
[ 1000, 3045 , 4567 , 1234 , 3714 ],
[ 3214, 2000 , 2060 , 124 , 3234 ],
[ 8761, 6545 , 3000 , 8045 , 647 ],
[ 3211, 1067 , 3214 , 4000 , 1006 ],
[ 2146, 1034 , 6745 , 4764 , 5000 ]
];
// 弦布局
var chord_layout = d3.layout.chord()
.padding(0.03)
.sortSubgroups(d3.descending)
.matrix(population);
// 布局转化数据
var groups = chord_layout.groups();
var chords = chord_layout.chords();
console.log(groups);
console.log(chords);
// svg画布
var width = 600;
var height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr('height', height)
.append("g")
.attr('transform', 'translate(' + width/2 + "," + height/2 + ")");
var color20 = d3.scale.category20();
// 弧生成器
var innerRadius = width/2 * 0.7;
var outerRadius = innerRadius * 1.1;
var outer_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
// 绘制节点
function fade(opacity){
return function(g,i){
g_inner.selectAll("path")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity",opacity);
}
}
var g_outer = svg.append("g");
g_outer.selectAll("path")
.data(groups)
.enter()
.append("path")
.style("fill",function(d) {
return color20(d.index);
})
.style("stroke",function(d) {
color20(d.index);
})
.attr("d",outer_arc) // 此处调用了弧生成器
.on("mouseover",fade(0.0)) // 0.0完全透明
.on("mouseout",fade(1.0)) // 1.0完全不透明
;
g_outer.selectAll("text")
.data(groups)
.enter()
.append("text")
.each(function(d,i) { // 对每个绑定的数据添加两个变量
d.angle = (d.startAngle + d.endAngle) / 2;
d.name = city_name[i];
})
.attr("dy",".35em")
.attr('transform', function(d) { // 平移属性
var result = "rotate(" + (d.angle*180/Math.PI) + ")";
result += "translate(0," + -1 * (outerRadius + 10) + ")";
if (d.angle > Math.PI * 3 / 4 && d.angle < Math.PI * 5 / 4 )
result += "rotate(180)";
return result;
})
.text(function(d) {
return d.name;
});
// 弦生成器
var inner_chord = d3.svg.chord()
.radius(innerRadius);
// 绘制内部弦,一共有5*5=25条
var g_inner = svg.append("g")
.attr("class","chord");
g_inner.selectAll("path")
.data(chords)
.enter()
.append("path")
.attr("d",inner_chord) // 调用弦的路径值
.style("fill",function(d) {
return color20(d.source.index);
})
.style("opacity",1)
;
</script>
</body>
</html>