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

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;
}
⬅️ Go back