D3 简介

选择集与数据绑定

掌握 D3 选择集和数据绑定的核心机制,学会用 enter、update、exit 处理数据的增删改。

🎯 引言

上一篇我们用 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
112000
2200160
31502120

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 区——这就是上一篇只写 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 就知道走的是 idb 的那个柱子,增删改全部对号入座。数据是对象数组时,建议总是加上 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) 函数,验证数据的增删改:

  1. 第一次调用 render([120, 200, 150]),画出 3 个柱子。
  2. 打开控制台再调用 render([120, 200, 150, 80, 70]),观察新增 2 个柱子。
  3. 最后调用 render([100, 60]),观察多余柱子被删掉、剩下的高度更新。
const svg = d3.select(svgRef.value);

function render(data) {
    // 请在这里编写代码(提示:使用 join 或 enter/update/exit)
}

render([120, 200, 150]);

🎉 恭喜你已经掌握 D3 最核心的数据绑定机制!下一篇我们学习比例尺,解决“数据值太大画不下”的换算问题。