d3 是基于数据绑定的思想,选择器选择元素并与数据绑定到一起,根据数据的内容给元素添加不同的属性,就可以实现各种可视化效果。但由于元素的数量和数据未必一致,因此在绑定的时候 d3 会根据数量的差异执行不同的处理:

  • 元素与数据一一对应:执行 update 部分,表示已经存在的元素,用于更新
  • 元素少于数据:执行 enter 部分,表示即将进入的元素,用于添加
  • 元素多于数据:执行 exit 部分,表示即将退出的元素,用于删除

data()

在执行不同的方法之前,需要先将数据绑定到选择的 DOM 元素上,这样就可以针对这些数据进行相关操作,比如设置属性等。

1
d3.select("svg").selectAll("circle").data([1, 2, 3]);

data 方法的作用是给对应的 DOM 元素添加一个__data__属性,可以通过document.getElementsByTagName("p")[0].__data__看到

update()

给定的数组中的个数和 DOM 元素的数量相等则进行 update 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<svg>
<circle>1</circle>
<circle>2</circle>
<circle>3</circle>
</svg>
<script>
d3.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "green")
.attr("fill", "none");
</script>

给定的 svg 中有 3 个 circle 元素,正好和 data 数据的数量相等,因此这三个元素都会被赋予cxcyr值,并被描边画成绿色

enter()

当 DOM 元素少于 data 的数量时,甚至一个元素都不存在的时候,通过 enter 方法就可以让程序帮忙创建。在执行 enter 方法之后,需要通过 append 方法指定多余的数据需要创建什么元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<svg>
<circle>1</circle>
<circle>2</circle>
</svg>
<script>
d3.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "green")
.attr("fill", "none")
.enter()
.append("circle")
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "red")
.attr("fill", "none");
</script>

给定 2 个元素,但是 data 有 3 个数据,因此会同时执行 updateenter 操作,最终的结果就是 2 个绿色的圆圈和 1 个红色圆圈。

这时候,如果将 enter 之前的操作部分去掉,那么 update 时则不会执行任何操作,页面上将只有一个红色的圆圈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<svg>
<circle>1</circle>
<circle>2</circle>
</svg>
<script>
d3.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.enter()
.append("circle")
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "red")
.attr("fill", "none");
</script>

为了区分 update 和 enter 的操作,建议将这两个部分分开编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<svg>
<circle>1</circle>
<circle>2</circle>
</svg>
<script>
const update = d3
.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "green")
.attr("fill", "none");

update
.enter()
.append("circle")
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "red")
.attr("fill", "none");
</script>

exit()

exit 的作用是在给定的数组元素个数中小于 DOM 个数时,多出来的元素所执行的操作。和 enter 方法不同的是,因为当前元素已经创建了,因此不需要额外的 append 方法来指定创建的元素:

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
<svg>
<circle>1</circle>
<circle>2</circle>
<circle>3</circle>
<circle>4</circle>
</svg>
<script>
const update = d3
.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "green")
.attr("fill", "none");

update
.enter()
.append("circle")
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "red")
.attr("fill", "none");

update
.exit()
.attr("cx", 10)
.attr("cy", (d, index) => 4 * 10)
.attr("r", 5)
.attr("stroke", "blue")
.attr("fill", "none");
</script>

svg 中的 circle 有 4 个,但是 data 数据只有 3 个,因此不会执行 enter 之后的方法,会执行 exit 之后的操作,需要注意的是,由于多余的元素没有对应的数据绑定,这时候使用 attr 方法赋予属性时第二个参数如果是一个函数,那么函数的默认参数将是 undefined

join()

在 v5 之后的 d3.js 版本,data 绑定数据之后增加 join 方法来同时执行多个操作,效果类似于原来的 merge 方法,但是 merge 方法已经在新版的 d3.js 中不再支持。比如上面 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
<svg>
<circle>1</circle>
<circle>2</circle>
<circle>3</circle>
<circle>4</circle>
</svg>
<script>
const update = d3
.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.join(
(enter) =>
enter
.append("circle")
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "red")
.attr("fill", "none"),

(update) =>
update
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "green")
.attr("fill", "none"),

(exit) =>
exit
.attr("cx", 10)
.attr("cy", (d, index) => 4 * 10)
.attr("r", 5)
.attr("stroke", "blue")
.attr("fill", "none")
);
</script>

join 函数可以接受三个函数作为参数,第一个函数为 enter 函数,第二个为 update,第三个为 exit 函数。同时 join 接受一个字符串作为参数,表示后面元素数量不够时增加的元素,后续继续进行赋值操作,这样最终的结果是页面元素数量和 data 数量相同,如果元素数量不足,则补充相同数量的元素,如果元素数量多了,则删除多余的元素,实现remove()方法的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<svg>
<circle>1</circle>
<circle>2</circle>
<circle>3</circle>
<circle>4</circle>
</svg>
<script>
const update = d3
.select("svg")
.selectAll("circle")
.data([1, 2, 3])
.join("circle")
.attr("cx", 10)
.attr("cy", (d) => d * 10)
.attr("r", 5)
.attr("stroke", "green")
.attr("fill", "none");
</script>