选择集与数据绑定
选择集(Selections)和数据绑定(Data Binding)是 D3.js 最核心的概念,也是它区别于其他可视化库的关键所在。只要掌握了这两个概念,你就已经理解了 D3.js 的大半精髓。
如果把创建可视化比作「根据蓝图施工」,那么选择集相当于「定位需要施工的场地」,而数据绑定则是「把具体的建筑材料分发给各个场地」。整个过程就是:选择 DOM 元素、绑定数据、根据数据进行 DOM 操作。
选择元素
D3 的所有操作都从选择 DOM 元素开始。选择集本质上是对一个或多个 DOM 元素的封装,通过它可以对这些元素进行属性设置、样式调整、内容修改等操作。
select 和 selectAll
D3 提供了两个基础的选择函数:
// 选择单个元素,返回第一个匹配的元素
d3.select('body') // 选择 body 元素
d3.select('#header') // 选择 id="header" 的元素
d3.select('.container') // 选择第一个 class="container" 的元素
// 选择所有匹配的元素,返回所有元素的集合
d3.selectAll('p') // 选择所有段落
d3.selectAll('.item') // 选择所有 class="item" 的元素
d3.selectAll('li a') // 选择所有 li 内部的链接
这两个函数的差异在于返回的元素数量:select 总是返回包含单个元素(或 null)的选择集;selectAll 返回包含零个、一个或多个元素的选择集。
支持的选择器语法
D3 的选择器兼容所有 CSS 选择器语法:
// 标签选择器
d3.selectAll('div')
d3.selectAll('svg')
// ID 选择器
d3.select('#main-chart')
// 类选择器
d3.selectAll('.axis')
d3.selectAll('.bar-chart .bar')
// 属性选择器
d3.selectAll('[data-type="primary"]')
d3.selectAll('[disabled]')
// 组合选择器
d3.selectAll('div svg') // div 内部的 svg
d3.selectAll('li > a') // li 直接子代的 a
d3.selectAll('ul li:first-child') // 每个 ul 中的第一个 li
链式调用
D3 的方法通常返回选择集本身,这使得可以链式调用多个方法:
d3.select('#my-div')
.attr('class', 'highlight') // 设置属性
.style('background-color', '#f0f0f0') // 设置样式
.text('Hello D3') // 设置文本内容
.on('click', handleClick); // 添加事件监听
这种链式语法是 D3 代码简洁优雅的关键。一个精心组织的 D3 代码看起来像一段流畅的操作指令,从选择元素开始,依次进行各种变换。
理解选择集
选择集不仅仅是一个 DOM 元素的引用,它还包含了一些重要的内部状态。
选择集的结构
当调用 d3.selectAll('p') 时,返回的选择集包含以下信息:
- _groups:存放选中元素的数组,每个组是一个 DOM 元素数组
- _parents:父级元素的引用
不过在日常使用中,你不需要直接操作这些内部属性。D3 提供了丰富的方法来与选择集交互。
选择集的方法分类
选择集的方法可以分为几大类:
属性操作:attr()、property()、style()、text()、html()、 classed()
元素操作:append()、insert()、remove()、clone()
数据操作:datum()、data()、enter()、exit()、join()
事件操作:on()
尺寸位置:node()、size()
接下来重点讲解数据绑定相关的操作,这是 D3 最强大的特性。
数据绑定基础
数据绑定将数据数组中的每个元素与 DOM 元素关联起来。当数据进行变化时,DOM 可以自动更新以反映新数据。
data() 方法
data() 是将数据与选中元素绑定的核心方法:
// 假设 HTML 中有 3 个段落
// <p></p><p></p><p></p>
const data = ['苹果', '香蕉', '橙子'];
d3.selectAll('p')
.data(data)
.text(d => d);
执行过程是这样的:data() 方法遍历选中的 p 元素(3 个),并遍历 data 数组(3 个)。它按位置索引一一对应:将"苹果"绑定到第一个 p,将"香蕉"绑定到第二个 p,将"橙子"绑定到第三个 p。然后 text() 方法为每个段落设置文本显示绑定的数据。
data() 还支持第二个参数作为键函数,用于更精确的数据匹配:
const data = [
{id: 'a', value: 10},
{id: 'b', value: 20},
{id: 'c', value: 30}
];
// HTML 中有 id="b", id="a", id="c" 的元素
d3.selectAll('.item')
.data(data, d => d.id) // 使用 id 作为键进行匹配
.text(d => d.value);
通过键函数,D3 就能准确地将数据元素与 DOM 元素对应起来,即使它们的顺序不同。
datum() 方法
如果要绑定单个数据对象而不是数组,使用 datum():
// 将单个数据对象绑定到一个元素
d3.select('#chart')
.datum({title: '销售额趋势', max: 1000})
.append('h2')
.text(d => d.title);
这在需要为某个容器元素附加元数据时特别有用。注意:datum() 不会触发 enter/exit 模式,因为它是绑定单个数据而非数组。
Enter-Update-Exit 模式
当数据与 DOM 元素的数量不完全匹配时,需要处理三种情况。Enter-Update-Exit 模式就是用来解决这个问题的。
Update(更新)
当数据数量与已有 DOM 元素数量相等时,直接更新元素:
const data = [10, 20, 30];
// 假设 HTML 中有 3 个 circle 元素
const circles = d3.selectAll('circle')
.data(data);
circles
.attr('r', d => d) // 更新半径
.attr('fill', 'steelblue');
Enter(进入)
当数据数量多于 DOM 元素数量时,需要创建新的 DOM 元素来承载多余的数据:
// 假设 HTML 中只有 2 个 circle,但 data 有 3 个元素
d3.selectAll('circle')
.data([10, 20, 30])
.enter() // 选择"需要新增"的数据对应的占位符
.append('circle') // 为每个占位符创建新元素
.attr('r', d => d) // 设置属性
.attr('fill', 'steelblue');
enter() 返回一个「进入选择集」,其中的元素是占位符,它们代表需要创建的新 DOM 节点。对这些占位符调用 append(),就会真正创建 DOM 元素。
Exit(退出)
当数据数量少于 DOM 元素数量时,多余的 DOM 元素不再需要对应数据,需要将其移除:
// 假设 HTML 中有 4 个 circle,但 data 只有 2 个元素
d3.selectAll('circle')
.data([10, 20])
.exit() // 选择多余的(没有数据绑定的)元素
.remove(); // 移除这些元素
exit() 返回一个「退出选择集」,其中的元素是已经没有数据对应的 DOM 节点。调用 remove() 将它们从 DOM 中删除。
完整示例
将三个阶段组合起来,就能完整处理数据变化的所有情况:
const circles = d3.selectAll('circle').data([10, 20]);
// Update + Exit
circles.attr('r', d => d);
// Enter(为新数据创建元素)
circles.enter()
.append('circle')
.attr('r', d => d)
.attr('fill', 'steelblue');
// Exit(移除不再需要的数据)
circles.exit().remove();
join() 方法
手动编写 enter-exit 逻辑繁琐且容易出错。从 D3 v5 开始,引入了 join() 方法,它封装了这个逻辑,让代码更简洁。
基本用法
d3.selectAll('circle')
.data([10, 20, 30])
.join('circle')
.attr('r', d => d)
.attr('fill', 'steelblue');
join('circle') 会自动处理:已有元素会更新(Update),新增数据会创建新元素(Enter),多余元素会移除(Exit)。
精细控制
join() 还支持为 enter、update、exit 分别提供处理函数:
d3.selectAll('circle')
.data([10, 20, 30])
.join(
// enter: 新元素创建时
enter => enter.append('circle')
.attr('r', 0) // 初始半径为 0
.attr('fill', 'gray')
.call(enter => enter.transition().attr('r', d => d)), // 动画展开
// update: 已有元素更新时
update => update
.attr('fill', 'steelblue')
.call(update => update.transition().attr('r', d => d)),
// exit: 元素退出时
exit => exit
.call(exit => exit.transition().attr('r', 0).remove()) // 动画缩小后消失
);
这种精细控制特别适合需要复杂动画效果的可视化场景。
实战:小例子
通过一个完整的例子来巩固选择集和数据绑定的理解。
需求
动态创建一组li元素,显示传入的数据。
实现
const fruits = [
{name: '苹果', price: 5},
{name: '香蕉', price: 3},
{name: '橙子', price: 4},
{name: '葡萄', price: 8},
{name: '西瓜', price: 10}
];
// 获取 ul 元素,选择其中的 li
const ul = d3.select('#fruit-list');
ul.selectAll('li')
.data(fruits)
.join('li')
.html(d => `<strong>${d.name}</strong>: ¥${d.price}/斤`)
.style('margin', '8px 0')
.style('font-size', '14px');
这段代码做了以下事情:选择id为fruit-list的ul元素;选择ul中所有的li元素(虽然初始为空);将fruits数组绑定到这些li元素上;使用join()创建新li并设置内容。
如果数据发生变化,只需要重新绑定即可:
// 3秒后更新数据
setTimeout(() => {
const newData = [
{name: '苹果', price: 5},
{name: '香蕉', price: 3},
{name: '橙子', price: 4},
{name: '葡萄', price: 8},
{name: '西瓜', price: 10},
{name: '草莓', price: 15} // 新增
];
ul.selectAll('li')
.data(newData)
.join('li')
.html(d => `<strong>${d.name}</strong>: ¥${d.price}/斤`);
}, 3000);
新增的"草莓"会自动创建对应的li元素,无需手动处理 enter/exit。
总结
选择集和数据绑定是 D3.js 的核心基础。选择集提供了选择和操作 DOM 元素的能力;数据绑定将数据与 DOM 元素关联;而 enter-update-exit 模式(或更简洁的 join() 方法)则处理数据变化时的所有场景。
掌握这些概念后,你已经具备了构建任何 D3.js 可视化的基础。后续我们将学习如何使用比例尺将数据映射到视觉属性,以及如何创建各种类型的图表。