这是搬运文章,请查看原文(原文会持续更新)datavis-note.benbinbin.com/article/d3/…
系列文章可以查看《数据可视化》专栏
参考
D3 开发者创建了一个在线开发环境 Observable,除了可以查看官方的教程和样例,它也是一个可视化作品分享社区,在里面可以找到很多优秀的可视化作品。
本文介绍 D3.js 基于数据操作 DOM 的基本流程,涉及几个核心概念:
- 选择元素:选择需要操作的 DOM 节点(一般是 SVG 元素)
- 绑定数据:将数据与 DOM 节点相关联,这样就实现了数据驱动 DOM 元素的属性样式更新
- 增删元素:使用 join 操作(或使用 enter-update-exit 操作)增删 DOM 节点
💡 这是 D3 将数据映射为页面元素的最基本流程,主要使用 selection 选择模块,除此之外 D3 还提供其他模块用以实现更复杂的数据可视化效果。
选择器
为了基于数据「驱动」DOM,首先要选中所需操作的元素。JS 原生提供的操作 DOM 的方法代码十分冗长。D3 提供了一种声明式的方法 d3.select(query) 或 d3.selectAll(query) 来选择 DOM 节点或节点集合。
方法 d3.select(query) 选中符合条件的第一个 DOM 元素,而方法 selectAll(query) 则选择所有符合条件的 DOM 元素。如果没有元素被选中则返回空的选择集。
该方法接收的入参 query 是 CSS 所支持的选择器,如标签选择器、类选择器、ID 选择器、属性值选择器等。
该方法返回的选择集对象(或数组)具有丰富的方法,如设置样式属性,更改 HTML 或 文本内容,注册事件监听器,添加、移除、排序节点等,这样就可以通过链式调用的方式操作 DOM。
// 操作单个节点
d3.select('body')
.style('background-color', 'black'); // 修改 <body> 元素的背景色
// 也可以操作节点集合
d3.selectAll('p')
.style('color', 'white'); // 修改 <p> 元素的字体颜色
第二个操作等价的原生操作
const paragraphs = document.getElementByTagName('p');
for (let i=0; i<paragraphs.length; i++) {
const paragraph = paragraph.item(i);
paragraph.style.setProperty('color', 'white');
}
💡 在 D3 中大部分情况下,属性值的值设置除了支持传递静态常量值,还支持传递函数,返回动态计算得到的值,例如 D3 的图形模块 shape 提供了相关函数,基于数据计算出 <path> 元素的属性 d 的值,用于绘制折线图。
// 为每个段落设置随机颜色
d3.selectAll('p').style('color', function() {
return 'hsl(' + Math.random() * 360 + ', 100%, 50%)';
}
数据绑定
由于 D3 在设置 DOM 元素的属性时,支持通过函数计算动态值,如果将图表数据作为参数,就可以实现数据「驱动」元素 Data-Driven Documents
在 D3 中提供方法 data() 可将数据与元素进行绑定,默认根据索引 join-by-index 将 DOM 节点(选择集)和数据(以数组的形式列出)一一对应(即数据数组的第一个元素与选择集的第一个节点绑定),然后在使用函数设置 DOM 节点样式属性时,分别将对应的数据作为第一个参数 d 传递到设置函数中,动态求出属性值,这样就实现了数据驱动文档 Data-Driven Documents。
// 根据数据为段落设置不同的字体大小
d3.selectAll('p') // 选择所有 <p> 元素
.data([4, 8, 15, 16, 23, 42]) // 将数据与 DOM 元素绑定
.style('font-size', function(d) { return d + 'px'; }); // 根据数据动态计算出每一个 <p> 元素的字体大小
💡 当 DOM 节点和数据绑定后,后续如果通过 D3 操作该 DOM 节点时(如更新 DOM 属性样式),D3 可以从选择集中再读取绑定的数据,即 DOM 节点一旦绑定了数据就会带有状态,选择集添加一个名为 __data__ 的属性,这样就不需要不断地进行数据的映射。
为了跟踪 DOM 节点,便于将它们分配到不同的选择集,绑定数据时一般还会提供一个 key 函数 selection.data(data, keyFunction) ,其返回值一般是字符串,如地名、id 等,作为 DOM 节点和数据的匹配依据(而不用默认的按照索引顺序 join-by-index 进行配对)
<div id="Ben"></div>
<div id="Tom"></div>
<div id="Jack"></div>
<div id="shouldBeDeletedNode"></div>
<script>
const dataset = [
{name: 'Ben', number: 4},
{name: 'Tom', number: 8},
{name: 'Jack', number: 15},
];
d3.selectAll('div')
.data(dataset, function(d) {
// key 函数返回数据的 d.name 作为标识,如果节点没有被绑定数据就返回节点的 id 属性作为标识
return d ? d.name : this.id;
})
.text(function(d) { return d.number; });
</script>
增删元素
一般情况下在 D3 中将节点与数据绑定时,选择集合中的节点和数据数组的元素一一匹配,但可能会出现节点和数据元素个数不匹配的问题。
针对这个问题,D3 提出 3 个概念:
- 如果 DOM 节点多出来,则未绑定数据的节点会进入名为 exiting 选择集(准备从页面「离去」的节点,一般在后续操作中删除)
- 如果数据元素多出来了,则对应多出来的占位节点(虚拟节点)会进入名为 entering 选择集(准备「进入」页面的节点,一般会在后续操作中实例化这些 DOM 节点,并插入在页面的相应位置)
- 可与数据对应上的 DOM 节点,进入名为 updating 选择集,它是默认选择集,即
data()方法返回的对象就是 update 选择集(而 enter 选择集和 exit 选择集需要调用该对象的enter()和exit()方法才能获得)
💡 在绑定数据后,D3 没有立即更新(增删)页面节点,而是生成 3 个选择集,这样为数据可视化提供了更大的灵活度和可定制性,例如对于 exiting 选择集的节点,可以在删除时设置一些淡出的动效;对于 entering 选择集的节点可以设置不一样的颜色,高亮出来它们是新增到页面上的
旧方法 enter-update-exit
然后对不同的选择集采用不同的操作,这样就可以根据数据元素动态增删 DOM 元素,流程一般如下(以下 selection 表示选择集):
- 移除 exiting 选择集中对应的页面上的 DOM 节点
selection.exit().remove() - 添加 entering 选择集中的虚拟节点到页面上
enter = selection.enter().append('tagName') - 合并 entering 和 updating 选择集(updating 选择集就在原来的
selection中),这样返回的选择集与新数据一一对应enter.merge(selection),之后可以方便地对新数据对应的 DOM 节点进行统一的样式设置
const circle = d3.selectAll('circle').data(anotherDataset)
.style('fill', 'blue'); // 该样式设置只会对 updating 选择集生效
circle.exit().remove(); // 移除 exiting 选择集中对应的 DOM 节点
circle = circle.enter()
.append('circle') // 添加 entering 选择集中对应的虚拟节点到页面
.style('fill', 'green') // 设置 entering 选择集中的节点样式
.merge(circle) // 合并 entering 和 updating 选择集
.style('stroke', 'black'); // 设置合并后选择集中的节点样式
💡 一般会在使用方法 append() 添加完节点之后,将 entering 选择集与 updating 选择集进行合并 merged,这样后续操作就可以同时应用到 enter 选择集与 update 选择集(这是旧方法,请参照下面更简洁的新方法)
新方法 join
D3 新增了一种方法 join(),它将自动对 exit 选择集中的 DOM 节点进行删除,并自动为页面添加 entering 选择集中的虚拟节点,再将 entering 选择集和 updating 选择集合并返回
d3..selectAll('circle')
.data(newDataset)
.join('circle') // 返回 entering 和 updating 选择集的合并集
// 然后可以对绑定了新数据的 DOM 节点进行整体样式设置
.attr('r', radius)
💡 如果希望对 exiting 选择集、entering 选择集或 updating 选择集分别进行操作,可以在方法 join 中依此传递相应的函数
d3.selectAll('circle')
.data(newData, d => d)
.join(
// 第一个传递的函数入参是 entering 选择集
enter => {
// entering selection handler
// 最后需要返回 entering 选择集实例化的节点,以便 join 方法最后将它以 updating 选择集进行合并
return enter.append('circle')
},
// 第二个传递的函数入参是 updating 选择集
update => {
// updating selection handler
update.attr("fill", "blue")
}
exit => {
// exiting selection handler
exit.remove() // 将 exiting 选择集对应的节点从页面删除
}
)
// 最后 join() 返回 entering 和 updating 的选择集
// 继续进行其他链式调用......