选择集与数据绑定
🎯 引言
上一篇我们用 enter().append() 从无到有画出了柱状图。但真实项目里数据是会变的:今天后台返回 5 天的销量,明天可能变成 7 天,后天只剩 3 天。数据变了,页面上的柱子怎么跟着增删?学会这篇文章,你能彻底搞懂 D3 最核心的数据绑定机制。
🧱 选择集:D3 的“抓手”
想操作元素,得先“抓住”它。D3 提供两个方法:
d3.select('svg'):抓第一个匹配的元素。d3.selectAll('rect'):抓所有匹配的元素。
d3.select('svg'); // 抓住页面上的 svg
d3.selectAll('rect'); // 抓住所有柱子
抓到的结果叫选择集,它可以链式调用——抓住元素后一个接一个地下命令。如果你用过 jQuery 的 $(),会发现它们非常像。
🧱 attr 的两种传值
.attr() 用来修改元素属性,它有两种写法,效果完全不同:
// 传固定值:所有柱子宽度都是 40
svg.selectAll('rect').attr('width', 40);
// 传函数:每个柱子单独计算
svg.selectAll('rect').attr('x', (d, i) => i * 60);
传函数时,D3 会对每个元素各调用一次,并传入两个参数:d 是这个元素绑定的数据,i 是它是第几个。假设数据是 [120, 200, 150],这个函数会被调用 3 次:
| 第几次调用 | d(数据) | i(索引) | 算出 x = i × 60 |
|---|---|---|---|
| 1 | 120 | 0 | 0 |
| 2 | 200 | 1 | 60 |
| 3 | 150 | 2 | 120 |
style(改 CSS 样式)和 text(改文字内容)的用法和 attr 完全一样。
⚡ datum 和 data 的区别
两个方法都是“绑数据”,长得像,用途完全不同:
.data(数组):把数组分发给一组元素,一个数据配一个元素。.datum(值):把同一个值整体绑给元素,一个元素拿走全部。
// 每个柱子配一个数据:120 给第一个、200 给第二个……
svg.selectAll('rect').data([120, 200, 150]);
// 一条折线是一个整体,整个数组都给它
svg.select('path').datum(points);
记忆方法:data 配数组(分发),datum 配单值(整体)。画多个图形用 data,画单个整体图形(如一条折线)用 datum。
🧱 enter、update、exit:数据和元素的配对
.data() 执行的那一刻,D3 会把数据和页面上已有的元素按顺序配对,配对结果只有三种情况。
情况一:数据比元素多——多余的数据进 enter 区,等着补元素:
数据(5 个): [120] [200] [150] [80] [70]
元素(2 个): [rect][rect]
前 2 个配上对 → update(更新已有的)
后 3 个没人要 → enter(补 3 个 rect)
情况二:元素比数据多——多余的元素进 exit 区,等着被删掉:
数据(3 个): [120] [200] [150]
元素(5 个): [rect][rect][rect][rect][rect]
前 3 个配上对 → update(更新已有的)
后 2 个多余 → exit(删掉这 2 个 rect)
情况三:一样多——全部进 update 区,直接更新即可。
所以数据更新时的完整动作是三件事,一件都不能少:
const rects = svg.selectAll('rect').data(data);
// 1. exit:多余的元素删掉
rects.exit().remove();
// 2. update:已有的元素更新属性
rects.attr('y', (d) => 300 - d).attr('height', (d) => d);
// 3. enter:新增的数据补上元素
rects
.enter()
.append('rect')
.attr('x', (d, i) => i * 60)
.attr('width', 40)
.attr('fill', '#5470c6')
.attr('y', (d) => 300 - d)
.attr('height', (d) => d);
记住口诀:多余的删、已有的改、新增的补。
enter().append() 就能成功的原因,它是三件套的“特殊情况”。🧰 join():一行顶三行
每次都写三件套有点啰嗦,D3 v6 之后提供了 join(),自动把三件事合并:
svg.selectAll('rect')
.data(data)
.join('rect') // 自动处理 enter、update、exit
.attr('x', (d, i) => i * 60)
.attr('width', 40)
.attr('fill', '#5470c6')
.attr('y', (d) => 300 - d)
.attr('height', (d) => d);
日常写这一行就够了。但一定要先理解背后的三件套,否则遇到“新增的元素要有入场动画、已有的元素要平滑过渡”这类需求时,就不知道从哪里下手了。
🔑 用 key 精确配对
默认配对按位置顺序进行,数据会插入、删除时就会出问题。
举个例子:页面上有 A、B、C 三根柱子,现在从数据中删掉 B。按位置配对时,D3 认为“前两个没变、删掉最后一个”——于是 C 的柱子被删了,B 的柱子改个数据继续冒充 C。虽然总数没错,但柱子和数据错位了。
给数据指定“身份证”就能精确配对:
const data = [
{ id: 'a', value: 120 },
{ id: 'b', value: 200 },
{ id: 'c', value: 150 },
];
svg.selectAll('rect').data(data, (d) => d.id);
有了 key,D3 就知道走的是 id 为 b 的那个柱子,增删改全部对号入座。数据是对象数组时,建议总是加上 key。
下面的对比演示里,每个 id 有固定颜色(A 蓝、B 红、C 绿)。点击「删除 B」:左边按位置配对时,红色的 B 柱子没有消失,而是改个数据“冒充”了 C;右边按 key 配对时,B 正确离开,C 保持自己的颜色和身份:
不按 key(按位置配对)
按 key 配对
🧾 小节总结
select抓单个、selectAll抓全部,返回可链式调用的选择集。attr传固定值则所有元素一样;传函数则逐个计算,回调参数是(d, i)。data把数组分发给一组元素,datum把单值整体绑给一个元素。- 配对结果三种:数据多了进 enter(补元素)、元素多了进 exit(删元素)、配上的进 update(改属性)。
join()一行合并三件套;对象数组用.data(data, d => d.id)指定 key 防止错位。
❓ 知识问答
Q1:第一次渲染时元素还不存在,为什么也要 selectAll?
选不到任何元素也没关系,此时所有数据都落入 enter 区,正好全部走 append 创建。这套模板对“从无到有”和“增量更新”是通用的。
Q2:attr 和 style 都能改外观,怎么选?
SVG 元素的几何属性(x、y、width、height、fill 等)用 attr,纯 CSS 层面的样式(如 opacity、cursor)用 style。拿不准时用 attr,SVG 属性基本都归它管。
Q3:join() 能分别定制三种状态吗?
可以,传三个函数:.join(enter => ..., update => ..., exit => ...)。初学阶段先用字符串形式,后面动画篇会看到它的实际用法。
Q4:不用 key 会有什么后果?
数据顺序稳定时没影响。一旦数据会插入、删除、排序,按位置配对会导致图形和数据错位,比如想删 B 柱子结果删成了 C 柱子。
🧪 小练习
封装一个 render(data) 函数,验证数据的增删改:
- 第一次调用
render([120, 200, 150]),画出 3 个柱子。 - 打开控制台再调用
render([120, 200, 150, 80, 70]),观察新增 2 个柱子。 - 最后调用
render([100, 60]),观察多余柱子被删掉、剩下的高度更新。
const svg = d3.select(svgRef.value);
function render(data) {
// 请在这里编写代码(提示:使用 join 或 enter/update/exit)
}
render([120, 200, 150]);
🎉 恭喜你已经掌握 D3 最核心的数据绑定机制!下一篇我们学习比例尺,解决“数据值太大画不下”的换算问题。
