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

📖 earlier posts 📖

D3 矩阵布局


矩阵布局(Treemap)是一个层级布局或分层布局扩展,最早由Ben Shneiderman1991进行提出,可以通过对一块举行进行切分,并达到层级演示的效果。会通过使用将一块类似于近正矩形并进行切块,之后将数据根据矩形树图算法进行计算。

ID DA FA
d3.layout.treemap() 创建矩阵数布局
treemap.nodes(root) 根据root进行计算,并获取节点数据
treemap.links(nodes) 根据 nodes 进行计算,获取连线数组
treemap.children([children]) 设置或获取子节点访问器(默认情况下子节点对象是children
treemap.sort([comparator]) 设置或获取布局的节点排序数据
treemap.value([value]) 设置或获取值访问器
treemap.size([size]) 设置或获取布局尺寸,(有两个参数分别为 表示以及
treemap.padding([padding]) 设置或获取矩阵单元之间的间隔(单位为像素)
treemap.round([round]) 设置或获取是否对计算结果是否进行四舍五路
treemap.sticky([sticky]) 设置或获取矩阵树图是否有粘性,粘性属性布局将保留整个过度中节点的相对排序
treemap.ratio([ratio]) 设置或获取矩阵树图是否有粘性
treemap.mode([mode]) 设置矩阵布局模式,不同算法之间的有很大的差异

确定数据

下述我们通过计算出广州省、吉林省、河南省的人口迁移数据,通过新建json文件来进行写入:

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
{
"name": "中国",
"children":
[
{
"name": "广州省",
"children": [
{"name": "佛山市", "ratio": 25.82},
{"name": "东莞市", "ratio": 10.26},
{"name": "深圳市", "ratio": 8.53},
{"name": "清远市", "ratio": 5.22},
{"name": "中山市", "ratio": 3.56},
{"name": "惠州市", "ratio": 3.49},
{"name": "肇庆市", "ratio": 2.85},
{"name": "江门市", "ratio": 2.52},
{"name": "珠海市", "ratio": 1.92},
{"name": "湛江市", "ratio": 1.62}
]
},
{
"name": "吉林省",
"children":[
{"name": "辽阳市", "ratio": 11.82},
{"name": "哈尔滨市", "ratio": 10.55},
{"name": "北京市", "ratio": 6.56},
{"name": "铁岭市", "ratio": 5.77},
{"name": "通辽市", "ratio": 5.14},
{"name": "兴安盟", "ratio": 4.46},
{"name": "大连市", "ratio": 3.34},
{"name": "大庆市", "ratio": 2.86},
{"name": "天津市", "ratio": 2.27},
{"name": "齐齐哈尔市", "ratio": 1.88}
]
},
{
"name": "河南省",
"children": [
{"name": "辽阳市","ratio": 11.82},
{"name": "哈尔滨市","ratio": 10.55},
{"name": "北京市", "ratio": 6.56},
{"name": "铁岭市","ratio": 5.77},
{"name": "通辽市","ratio": 5.14},
{"name": "兴安盟","ratio": 4.46},
{"name": "大连市","ratio": 3.34},
{"name": "大庆市","ratio": 2.86},
{"name": "天津市","ratio": 2.27},
{"name": "齐齐哈尔市","ratio": 1.88}
]
}
]
}

布局运算


上述的 json 数据是无法直接被 d3.js所识别的,所以需要通过使用布局,将得到node以及links的连线关系并进行转换,上图我们以河南省数据为例,转换如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
转换数据 -> nodes(节点) -> links (连线关系)
*/
var treemap = d3.layout.treemap()
.size([padding.width,padding.height])
.value(function (d) {
return d.ratio
})


d3.json("treemap.json",function (error, root) {
var nodes = treemap.nodes(root)
var links = treemap.links(nodes)

console.log(nodes)
console.log(links)
})

绘制矩阵

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

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

/*
转换数据 -> nodes(节点) -> links (连线关系)
*/
var treemap = d3.layout.treemap()
.size([padding.width,padding.height])
.value(function (d) {
return d.ratio
})


d3.json("treemap.json",function (error, root) {
var nodes = treemap.nodes(root)
var links = treemap.links(nodes)

console.log(nodes)
console.log(links)

/* 选择颜色 */
var color = d3.scale.category20c()

/* <g> 组 */
var groups = svg.selectAll("g")
.data(nodes.filter(function (d) {
return !d.children
}))
.enter()
.append("g")

/* 矩形 */
var rects = groups.append("rect")
.attr("class","nodeRect")
.attr("x", function (d) {
return d.x
})
.attr("y", function (d) {
return d.y
})
.attr("width", function (d) {
return d.dx
})
.attr("height", function (d) {
return d.dy
})
.style("fill", function (d,i) {
return color(d.parent.name)
})

/* 文字 */
var text = groups.append("text")
.attr("class","nodeName")
.attr("x", function (d) {
return d.x
})
.attr("y", function (d) {
return d.y
})
.attr("dx", "0.5em")
.attr("dy", "1.5em")
.text(function (d) {
return d.name + "" + d.ratio
})
})

CSS

1
2
3
4
5
6
7
8
.nodeRect {
stroke: white;
}
.nodeName {
fill: white;
font-size: 12px;
font-weight: bold;
}

D3 直方布局

直方布局(Histogram Layout),可以用于表示数据的分布。可分为矩阵直方图(Matrix histogram)曲线直方图(Curve histogram)两种表达方式。

ID DA FA
d3.layout.histogram() 创建一个直方图布局
histogram.value() 设置或获取值访问器
histogram.range() 设置数据的分布范围,主要分为两个数组指定(分别表述上限(min)以及下线(max)
histogram.bins() 设置数据的分布区间的个数或长度,如果没有参数则默认返回当前设置的值
histogram.bins(count) count作为参数则代表数据的分布区间(bins) 的数量,bins的区间长度则会被均匀的分割
histogram.bins(thresholds) 可以指定任意长度的区间(如:[100, 125, 130, 135, 140, 145]
histogram.bins(function) 当前函数必须返回 一个thresholds 数组
histogram.frequency(frequency) 值为true为统计数量;反之false则是统计频率
d3.random.normal() 用于生成正态分布,主要用于表述一个不明的随机变量 ,通常第一个参数用于表述平均值,第二个参数是标准差

数据转换与确定数据

normal 确定数据


normal 即 正态分布,主要用于表示出一个不明的随机变量,通常第一个参数用于表述出平均值,而第二个参数即代表标准差,(Standard Deviation)在概率统计上标准差定义是总体各单位的标准值与其平均数离差的算数平均数的平方根,可以反映出个体间的离散程度,通过下述 Code 你会感觉到每次输出的数据坐标,都是不一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
|_ 创建初始数据
|_|-- 通过 databases 来保存 rand 数据
|_|-- 根据 rand 生成平均值为170,差值为10,保存至 databases 中。

*/
var rand = d3.random.normal(170,50);
var databases = []

for(var i =0; i<100;i++) {
databases.push(rand())
}

console.log(databases)

转换数据

通过使用normal生成的随机数据是人类无法直接理解其意思的,因此需要通过使用其直方布局进行转换出人类可理解的数据含义,主要有三个变量,分别为x:区间的下限值、dx:区间的长度、y:此处的区间数量(frequency true)或频率(frequency false)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
|_ 转换数据
|_|-- 数据的分布范围 rangeMin / rangeMax
|_|-- 数据的分布区间 binNum
*/
var binding = {
binNum: 20,
rangeMin: 130,
rangeMax: 210
}

var histogram = d3.layout.histogram()
.range([binding.rangeMin,binding.rangeMax])
.bins(binding.binNum)
.frequency(true)

var hisData = histogram(databases)
console.log(hisData)

绘制

矩形直方图


通过使用 normal 以及 Histogram Layout计算出数据后,通过比例尺来创建x\y轴并绘制矩形,最后形成为矩形直方图。

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
var padding = {
width: 500,
height: 500,
top: 20,
right: 30,
bottom: 30,
left: 30
}

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

/*
|_ 创建初始数据
|_|-- 通过 databases 来保存 rand 数据
|_|-- 根据 rand 生成100个随机数据,保存至 databases 中。

*/
var rand = d3.random.normal(170,50);
var databases = []

for(var i =0; i<100;i++) {
databases.push(rand())
}

console.log(databases)
/*
|_ 转换数据
|_|-- 数据的分布范围 rangeMin / rangeMax
|_|-- 数据的分布区间 binNum
*/
var binding = {
binNum: 20,
rangeMin: 130,
rangeMax: 210
}

var histogram = d3.layout.histogram()
.range([binding.rangeMin,binding.rangeMax])
.bins(binding.binNum)
.frequency(true)

var hisData = histogram(databases)
console.log(hisData)

/* 定义 x 轴比例尺 */
var xAxisWidth = 450,
xTicks = hisData.map( function (d) {
return d.x
})

var xScale = d3.scale.ordinal()
.domain(xTicks)
.rangeRoundBands([0, xAxisWidth],0.1);


/* 定义 y 轴比例尺 */
var yAxisWidth = 450;
var yScale = d3.scale.linear()
.domain([d3.min(hisData, function (d) {
return d.y
}),
d3.max(hisData, function (d) {
return d.y
})])
.range([padding.height - padding.top - padding.bottom ,0])

/*
分别定义 x/y 轴比例尺
绘制比例尺。
*/
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(d3.format(".0f"))

svg.append("g")
.attr("class","axis")
.attr("transform", "translate(" + padding.left + "," + (padding.height - padding.bottom) + ")")
.call(xAxis)

var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickFormat(d3.format(".0f"))

svg.append("g")
.attr("class","axis")
.attr("transform", "translate("+ padding.left + "," + padding.top + ")")
.call(yAxis)

/* 绘制矩形 */
var gRect = svg.append("g")
.attr("transform", "translate(" + padding.left + "," + (- padding.bottom + -10) + ")" )
.style("opacity",1.0)

gRect.selectAll("rect")
.data(hisData)
.enter()
.append("rect")
.attr("class","rect")
.attr("x", function (d,i) {
return xScale(d.x)
})
.attr("y", function (d,i) {
return padding.height - yScale(d.y)
})
.attr("width", function (d,i) {
return xScale.rangeBand()
})
.attr("height", function (d) {
return yScale(d.y)
})

需要注意的是,我们需要为此添加CSS样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@color: #929292;
.axis path, /* 坐标轴路径设置 */
.axis line { /* 设置 坐标轴的线条 */
fill: none; /* 设置坐标轴宽度 */
stroke: @color; /* 坐标轴颜色 */
shape-rendering: crispEdges; /* 将形状渲染为清晰的边缘 */
}

.axis text {
font-family: sans-serif;
font-size: 11px;
fill: @color;
}

rect {
fill: @color;
}

曲线直方图


相比与矩形直方图( Rectangle Histogram)来比,曲线直方图(Curve Histogram)更加的的连贯,在下述的 code 中,主要将坐标轴以及比例尺进行倒序,之后通过line将其绘制路径。

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
var padding = {
width: 500,
height: 500,
top: 20,
right: 30,
bottom: 30,
left: 30
}

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

/*
|_ 创建初始数据
|_|-- 通过 databases 来保存 rand 数据
|_|-- 根据 rand 生成100个随机数据,保存至 databases 中。

*/
var rand = d3.random.normal(170,10);
var databases = []

for(var i =0; i<100;i++) {
databases.push(rand())
}

console.log(databases)
/*
|_ 转换数据
|_|-- 数据的分布范围 rangeMin / rangeMax
|_|-- 数据的分布区间 binNum
*/
var binding = {
binNum: 20,
rangeMin: 130,
rangeMax: 210
}

var histogram = d3.layout.histogram()
.range([binding.rangeMin,binding.rangeMax])
.bins(binding.binNum)
.frequency(true)

var hisData = histogram(databases)
console.log(hisData)

/* 定义 x 轴比例尺 */
var xAxisWidth = 450,
xTicks = hisData.map( function (d) {
return d.x
})

var xScale = d3.scale.ordinal()
.domain(xTicks)
.rangeRoundBands([0, xAxisWidth],0.1);


/* 定义 y 轴比例尺 */
var yAxisWidth = 450;
var yScale = d3.scale.linear()
.domain([d3.min(hisData, function (d) {
return d.y
}),
d3.max(hisData, function (d) {
return d.y
})])
.range([0, yAxisWidth])

/* 绘制路径 */
var lineGenerator = d3.svg.line()
.x(function(d){ return xScale(d.x); })
.y(function(d){ return padding.height - yScale(d.y); })
.interpolate("basis");

var gLine = svg.append("g")
.attr("transform","translate(" + padding.left + "," + ( -padding.bottom ) + ")")

gLine.append("path")
.attr("fill","none")
.attr("stroke-width",1)
.attr("stroke","#929292")
.attr("d",lineGenerator(hisData));

/*
分别定义 x/y 轴比例尺
绘制比例尺。
*/
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")

yScale.range([yAxisWidth, 0])

var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")

svg.append("g")
.attr("class","axis")
.attr("transform", "translate(" + padding.left + "," + (padding.height - padding.bottom) + ")")
.call(xAxis)

svg.append("g")
.attr("class","axis")
.attr("transform", "translate(" + padding.left + "," + (padding.height - padding.bottom - yAxisWidth) + ")")
.call(yAxis)

D3 捆布图

捆布图(Bundle Layout),即捆图(Bundle),主要实现了Danny Holten的分层边缘捆绑算法,对于每个输入的连接都会计算出树的路径,从父层次结构上达到最小公共节点,之后在返回目标节点,最终生成捆绑样条,捆布图的主要作用就是计算出连线的路径

ID DA FA
d3.layout.bundle() 创建饼布图
bundle(links) 根据数组链接的来源(source)与目标(target)来计算出路径

层级布局

层级布局(Hierarchical layout)即采用了嵌套结构(父节点与字节点)来描述信息的布局,通过层级布局总共延伸了集群图(Cluster)、打包图(Pack)、分区图(Paratition)、树图(Tree)以及矩阵树图(Treemap)

布局运算


首先通过使用集群图布局进行计算出节点,来符合捆图使用的节点数据,在通过使用mapgitLink转换为节点对象,在通过使用捆图布局转换成捆图数据。

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/*
基本 svg 样式
*/
var padding = {
width: 800,
height: 800
}

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

/* 捆图数据 */
var gitName = {
name: "",
children: [
{name: "gitee"},
{name: "Kooder"},
{name: "CEF4Delphi"},
{name: "Debian-Pi-Aarch64"},
{name: "QwidgetExe"},
{name: "debian-cn"},
{name: "PowerToys"},
{name: "FastAPI"},
{name: "vscode-drawio"},
{name: "PaddleDetection"},
{name: "QWidgetExe"},
{name: "Stduino IDE"},
{name: "react-photo-view"},
{name: "Waifu2x-Extension-GUI"},
{name: "egg-decorator-router"},
{name: "PowerToys"},
{name: "InjectFix"},
{name: "dnSpy"},
{name: "proxyer"},
{name: "wenyan-lang"},
{name: "Ventoy"}
]
}

var gitLink = [
{
source: "gitee",
target: "Kooder"
},
{
source: "gitee",
target: "CEF4Delphi"
},
{
source: "gitee",
target: "CEF4Delphi"
},
{
source: "gitee",
target: "Debian-Pi-Aarch64"
},
{
source: "gitee",
target: "QwidgetExe"
},
{
source: "gitee",
target: "debian-cn"
},
{
source: "gitee",
target: "PowerToys"
},
{
source: "gitee",
target: "FastAPI"
},
{
source: "gitee",
target: "vscode-drawio"
},
{
source: "gitee",
target: "PaddleDetection"
},
{
source: "gitee",
target: "QWidgetExe"
},
{
source: "gitee",
target: "Stduino IDE"
},
{
source: "gitee",
target: "react-photo-view"
},
{
source: "gitee",
target: "Waifu2x-Extension-GUI"
},
{
source: "gitee",
target: "egg-decorator-router"
},
{
source: "gitee",
target: "PowerToys"
},
{
source: "gitee",
target: "InjectFix"
},
{
source: "gitee",
target: "dnSpy"
},
{
source: "gitee",
target: "proxyer"
},
{
source: "gitee",
target: "wenyan-lang"
},
{
source: "gitee",
target: "Ventoy"
}
]

/*
|_ 集群图布局
|__|- 计算出集群图布局节点
|_ 捆图布局
*/
var cluster = d3.layout.cluster()
.size([360, padding.width /2 -50])
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth
})

var bundle = d3.layout.bundle()

var nodes = cluster.nodes(gitName)
console.log(nodes)

/*
|_ 将连线端转换为节点对象
|_ 调用捆布图转换数据
*/
var oLinks = map(nodes, gitLink)
console.log(oLinks)

var links = bundle(oLinks)
console.log(links)

/*
将 source and 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
}

捆布图


通过放射性线段生成器来绘制出捆图的路径(path),再使用node计算出节点位置并添加结点 <circle> and <text>最终绘制出捆图

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/*
基本 svg 样式
*/
var padding = {
width: 800,
height: 800
}

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

/* 捆图数据 */
var gitName = {
name: "",
children: [
{name: "gitee"},
{name: "Kooder"},
{name: "CEF4Delphi"},
{name: "Debian-Pi-Aarch64"},
{name: "QwidgetExe"},
{name: "debian-cn"},
{name: "PowerToys"},
{name: "FastAPI"},
{name: "vscode-drawio"},
{name: "PaddleDetection"},
{name: "QWidgetExe"},
{name: "Stduino IDE"},
{name: "react-photo-view"},
{name: "Waifu2x-Extension-GUI"},
{name: "egg-decorator-router"},
{name: "InjectFix"},
{name: "dnSpy"},
{name: "proxyer"},
{name: "wenyan-lang"},
{name: "Ventoy"}
]
}

var gitLink = [
{
source: "gitee",
target: "Kooder"
},
{
source: "gitee",
target: "CEF4Delphi"
},
{
source: "gitee",
target: "CEF4Delphi"
},
{
source: "gitee",
target: "Debian-Pi-Aarch64"
},
{
source: "gitee",
target: "QwidgetExe"
},
{
source: "gitee",
target: "debian-cn"
},
{
source: "gitee",
target: "PowerToys"
},
{
source: "gitee",
target: "FastAPI"
},
{
source: "gitee",
target: "vscode-drawio"
},
{
source: "gitee",
target: "PaddleDetection"
},
{
source: "gitee",
target: "QWidgetExe"
},
{
source: "gitee",
target: "Stduino IDE"
},
{
source: "gitee",
target: "react-photo-view"
},
{
source: "gitee",
target: "Waifu2x-Extension-GUI"
},
{
source: "gitee",
target: "egg-decorator-router"
},
{
source: "gitee",
target: "PowerToys"
},
{
source: "gitee",
target: "InjectFix"
},
{
source: "gitee",
target: "dnSpy"
},
{
source: "gitee",
target: "proxyer"
},
{
source: "gitee",
target: "wenyan-lang"
},
{
source: "gitee",
target: "Ventoy"
}
]

/*
|_ 集群图布局
|__|- 计算出集群图布局节点
|_ 捆图布局
*/
var cluster = d3.layout.cluster()
.size([360, padding.width /2 -50])
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth
})

var bundle = d3.layout.bundle()

var nodes = cluster.nodes(gitName)
console.log(nodes)

/*
|_ 将连线端转换为节点对象
|_ 调用捆布图转换数据
*/
var oLinks = map(nodes, gitLink)
console.log(oLinks)

var links = bundle(oLinks)
console.log(links)

/*
将 source and 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 即捆图
|__| 将张力设置为 45
*/
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.45)
.radius(function (d) {
return d.y
})
.angle(function (d) {
return d.x / 180 * Math.PI
})

/* 坐标组 <g> 元素 */
gBundel = svg.append("g")
.attr("transform", "" +
"translate(" + (padding.width/2) + "," + (padding.height/2) + ")")

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

/* 绘制连线路径 <path> */
var link = gBundel.selectAll(".link")
.data(links)
.enter()
.append("path")
.attr("class","link")
.attr("d", line)

/* 添加节点 */
var node = gBundel.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) + ")"
})

/* 绘制节点圆形 <circle> */
node.append("circle")
.attr("r",1)
.style("fill", function (d,i) {
return color(i)
})

/* 绘制节点名称 <text> */
node.append("text")
.attr("dy",".12em")
.style("text-anchor","middle")
.text(function (d) {
return d.name
})

CSS

1
2
3
4
5
6
7
8
9
.link {
fill: transparent;
stroke: #e5e5e5;
}
.node {
font-weight: bold;
font-size: 12px;
fill: #242424;
}

D3 树布图

树图(Tree Diagram),实在区域内按照规则布置节点(node),之后通过连线(link)连接起来的图形之一,在数据可视化中被成为树布图,或称之为流程图,通过布局运算后的树图将会被称之为“树布图(Tree Layout)

ID DA FA
d3.layout.tree() 创建一个树布图
tree.size(width,[height]) 设置或获取大小,分别表示宽和高
tree.nodeSize() 设置或获取各节点的大小
tree.value() 设置或获取值访问器,默认为空,如果设置则会在此节点中多出一个value
tree.children() 设置或获取各个节点的访问器
tree.sort() 设置或获取排序的比较器
tree.separation() 设置或获取相邻节点之间的间隔
tree.nodes() 根据根进行计算,获取节点数组
tree.links() 根据 nodes计算,获取连线数组

布局运算

确定数据

D3 的树布图的原始数据主要通过使用.json进行写入,在 .json中主要可以分为name以及children,将用于定义叶子的名称以及划分出一个分支,即子节点并继续写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "开始",
"children": [
{"name": "子节点"},
{
"name": "子节点",
"children": [
{"name": "子节点的子节点"},
{
"name": "子节点的子节点",
"children": [
{
"name": "子节点的子节点的子节点"
}
]
}
]
},
{"name": "子节点"}
]
}

计算数据


通过使用树布图(Tree Layout)将会计算出树图的基本节点以及连线数据(source and target)通常节点(nodes)他会共输出parent(父节点)、children(子节点)、depth(节点深度)、x\y(节点x\y坐标),而连线 (links)将会输出source and targe

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

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

/*
设置 x 的位置为 80
*/
var gTree = svg.append("g")
.attr("transform","translate(80,0)")

/*
|_ 开始布局运算
|__|- 获取长宽高并对高-200
|__|- 设置相邻节点间的位置
|__|--| a,b 表示两个相邻节点,如果a和b的节点相同,则间隔为1,否则为2
*/
var tree = d3.layout.tree()
.size([padding.width, padding.height-200])
.separation(function (a,b) {
return (a.parent == b.parent ? 1:2)
})

/* 对角线投射 */
var diagonal = d3.svg.diagonal()
.projection(function (d) {
return [d.y, d.x]
})

/*
|_ 读取 json 数据
|__|-- nodes 计算得到的节点数组
|__|-- links 计算出连线数组
*/
d3.json("./tree.json", function (error, root) {
var nodes = tree.nodes(root)
var links = tree.links(nodes)

console.log(nodes)
console.log(links)
})

树布图


树图(Tree)是所谓的流程图的基本组成部分,在本文中,主要通过.json数据并通过对角线生成器进行投射,通过使用<path>绘制出节点的连接线,然后分别添加<circle> and <text>的出一个完整的树图。

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
/* 基本 svg 布局 */
var padding = {
width: 800,
height: 800,
}

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

/*
设置 x 的位置为 80
*/
var gTree = svg.append("g")
.attr("transform","translate(80,0)")

/*
|_ 开始布局运算
|__|- 获取长宽高并对高-200
|__|- 设置相邻节点间的位置
|__|--| a,b 表示两个相邻节点,如果a和b的节点相同,则间隔为1,否则为2
*/
var tree = d3.layout.tree()
.size([padding.width, padding.height-200])
.separation(function (a,b) {
return (a.parent == b.parent ? 1:2)
})

/* 对角线投射 */
var diagonal = d3.svg.diagonal()
.projection(function (d) {
return [d.y, d.x]
})

/*
|_ 读取 json 数据
|__|-- nodes 计算得到的节点数组
|__|-- links 计算出连线数组
*/
d3.json("./tree.json", function (error, root) {
var nodes = tree.nodes(root)
var links = tree.links(nodes)

/* 对角线投射绘制 */
var link = gTree.selectAll(".link")
.data(links)
.enter()
.append("path")
.attr("class","link")
.attr("d",diagonal)

/* 为每个点添加足够的 <circle> and <text> 元素 */
var node = gTree.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class","node")
.attr("transform",function (d) {
return "translate(" + d.y + "," + d.x + ")"
})

node.append("circle")
.attr("r",4.5)

node.append("text")
/*
|_ x
|__|-- 以节点为判断目标,如果是叶子节点则在左边(end),否则在右边(start)
|_ y
|__|-- 设置文字的 y 轴 高度使得与圆圈对齐。
*/
.attr("dx",function (d) {
return d.children ? -18 :18
})
.attr("dy",3)
.style("text-anchor", function (d) {
return d.children ? "end":"start"
})
.text(function (d) {
return d.name
})
})


开关


开关是数据图表的一种交互方式,当点击某个元素的时候会出现新的元素,再次点击支护有会被隐藏,这种方式通常会被运用在思维导图中。在下述的code中,主要会分为enter、update、exit,分别表示展开数据、展开后的数据、比和数据等三种表达方式。

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/* 基本 svg 布局 */
var padding = {
width: 1800,
height: 2800,
left: 80,
right: 50,
top: 20,
bottom: 20
}

var svg = d3.select("body")
.append("svg")
.attr("width", padding.width + padding.left + padding.right)
.attr("height", padding.height + padding.top + padding.bottom)
.append("g")
.attr("transform", "translate(" + padding.left + "," + padding.top + ")")


/*
|_ 开始布局运算
|__|- 获取长宽高并对高-200
|__|- 设置相邻节点间的位置
|__|--| a,b 表示两个相邻节点,如果a和b的节点相同,则间隔为1,否则为2
*/
var tree = d3.layout.tree()
.size([padding.width, padding.height-200])
.separation(function (a,b) {
return (a.parent == b.parent ? 1:2)
})

/* 对角线投射 */
var diagonal = d3.svg.diagonal()
.projection(function (d) {
return [d.y, d.x]
})

/*
|_ 读取 json 数据
|__|-- 为第一个节点添加初始坐标 x0/y0
|__|-- nodes 计算得到的节点数组
|__|-- links 计算出连线数组
*/
d3.json("./teamLayout.json", function (error, root) {
root.x0 = padding.height/2
root.y0 = 0;

redraw(root) /* 第一个节点为起始节点重绘和他的重绘函数 */

function redraw(source) {
/*
计算出节点的连线位置,
nodes / links 计算出节点的连线和位置
*/
var nodes = tree.nodes(root)
var links = tree.links(nodes)

// 重新计算点的 y坐标
nodes.forEach(function (d) {
d.y = d.depth * 400;
})

/*
处理节点
获取节点的 update 以及 enter 和 exit 部分
*/
var nodeUpdate = svg.selectAll(".node")
.data(nodes, function (d) {
return d.name
})
var nodeEnter = nodeUpdate.enter()
var nodeExit = nodeUpdate.exit()

/*
节点的 enter 处理方法
*/
var enterNodes = nodeEnter.append("g")
.attr("class","node")
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")"
})
.on("click", function (d) {
toggle(d) // 开关,“d”为节点如果有则保存到 _children,并将此设置为 null,如果有没子节点则将原来的设置为 null
redraw(d) // 计算出节点的连线位置
})
enterNodes.append("circle")
.attr("r", 0)
.style("fill", function (d) {
return d._children ? "none" : "#fff"
})

enterNodes.append("text")
.attr("x", function (d) {
return d.children || d._children ? -19 : 19
})
.attr("dy", ".35em")
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start"
})
.text(function (d) {
return d.name
})
.style("fill-opacity",0)

/*
节点的 update 处理方法
*/
var updateNodes = nodeUpdate.transition()
.duration(1000)
.attr("transform", function (d) {
return " translate(" + d.y + "," + d.x + ")"
})

updateNodes.select("circle")
.style("fill", function (d) {
return d.children ? "lightsteelblue" : "#fff"
})

updateNodes.select("text")
.style("fill-opacity",0)

// Update 处理方法
updateNodes.select("circle")
.attr("r",5)
.style("fill", function (d) {
return d.children ? "while":"#fff"
})
updateNodes.select("text")
.style("fill-opacity",1)

// Exit 处理方法
var exitNodes = nodeExit.transition()
.duration(1000)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")"
})
.remove()
exitNodes.select("circle")
.attr("r",0)

exitNodes.select("text")
.style("fill-opacity",0)

/*
处理连线
获取连线的 update 部分
*/
var linkUpdate = svg.selectAll(".link")
.data(links, function (d) {
return d.target.name
})

/*
连线的 enter 处理方法
*/
var linkEnter = linkUpdate.enter() // 获取连线的 enter 部分

linkEnter.insert("path",".node")
.attr("class","link")
.attr("d", function (d) {
var o = {
x: source.x0,
y: source.y0,
}
return diagonal({
source: o, target: o
})
})
.transition()
.duration(1000)
.attr("d", diagonal)

/*
连线的 update 处理方法
*/
linkUpdate.transition()
.duration(1000)
.attr("d", diagonal)

/*
连线的 exit 处理方法
*/
var linkExit = linkUpdate.exit() // 获取连线的 Exit 部分
linkExit.transition()
.duration(1000)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
/*
将当前节点保存在 x0\y0变量
*/
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y
})
}
/*
开关,”d“ 为节点,
如果有节点,则将节点保存到 _children,并将字节点设置为 null
如果没有子节点,则去会原来的子节点,设置为 null
*/
function toggle(d) {
if (d.children) {
d._children = d.children
d.children = null
} else {
d.children = d._children
d._children = null
}
}
})

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.link {
fill: transparent;
stroke: #e5e5e5;
}
.node circle{
stroke-width: 1px;
stroke: #ec6868;
fill: transparent;
cursor: pointer;
}

.node {
font-weight: bold;
font-size: 12px;
}

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)
}
}

D3 behavior

behavior,即“行为”,他主要包含了d3.behavior.drag以及d3.behavior.zoom来分别实现和处理拖拽和缩放的交互形式。

click


在先介绍 behavior前,我们需要了解到基本的鼠标事件,鼠标事件主要分为6种,主要是辨别鼠标的按下、触点等两种:

ID DA
click 鼠标单击某元素时(被 mousedown(鼠标按下) and mouseup(鼠标松开)组合在一起)
mouseover 当接收到光标触点时 ……
mouseout 当光标从 …… 移出时
mousemove 当鼠标移动时 ……
mousedown 当鼠标 按下 时 ……
mouseup 当鼠标按下松开时 ……

在下述的 code 中我们组要实现了鼠标的按下(mousedown \ click)、鼠标松开(mouseup)、光标接触(mouseover)、光标移出(mouseout)的例子,并配合过渡效果来分别的进行显示,需要值得注意的是click与 mousedown不是完全一样的,可以从下述 code 看出差别。

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

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

svg.append("rect")
.attr("fill","blue")
.attr("x",200)
.attr("y",20)
.attr("width",100)
.attr("height",100)
/*
|- mouseover 收到光标时
|- mouseout 当光标移出时
|- mousedown 当鼠标按下时 / click 当鼠标按下时
|- mouseup 当鼠标松下时
*/
.on("mouseover", function () {
d3.select("rect")
.transition()
.duration(2000)
.attr("fill","red")
})
.on("mouseout", function () {
d3.select("rect")
.transition()
.duration(2000)
.attr("fill","blue")
.attr("width",100)
})
.on("click", function () {
d3.select("rect")
.transition()
.duration(2000)
.attr("width",200)
})
.on("mouseup", function () {
d3.select("rect")
.transition()
.duration(2000)
.attr("width",100)
})

drag


拖拽 (drag) 主要通过使用d3.behavior.drag来进行实现,是指通过鼠标将元素从一个位置到另一位置,通常主要分为按下鼠标、鼠标移动、鼠标松开等三个步骤,D3 为此提供了一种更为简单方法 :

ID DA FA
d3.behavior.drag() 创建一个拖拽
drag.on(type[,listener]) 设置时间监听器,主要支持三种类型,分别为:“dragstart(拖拽开始)、drag(拖拽中)、dragend(拖拽结束)“,而listener是监听函数,默认返回当前指定事件监听器
drag.origin() 设置拖拽起点,可以使得鼠标与被平移元素以相对不变的偏移量进行移动 ,如果设置了起点,那么将会在鼠标按下(mousedown)事件发生时进行调用
on.dragstart 监听 drag 拖动是否开始
on.dragend 监听 dran 拖动是否结束

在下述的 code 中,我们主要通过使用drag来进行移动,当移动的时候将数据cy\cx数据赋值到当前svg图像的x\y轴中,并通过使用dragstartdragend来进行监听drag是否被拖动,从而添加过渡属性

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
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 圆数据与位置 */
var circles = [
{cx: 50, cy: 50, r:20}
]

/*
|_ 起点坐标 origin
|_ 将拖拽时的坐标赋值给 event.x \ event.y
|_ 当拖拽开始时将颜色设置为 red 并将圆弧半径设置为 50 dragstart
|_ 拖拽结束后将颜色与半径设置原来的样子 dragend
*/
var drag = d3.behavior.drag()
.origin(function (d,i) {
return {x: d.cx, y: d.cy}
})
.on("drag", function (d) {
d3.select(this)
.attr("cx", d.cx = d3.event.x)
.attr("cy", d.cy = d3.event.y)
})
.on("dragstart", function (d) {
d3.select("circle")
.transition()
.duration(2000)
.attr("fill","red")
.attr("r",50)
})
.on("dragend", function (d) {
d3.select("circle")
.transition()
.duration(2000)
.attr("fill","blue")
.attr("r",20)
})

/* 绘制圆 */
svg.selectAll("circle")
.data(circles)
.enter()
.append("circle")
.attr("cx", function (d) {
return d.cx
})
.attr("cy", function (d) {
return d.cy
})
.attr("r", function (d) {
return d.r
})
.attr("fill","blue")
.call(drag)

zoom

缩放(zoom)是在数据可视化中较为常用的小细节之一,因此 d3 为我们提供了d3.behavior.zoom方法来用于构建缩放行为。

ID DA FA
d3.behavior.zoom 构建一个缩放行为
zoom(selection) 将此行为应用到选择集中
zoom.translate() 设置缩放的平移量,默认为0,并返回当前的平移向量
zoom.scale() 设置初始放大、缩小量的最大值,默认为0~∞
zoom.center() 设置缩放的中心点,默认为鼠标的位置
zoom.x() 设置一个 x方向比例尺,比例尺会随着放大、缩小改变定义域
zoom.y() 设置一个 y方向比例尺,比例尺会随着放大、缩小改变定义域
zoom.on 设置事件类型,主要分为:”zoomstart(缩放开始)、zoom(正在缩放)、zoomend(缩放结束)“三种


通过on.zoom来计算出缩放的开始并因此得到圆的大小和坐标信息,之后绑定数据进行绘制,在这其中主要通过使用scaleExtent来设置缩放的倍数,其方法主要分为最小值、最大值,下述code中的缩放最大为2倍,最小为1倍,并设置基础的比例尺,通过缩放来观察比例尺的缩放变化

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
/* 基本 svg 结构 */
var padding = {
width: 800,
height: 800
}

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

var circles = [
{cx: 150, cy: 200, r:30}
]

/* 比例尺 */
var x = d3.scale.linear()
.domain([0, padding.width])
.range([0, padding.width])


var y = d3.scale.linear()
.domain([0, padding.height])
.range([0, padding.height])

/* 计算出基本缩放后大小与位置 */
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1,2])
/*
|_ 缩放开始进行颜色过渡 zoomstart
|_ 缩放中计算出位置与大小 zoom
|_ 缩放结束进行控制台输出 zoomend
*/
.on("zoomstart", function () {
d3.select("circle")
.transition()
.duration(2000)
.attr("fill","red")
})
.on("zoom", function (d) {
d3.select(this)
.attr("transform","translate(" + d3.event.translate + ")" + /* 平移位置 */
"scale(" + d3.event.scale + ")") /* 缩放大小 */
console.log("x 的定义域" + x.domain())
console.log("y 的值域" + y.domain())
})
.on("zoomend", function () {
d3.select("circle")
console.log("缩放结束")
})

/* 绑定位置数据到 元素 <g> */
var g = svg.append("g")
.call(zoom)

/* 绘制圆 */
g.selectAll("circle")
.data(circles)
.enter()
.append("circle")
.attr("cx", function (d) {
return d.cx
})
.attr("cy", function (d) {
return d.cy
})
.attr("r", function (d) {
return d.r
})
.attr("fill", "blue")

D3 力布图

力布图,也被称之为力导向图(Force),通过使用布局可以物理模拟的方式将力导向图更加灵活的实现位置的简单控制与计算。而力导向图本身是一种绘图算法,即在二位或三维空间中配置一个节点,节点之间通过直线连接,这种方式被称之为连线,每次连线的长度基本长度相等,需要注意的是这种线会显得尽量不可相交

节点和连线都被施加了力的作用,根据力的作用,来计算出节点和连线的运动轨迹,最终使得让他们呈现一种静止的画面。

ID DA FA
d3.layout.force 创建一个力布图,即力导向图布局
force.size([size]) 设置胡获取力布图的作用范围,size 主要影响里到力布图的重力中心和初始的随机位置,默认为1x1
force.nodes([node]) 设置或获取力导向图的布局节点数组,默认为返回当前节点数组
force.charge([charge]) 指定电荷强度,主要用于决定是排斥还是吸引,如果输入的值是的则导致节点互相吸引,输入负值则会导致节点互相排斥。(需要足以的是,电荷是通过使用Barnes-Hut算法进行实现的)
force.links([links]) 设置或获取力布图的连接数组,默认为返回当前的连接数组
force.linkDistance([distance]) 设置链接点间目标距离的连线长度,默认为20
force.start() 启动模拟,首次创建力布图时必须被调用,然后分配节点和链接,当节点发生变化的时候将会再次被调用,通过此函数可以及那指定从何时开始计算。
force.tick() 进入力布图的仿真步骤,主要通过配合start and stop来计算出静态布局
force.on(type,listener) type是一个事件类型,主要有:start、tick、end三种,而listener则是监听器,主要用于更新节点和链接的显示位置
force.drag() 绑定一个行为允许的交互式托转节点,drag函数主要用于实现拖拽操作

布局运算


当通过下述 code 运行之后,那么数据将会通过start()进行转换,转换后的数据将会发生变化,原有的节点将会得出index、x、y、px、py、fixed、weight等额外属性,这些属性分别意思为索引、x坐标、y坐标、上一时刻的x坐标、上一时刻的y坐标、节点是否固定、节点有多少连线并与之相连

而链接数组将会返回出的数据我们可以的出两个非常重要的点即sourcetarge,分别代表来来源与目标,就如source: 0, target: 2,那么就表述数组one链接2,在这里我们需要注意的是,D3 是从0开始计算的,或者说我不能链接我自己,source: 0, targe: 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
/* 基本 svg 规格 */
var padding = {
width: 800,
height: 800
}

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


/*
定义节点数组以及连线数组
nodes and edges
*/
var nodes = [
{name: "one"},
{name: "two"},
{name: "three"},
{name: "3"},
{name: "4"},
{name: "5"},
{name: "6"}
]

var edges = [
{source: 0, target: 1},
{source: 0, target: 2},
{source: 0, target: 3},
{source: 1, target: 4},
{source: 1, target: 5},
{source: 1, target: 6}
]

/*
| 设置节点数据
|_ nodes 设置节点数组
|_ links 设置连线数组
|_ size 设置大小
|_ linkDistance 设置连线距离
|_ charge 设置电荷数
| 开始计算
|_ start 开始布局计算
*/
var force = d3.layout.force()
.nodes(nodes)
.links(edges)
.size([padding.width,padding.height])
.linkDistance(90)
.charge(-400)

force.start()
console.log(nodes)
console.log(edges)

力布图

绘制力布图

在下述的 code 中我们主要绘制了连接线及其样式、还有的绘制与样式,在这其中,主要通过使用drag来赋予圆可以被拖拽,之后通过使用.on来重新计算出链接点的坐标以及文字的坐标。

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
/* 基本 svg 规格 */
var padding = {
width: 800,
height: 800
}

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


/*
定义节点数组以及连线数组
nodes and edges
*/
var nodes = [
{name: "one"},
{name: "two"},
{name: "three"},
{name: "3"},
{name: "4"},
{name: "5"},
{name: "6"}
]

var edges = [
{source: 0, target: 1},
{source: 0, target: 2},
{source: 0, target: 3},
{source: 1, target: 4},
{source: 1, target: 5},
{source: 1, target: 6}
]

/*
| 设置节点数据
|_ nodes 设置节点数组
|_ links 设置连线数组
|_ size 设置大小
|_ linkDistance 设置连线距离
|_ charge 设置电荷数
| 开始计算
|_ start 开始布局计算
|_ category20c 由D3 所提供的 20种颜色(第三方案)
*/
var force = d3.layout.force()
.nodes(nodes)
.links(edges)
.size([padding.width,padding.height])
.linkDistance(190)
.charge(-900)

force.start()

var color = d3.scale.category20c()

/* 绘制连线 */
var lines = svg.selectAll(".forceLine")
.data(edges)
.enter()
.append("line")
.attr("class","forceLine")

/*
|_ 绘制节点圆圈
|———— 添加 circle
|———— 设置样式为 forceCircle
|———— 设置圆半径为 40r
|———— 允许被拖动
*/
var circles = svg.selectAll(".forceCircle")
.data(nodes)
.enter()
.append("circle")
.attr("r",40)
.style("fill", function (d,i) {
return color(i)
})
.call(force.drag)

/* 绘制文字 */
var texts = svg.selectAll(".forceText")
.data(nodes)
.enter()
.append("text")
.attr("class","forceText")
.attr("x", function (d) {
return d.x
})
.attr("y", function (d) {
return d.y
})
.attr("dy",".3em")
.text(function (d) {
return d.name;
})

/* 鼠标事件监听器 */
force.on("tick", function () {
/* 更新连线端点坐标 */
lines.attr("x1", function (d) {
return d.source.x
})
lines.attr("y1", function (d) {
return d.source.y
})
lines.attr("x2", function (d) {
return d.target.x
})
lines.attr("y2", function (d) {
return d.target.y
})

/* 更新节点坐标 */
circles.attr("cx", function (d) {
return d.x
})
circles.attr("cy", function (d) {
return d.y
})

/* 更新节点的文字坐标 */
texts.attr("x", function (d) {
return d.x
})
texts.attr("y", function (d) {
return d.y
})
})

CSS

1
2
3
4
5
6
7
8
9
10
11
.forceLine {
fill: none;
stroke: #d0d0d0;
stroke-width: 2px;
}

.forceText {
text-anchor: middle;
fill: white;
font-weight: bold;
}

on

如一开始介绍的函数方法一样,force.on(type.listener)是一个监听器可通过使用on进行更新节点以及链接的显示位置,当然也可以设置拖拽后固定对象的位置,可添加:

1
2
3
4
var drag = force.drag()
.on("dragstart", function (d) {
d.fixed = true
})

D3 饼布图

饼状图(Pie Chart)通过使用布局可以方便计算出饼图或圈外的开始以及结束的角度,饼布图还可以用于绘制弧形。当开始绘制的时候布局会将数据数组转换成一个对象数据,在这个对象数据中包含了开始角度以及结束角度,之后传递给弧形生成器。

ID DA FA
d3.layout.pie() 创建一个新的饼图
pie(value[,index]) 转换数据,转换后的每个对象都包含有起始角度终止角度
pie.value([accessor]) 设置或获取值访问器,即从接收的数据中提取初始值
pie.sort([comparator]) 设置或获取比较器,主要用于进行排序
pie.startAngle([angle]) 如果指定angle,那么将会来设置饼布图所有起始弧度,默认为0,单位为弧度
pie.endAngle([angle]) 如果设置angle,那么将会设置饼布图的所有终止弧度,默认为,单位为弧度

布局运用


在下述 code 中,主要定义了基本的饼布图数据,之后通过使用d3.layout.pie创建布局,共过使用值访问将传入的数组提取出需要转换的数据方法,因此所有数据都会被调用一次此方法,通过使用值访问器,那么将会分别返回没一项的对应数据,如[2015,1000],那么通过值访问器将会返回1000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var database = [
[2015,1000],
[2016,2000],
[2017,3000],
[2018,4000],
[2019,5000],
[2020,6000],
[2021,7000]
]
var pie = d3.layout.pie()
.value(function(d) {
return d[1]
})

var pieEcho = pie(database)
console.log(pieEcho)

Start and EndAngle


通过使用布局(layout),可以不需要开发者手动进行计算来根据布局得出结果,在下述 code 中主要通过使用 startAngle以及endAngle来分别指定圆弧的开始以及结束半径,整个过程都无需开发者进行手动计算,直接根据布局运算的出结果。

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
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 饼布图数据 */
var database = [
[2015,1000],
[2016,2000],
[2017,3000],
[2018,4000],
[2019,5000],
[2020,6000],
[2021,7000]
]

/*
/- 值访问器 function
/——— 输出经过布局计算出的值 pie(databases)
/———— 自定义设置圆弧的开始半径以及结束半径
*/
var pie = d3.layout.pie()
.startAngle(Math.PI * 0.2)
.endAngle(Math.PI * 1.5)
.value(function(d) {
return d[1]
})

var pieEcho = pie(database)

/*
/—— 半径设置 arc
/———— 外半径 outerRadius
/———— 内半径 innerRadius(为区分这里设置200,饼状图应该设置为0)
*/
var outerRadius = padding.width / 3
var innerRadius = 0

var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)

/* 设置颜色 */
var color = d3.scale.category20c()

/* 为对应数据添加弧的基本位置 */
var arcs = svg.selectAll("g")
.data(pieEcho) /* 绑定计算后的数据 */
.enter()
.append("g")
.attr("transform",
"translate(" + (padding.width /2) +"," + (padding.height /2) + ")")

/* 添加路径元素 */
arcs.append("path")
.attr("fill",function(d,i) {
return color(i)
})
.attr("d",function (d) {
return arc(d)
})

/*
/—— 文字
/———— 问别定义文文字的xy坐标
/———— 计算出数据的百分比
*/
arcs.append("text")
.attr("transform", function (d) {
var x = arc.centroid(d)[0]
var y = arc.centroid(d)[1]
return "translate(" + x + "," + y + ")"
})
.attr("text-anchor","middle")
.attr("fill","white")
.text(function (d) {
var percent = Number(d.value)/d3.sum(database, function (d) {
return d[1]
}) * 100
return percent.toFixed(1) + "%"
})

/*
外部文字及线条
*/
arcs.append("line")
.attr("stroke","black")
.attr("x1", function (d) {
return arc.centroid(d)[0] * 2.3
})
.attr("y1", function (d) {
return arc.centroid(d)[1] * 2.3
})
.attr("x2", function (d) {
return arc.centroid(d)[0] * 2.2
})
.attr("y2", function (d) {
return arc.centroid(d)[1] * 2.2
})

arcs.append("text")
.attr("transform", function (d) {
var x = arc.centroid(d)[0] * 2.5
var y = arc.centroid(d)[1] * 2.5
return "translate(" + x + "," + y + ")"
})
.attr("text-anchor","middle")
.text(function (d) {
return d.data[0]
})

绘制弧

通常来讲布局与弧生成器来进行绘制的图被称之为饼布图,与弧生成器来直接绘制饼图类似,因此startAngle以及endAngle都将适用与饼布图。

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
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 饼布图数据 */
var database = [
[2015,1000],
[2016,2000],
[2017,3000],
[2018,4000],
[2019,5000],
[2020,6000],
[2021,7000]
]

/*
/- 值访问器 function
/——— 输出经过布局计算出的值 pie(databases)
*/
var pie = d3.layout.pie()
.value(function(d) {
return d[1]
})

var pieEcho = pie(database)

/*
/—— 半径设置 arc
/———— 外半径 outerRadius
/———— 内半径 innerRadius(为区分这里设置200,饼状图应该设置为0)
*/
var outerRadius = padding.width / 3
var innerRadius = 200

var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)

/* 设置颜色 */
var color = d3.scale.category20c()

/* 为对应数据添加弧的基本位置 */
var arcs = svg.selectAll("g")
.data(pieEcho) /* 绑定计算后的数据 */
.enter()
.append("g")
.attr("transform",
"translate(" + (padding.width /2) +"," + (padding.height /2) + ")")

/* 添加路径元素 */
arcs.append("path")
.attr("fill",function(d,i) {
return color(i)
})
.attr("d",function (d) {
return arc(d)
})

内部文字


在下述的 code 中,我们通过使用 arc.centroid来计算出弧的中心坐标,之后在通过计算数据的百分比最终得出一个完整的饼图内部数据。

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
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 饼布图数据 */
var database = [
[2015,1000],
[2016,2000],
[2017,3000],
[2018,4000],
[2019,5000],
[2020,6000],
[2021,7000]
]

/*
/- 值访问器 function
/——— 输出经过布局计算出的值 pie(databases)
*/
var pie = d3.layout.pie()
.value(function(d) {
return d[1]
})

var pieEcho = pie(database)

/*
/—— 半径设置 arc
/———— 外半径 outerRadius
/———— 内半径 innerRadius(为区分这里设置200,饼状图应该设置为0)
*/
var outerRadius = padding.width / 3
var innerRadius = 200

var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)

/* 设置颜色 */
var color = d3.scale.category20c()

/* 为对应数据添加弧的基本位置 */
var arcs = svg.selectAll("g")
.data(pieEcho) /* 绑定计算后的数据 */
.enter()
.append("g")
.attr("transform",
"translate(" + (padding.width /2) +"," + (padding.height /2) + ")")

/* 添加路径元素 */
arcs.append("path")
.attr("fill",function(d,i) {
return color(i)
})
.attr("d",function (d) {
return arc(d)
})

/* 内部文字 */
arcs.append("text")
.attr("transform", function (d) {
var x = arc.centroid(d)[0]
var y = arc.centroid(d)[1]
return "translate(" + x + "," + y + ")"
})
.attr("text-anchor","middle")
.attr("fill","white")
.text(function (d) {
var percent = Number(d.value)/d3.sum(database, function (d) {
return d[1]
}) * 100
return percent.toFixed(1) + "%"
})

外部文字


外部文字依然可以通过使用arc.centroid来直接根据圆弧从中心点进行计算,从而分别出的线以及文字的路径和x,y轴的坐标。

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
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 饼布图数据 */
var database = [
[2015,1000],
[2016,2000],
[2017,3000],
[2018,4000],
[2019,5000],
[2020,6000],
[2021,7000]
]

/*
/- 值访问器 function
/——— 输出经过布局计算出的值 pie(databases)
*/
var pie = d3.layout.pie()
.value(function(d) {
return d[1]
})

var pieEcho = pie(database)

/*
/—— 半径设置 arc
/———— 外半径 outerRadius
/———— 内半径 innerRadius(为区分这里设置200,饼状图应该设置为0)
*/
var outerRadius = padding.width / 3
var innerRadius = 0

var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)

/* 设置颜色 */
var color = d3.scale.category20c()

/* 为对应数据添加弧的基本位置 */
var arcs = svg.selectAll("g")
.data(pieEcho) /* 绑定计算后的数据 */
.enter()
.append("g")
.attr("transform",
"translate(" + (padding.width /2) +"," + (padding.height /2) + ")")

/* 添加路径元素 */
arcs.append("path")
.attr("fill",function(d,i) {
return color(i)
})
.attr("d",function (d) {
return arc(d)
})

/*
/—— 文字
/———— 问别定义文文字的xy坐标
/———— 计算出数据的百分比
*/
arcs.append("text")
.attr("transform", function (d) {
var x = arc.centroid(d)[0]
var y = arc.centroid(d)[1]
return "translate(" + x + "," + y + ")"
})
.attr("text-anchor","middle")
.attr("fill","white")
.text(function (d) {
var percent = Number(d.value)/d3.sum(database, function (d) {
return d[1]
}) * 100
return percent.toFixed(1) + "%"
})

/*
外部文字
*/
arcs.append("line")
.attr("stroke","black")
.attr("x1", function (d) {
return arc.centroid(d)[0] * 2.3
})
.attr("y1", function (d) {
return arc.centroid(d)[1] * 2.3
})
.attr("x2", function (d) {
return arc.centroid(d)[0] * 2.2
})
.attr("y2", function (d) {
return arc.centroid(d)[1] * 2.2
})

arcs.append("text")
.attr("transform", function (d) {
var x = arc.centroid(d)[0] * 2.5
var y = arc.centroid(d)[1] * 2.5
return "translate(" + x + "," + y + ")"
})
.attr("text-anchor","middle")
.text(function (d) {
return d.data[0]
})

增加交互


在下述的 code 中,我们主要通过getPixelLength以及分别定义两个连接弧文字直线,将直线变得斜上一点,之后通过使用d3所提供的behavior增加鼠标的交互。

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 饼布图数据 */
var database = [
["2015",1000],
["2016",2000],
["2017",3000],
["2018",4000],
["2019",5000],
["2020",6000],
["2021",7000]
]

/*
/- 值访问器 function
/——— 输出经过布局计算出的值 pie(databases)
*/
var pie = d3.layout.pie()
.value(function(d) {
return d[1]
})

var pieEcho = pie(database)

/*
/—— 半径设置 arc
/———— 外半径 outerRadius
/———— 内半径 innerRadius(为区分这里设置200,饼状图应该设置为0)
*/
var outerRadius = padding.width / 3
var innerRadius = 0

var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)

/* 设置颜色 */
var color = d3.scale.category20c()

/* 为对应数据添加弧的基本位置 */
var arcs = svg.selectAll("g")
.data(pieEcho) /* 绑定计算后的数据 */
.enter()
.append("g")
.attr("transform",
"translate(" + (padding.width /2) +"," + (padding.height /2) + ")")

/* 添加路径元素 */
arcs.append("path")
.attr("fill",function(d,i) {
return color(i)
})
.attr("d",function (d) {
return arc(d)
})

/*
/—— 文字
/———— 问别定义文文字的xy坐标
/———— 计算出数据的百分比
*/
arcs.append("text")
.attr("transform", function (d) {
var x = arc.centroid(d)[0]
var y = arc.centroid(d)[1]
return "translate(" + x + "," + y + ")"
})
.attr("text-anchor","middle")
.attr("fill","white")
.text(function (d) {
var percent = Number(d.value)/d3.sum(database, function (d) {
return d[1]
}) * 100
return percent.toFixed(1) + "%"
})


/*
外部文字
*/
var fontsize = 14

arcs.append("line")
.attr("stroke","black")
.attr("x1", function (d) {
return arc.centroid(d)[0] * 2
})
.attr("y1", function (d) {
return arc.centroid(d)[1] * 2
})
.attr("x2", function (d) {
return arc.centroid(d)[0] * 2.2
})
.attr("y2", function (d) {
return arc.centroid(d)[1] * 2.2
})

/*
分别定义两个连接弧文字直线
*/
arcs.append("line")
.style("stroke","black")
.each(function(d){
d.textLine = {x1:0, y1:0, x2:0, y2:0};
})
.attr("x1",function(d){
d.textLine.x1 = arc.centroid(d)[0] * 2.2;
return d.textLine.x1;
})
.attr("y1",function(d){
d.textLine.y1 = arc.centroid(d)[1] * 2.2;
return d.textLine.y1;
})
.attr("x2",function(d){
var strLen = getPixelLength(d.data[0],fontsize) * 1.5;
var bx = arc.centroid(d)[0] * 2.2;
d.textLine.x2 = bx >= 0 ? bx + strLen : bx - strLen;
return d.textLine.x2;
})
.attr("y2",function(d){
d.textLine.y2 = arc.centroid(d)[1] * 2.2;
return d.textLine.y2;
});

arcs.append("text")
.attr("transform", function (d) {
var x = 0
var y = 0
x = (d.textLine.x1 + d.textLine.x2) /2
y = d.textLine.y1
y = y > 0 ? y + fontsize * 1.1 : y - fontsize * 0.4
return "translate(" + x + "," + y + ")"
})
.style("text-anchor","middle")
.style("font-size",fontsize)
.text(function (d) {
return d.data[0]
})

/*
添加提示
默认的情况下透明度为 0,
当鼠标经过的时候将透明度设置为 0.9
移出时设在将透明度设置为0
当鼠标移动的时候,计算出当前所在位置
*/
var givecuo = d3.select("body")
.append("div")
.attr("class","style")
.style("opacity",0.0)

arcs.on("mouseover", function (d) {
givecuo.html(d.data[0] + "当前数据为" + "<br/>" + d.data[1] + "%")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY + 20) + "px")
.style("opacity",0.9)
})
.on("mousemove", function (d) {
givecuo.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY + 20) + "px")
})
.on("mouseout", function (d) {
givecuo.style("opacity",0.0)
})
/*
将直线稍微斜一点
*/
function getPixelLength(str,fontsize) {
var curLen = 0;
for (var i = 0;i<str.length;i++) {
var code = str.charCodeAt(i)
var pixelLen = code > 255 ? fontsize : fontsize / 2
curLen += pixelLen
}
return curLen
}

css

1
2
3
4
5
6
7
8
9
.style {
font-family: simsun;
font-size: 14px;
width: 120px;
height: auto;
position: absolute;
text-align: center;
background-color: white;
}

D3 布局

布局(layout)是 D3 中最为重要的概念之一,其中他的作用就是进行一个数据的转换,以及进行分别进行计算图形的位置和形状的关系,他总共提供了饼布局、力布局、弦布局、树布局、捆布局、直方布局、矩阵树布局七种常用布局

ID DA
饼布图(Pie) 饼布图主要用于方便计算出饼图或圈内的弧线开始和结束的角度,如果没有 Layout,那通过 弧生成器也可以实现,但通过饼布图会更加的方便且更加智能
力布图 (Force) 通过物理模拟的方式将力导向图更加灵活的实现位置的简单控制
弦布图(Chord) 弦图所示的内容为一组实体或数据之间的关系,并通过使用不同的弧线之间画出二次被塞尔曲线,将关系表述在一张弦图中
树布图(Tree) 树布图主要通过使用莱因戈尔德-蒂尔福德算法来产生一个简洁的树状节点,来展示层级结构
捆布图(Bundle) 通过使用丹尼-霍尔登分层捆图的实现,将每个输入的连接来计算出一条路径,然后路径分别被不同的路径穿过,也就是说捆布图的主要作用就是计算链接路径
直方布局(Histogram) 直方布图主要用于展示数据分布
矩形树布局(Treemap) 矩形树布局由Ben Shneiderman在1991年提出,将一块矩形区域进行切割多次,来达到层级的效果

还有一种在 D3 极为不同的布局就是 层次布局,层次布局也被称之为分层布局,他是一种抽象的布局,不可以直接进行使用,但是他允许在不同的分层布局中共享节点,以他为中心分出了:

ID DA
Cluster 簇实体构成的树状图
Pack 使用递归圆填充法来创建一个分层布局
Partition 将节点树递归划分成分辐射状或环状
Tree 将节点树整体放置
Treemap 使用递归空间划分显示节点树(虽然他不是分层布局,但捆绑布局可以同分层布局共同使用

D3 transition

过渡(excessive),主要用于进行视觉效果的交互,让原有生动的数据变得更加活跃,通常在前端图标中较为应用,而其学术图标仅针对静态数据的展示效果。

transition

在 d3 中创建过度主要需要通过d3.transition以及 selection.transition,他们都可以返回一个过度的效果,他们两个的区别在于d3.transtion主要用于直接创建一个对象,然后在指定一个作用对象,而selection.transtion通过作用对象创建过渡,简单的就是一个是选择集对象,而另一个就是过度对象

ID DA FA
d3.transition([选择],[名称]) 创建一个过度对象
transition.delay([延迟]) 设置一个过度的延迟时间,单位是ms (默认的延迟时间为 0ms)
transtion.duration([持续时间]) 设置过度的持续时间,单位是ms (默认的持续时间为250ms
transtion.ease() 创建一个过度的样式
linear 线性缓动
polynomial 多项式缓动
quadratic 二次缓动
cubic 三次缓动
sinusoidal 正弦缓动
exponential 指数缓动
back 回缩后冲击终点
circular 环形缓动
circular 缓动效果
elastic 震荡缓动,就像是类似于震荡的橡皮筋一样
bounce 弹跳缓动,像是一个橡皮球一样

创建

d3.transition

在下述的 code 中我们主要使用d3.transtion方法来进行添加一个过渡效果,通过使用过渡效果可以将宽度为100的矩形过渡为宽度500且效果非常的丝滑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var padding = {
width: 800,
height: 800
}

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


svg.append("rect")
.attr("fill","blue")
.attr("x",10)
.attr("y",10)
.attr("width",100)
.attr("height",30)
.transition()
.attr("width",500)

selection.transition


在上图中,我们通过使用selection.transtion选择集对象来实现过渡的delay、duration、ease即延迟、持续时间以及震荡过渡样式等效果的实现,需要注意的是 transition可以被多次的调用

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

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


var rect = svg.append("rect")
.attr("fill","blue")
.attr("x",10)
.attr("y",10)
.attr("width",100)
.attr("height",30)

/*
/- 选择了 rect 函数,从而创建一个过度对象
/-- 设置延迟时间 @delay 为 1000,即 1s
/-- 设置持续时间 @duration 为 10s
/-- 指定过度样式 @ease 为 elastic即震荡缓动
*/
var rectTran = rect.transition()
.delay(1000)
.duration(10000)
.ease("elastic")
.attr("width",500)

属性


通常为了满足部分开发者的变态心里,D3 为此提供了图表的过度属性,主要有两个,分别我为transition.attr以及transition.attrTween两个过度效果,通常transition.attr最为常用,而transition.attrTween则是可以手动设置插值函数,虽然他属性变化的中间值是默认插值函数计算,但通过使用transition.attrTween则可以手动设置插值函数:

ID DA FA
transition.attr(name,value) 将属性name过度到目标值
transition.attrTween(name,tween) name使用插值函数tween进行过度
linear-in 按照正方向运动
linear-out 按照反向运动
linear-in-out 前半段用正方向运动,后半段有那个反方向运动
linear-out-in 前半段用反方向运动,后半段用正方向运动 (除了linear过渡样式可用之外,也可以用于其他的过渡样式)

transition.attr

在下述的 code 中,我们通过使用selection.transtion选择集对象来实现过渡的delay、duration、ease即延迟、持续时间以及震荡过渡样式等效果的实现。

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

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


var rect = svg.append("rect")
.attr("fill","blue")
.attr("x",10)
.attr("y",10)
.attr("width",100)
.attr("height",30)

/*
/- 选择了 rect 函数,从而创建一个过度对象
/-- 设置延迟时间 @delay 为 1000,即 1s
/-- 设置持续时间 @duration 为 10s
/-- 指定过度样式 @ease 为 elastic即震荡缓动
*/
var rectTran = rect.transition()
.delay(1000)
.duration(10000)
.ease("elastic")
.attr("width",500)

transition.attrTween

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
/* 基本 svg 数据 */
var padding = {
width: 900,
height: 900
}

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

/* 矩形与函数过度 */
var rect = svg.append("rect")
.attr("fill","blue")
.attr("x",10)
.attr("y",10)
.attr("width",100)
.attr("height",30)

var rectTran = rect.transition()
.duration(1500)
.attrTween("width",
function(d, i, a) {
return function(t) {
return Number(a) + t * 500;
}
})
.ease("elastic-in-out")
.attr("fill","red")

/* 文字与文字过度 */
var text = svg.append("text")
.attr("fill","blue")
.attr("x",150)
.attr("y",10)
.attr("dy","1.2em")
.attr("text-anchor","end")
.text(100)

var initx = text.attr("x")
var initText = text.text()

var textTran = text.transition()
.duration(1500)
.tween("text", function() {
return function(t) {
d3.select(this)
.attr("x",Number(initx) + t * 500)
.text(Math.floor(Number(initText) + t * 500))
}
})
.ease("elastic-in-out")
.attr("fill","red")

在上述 code 中 :

1
2
3
4
5
6
.attrTween("width",
function(d, i, a) {
return function(t) {
return Number(a) + t * 500;
}
})

其中我们使用了attrTween来手动设置中间值默认的插值函数,function (d,i,a分别代表数据、索引、属性等三种信息,其中awidth是一个初始值。而return function(t)所返回的t则是插值函数,他的范围是0,1,分别代表了开始、结束,所以当t处于0时,function(t)返回100,但是当t返回1时,function(t)将会返回500来表述过渡结束。

axis

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
/* 基本 svg 样式 */
var padding = {
width: 800,
height: 800
}

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

/* 线性比例尺 */
var xScale = d3.scale.linear()
.domain([0,100])
.range([0,500])

/* 坐标轴 */
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")

/* 添加方法 g 绑定 xAxis 坐标轴 */
var g = svg.append("g")
.attr("class","axis")
.attr("transform","translate(50,400)")
.call(xAxis)

/* 添加过渡 */
xScale.domain([0,10])
g.transition()
.duration(2000)
.call(xAxis)
📖 more posts 📖