梦入琼楼寒有月,行过石树冻无烟

D3 弦布图


弦图(Chord Diagram),是一种表示一组元素之间关系的学术图表之一。主要可以分为外部节点内部弦。通过弦图布局(Chord Layout)可以将方块矩阵转换成一个可用于绘制节点的数据。

ID DA FA
d3.layout.chord() 创建弦布图
chord.matrix() 设置或获取矩阵(必须为方块矩阵,行列数相等)
chord.padding() 设置或获取元素节点之间的间距,默认为0
chord.sortGroups() 对节点进行排序
chord.sortSubgroups() 对各点所在行的数据进行排序
chord.sortChords() 对弦进行排序
chord.chords() 返回弦数组
chord.groups() 返回节点数组

布局运算


使用弦布图(Chord Layout)可以直接通过 d3 来计算出startAngle and endAngle等,并通过使用chords() and groups()来直接输出弦图的弦数组以及节点数组,需要注意的是弦图使用的数据添加方法采用了方块矩阵

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
var padding = {
width: 800,
height: 800
}

var svg = d3.select("svg")
.append("svg")
.attr("width",padding.width)
.attr("height",padding.height)

var node = [
"长春市","四平市","吉林市","松原市","通化市","白城市"
]

var nodeData = [
[0,20.56,14.16,11.65,4.87,4.76],
[0,20.56,14.16,11.65,4.87,4.76],
[53.57,0,3.09,4.77,1.5,1.43],
[53.57,4.48,0,3.19,4.86,1.59],
[43.96,5.91,2.59,0,0.62,11.13],
[21.22,2.6,8.81,0.84,0,0]
]

var chord = d3.layout.chord()
.padding(0.03)
.sortSubgroups(d3.ascending)
.matrix(nodeData)

console.log(chord.groups())
console.log(chord.chords())

弦布图

在下述 code 中分别添加了三个<g>元素,分别包括了位置、节点、弦,分别代表了gOuter(g)、gInner(g)、path(outerPath)、text(outerText)、innerPath(path),在 web 展示的结构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<svg>
<!-- gChord -->
<g>
<!-- gOuter -->
<g>
<path></path> <!-- outerPath / path -->
<text></text> <!-- outerText / text -->
</g>
<!-- gInner -->
<g>
<path></path> <!-- innerPath / path -->
</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
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
/* 基本 svg 规格 */
var padding = {
width: 800,
height: 800
}

var svg = d3.select("body")
.append("svg")
.attr("width",padding.width)
.attr("height",padding.height)

/* 弦图数据 */
var node = [
"长春市","四平市","吉林市","松原市","通化市","白城市"
]

var nodeData = [
[0,20.56,14.16,11.65,4.87,4.76],
[0,20.56,14.16,11.65,4.87,4.76],
[53.57,0,3.09,4.77,1.5,1.43],
[53.57,4.48,0,3.19,4.86,1.59],
[43.96,5.91,2.59,0,0.62,11.13],
[21.22,2.6,8.81,0.84,0,0]
]

/* 布局运算 */
var chord = d3.layout.chord()
.padding(0.03)
.sortSubgroups(d3.ascending)
.matrix(nodeData)

/* 计算出 <g> 元素位置 */
var gChord = svg.append("g")
.attr("transform",
"translate(" + padding.width/2 + "," + padding.height/2 + ")")


/* 节点和弦的 <g> 元素 */
var gOuter = gChord.append("g")
var gInner = gChord.append("g")

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

/* 内部与外部半径 */
var innerRadius = padding.width / 2 * 0.7
var outerRadius = innerRadius * 1.1

/* 弧生成器 */
var arcOuter = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)

/*
|- 添加 path 元素
|- 绑定弧生成器 arcOuter
*/
gOuter.selectAll(".outerPath")
.data(chord.groups())
.enter()
.append("path")
.attr("class","outerPath")
.style("fill", function (d) {
return color(d.index)
})
.attr("d", arcOuter)

/*
|- 绘制节点文字
|- 选择节点文字 node
*/
gOuter.selectAll(".outerText")
.data(chord.groups())
.enter()
.append("text")
/* 计算出弧中心角度 */
.each(function (d,i) {
d.angle = (d.startAngle + d.endAngle) /2
d.name = node[i]
})
.attr("class","outerText")
.attr("dy",".35em")
/*
|- 旋转 d.angle
*/
.attr("transform", function (d) {
var result = "rotate(" + (d.angle * 180 / Math.PI) + ")"

/*
|- 平移到半径外
*/
result += "translate(0," + -1.0 * (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 arcInner = d3.svg.chord()
.radius(innerRadius)

/* 绑定数据并生成弦 */
gInner.selectAll(".innerPath")
.data(chord.chords())
.enter()
.append("path")
.attr("class","innerPath")
.attr("d",arcInner)
.style("fill", function (d) {
return color (d.source.index)
})

less

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@colorWhite: #ffffff;
.outerPath{
stroke: @colorWhite;
}

.outerText{
text-anchor: middle;
font-size: 16px;
fill: @color;
}

.innerPath{
stroke: @colorWhite;
}

为了体现出外弦节点的区分,我们需要为outerPath(外弦路径)、outerText(外弦文字/节点文字)、innerPath(内弦路径)添加样式。

css

1
2
3
4
5
6
7
8
9
10
11
12
13
.outerPath{
stroke: #ffffff;
}

.outerText{
text-anchor: middle;
font-size: 16px;
fill: #929292;
}

.innerPath{
stroke: #ffffff;
}

behavior

为了增加观感上的体验,我们为弦布图增加了behavior,主要通过使用使用mouseover(鼠标光标) and mouseout(光标移出),然后通过d.source.index !=i && d.target.index !=i排除光标选择外的数据,在通过添加过渡来实现效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*  |_ 鼠标事件
|_| _ mouseover 光标接触
|_| _ mouseout 光标移出
*/
gOuter.selectAll(".outerPath")
.on("mouseover", fade(0.0))
.on("mouseout", fade(1.0))

/*
|_ 透明与半透明方法
|_|_ 选择鼠标接触到的内部弦
|_|_ 设置过度与延迟
*/
function fade(opacity) {
return function (g,i) {
gInner.selectAll(".innerPath")
.filter(function (d) {
return d.source.index !=i && d.target.index !=i
})
.transition()
.duration(2000)
.style("opacity", opacity)
}
}
⬅️ Go back