想象一个装修现场:王师傅接过一张全屋家具清单,清单上写着每件家具的尺寸、颜色和摆放位置。他没有多问,只扫了一眼清单,就开始有条不紊地搬家具、调角度、添新件。没过多久,你改动了清单——删掉两把椅子、加进一张书桌——王师傅二话不说,多余的椅子搬走,书桌从仓库取出放好。在这个世界里,网页里的每个元素都是“家具”,数据就是随时更新的“清单”,而 D3.js 正是那位不知疲倦的“智能装修师傅”。 D3 全称 Data-Driven Documents,直译就是“数据驱动的文档”。它最迷人的地方在于:你不需要手动告诉页面“这里加一个柱子、那里删一个圆点”,只要把数据递给它,页面就自动长出与数据匹配的图形。本文要带你从头认识这位师傅:他怎样“找家具”(选择集)、“贴标签”(数据绑定)、“加椅子撤椅子”(enter/exit 模式)、“换算尺寸”(比例尺)、“挪动家具”(过渡动画),以及他和“成品家具店”(Chart.js / ECharts)的差别。读完你会明白,为什么 D3.js 是前端可视化工程师离不开的罗盘。

岗位关联

D3.js 是前端可视化开发者的核心技能。掌握数据驱动 DOM 的思维,你就能从“套模板”进阶到“造轮子”,在定制可视化场景中拥有不可替代的竞争力。

目录
  1. 可视化演示
  2. 认识 D3.js
  3. 选择集操作
  4. 数据绑定
  5. enter/exit 模式
  6. 比例尺与坐标轴
  7. 过渡与交互
  8. 框架对比实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

交互演示D3.js核心机制:数据绑定与Enter/Update/Exit模式、比例尺域到范围映射、以及D3.js与ECharts/Chart.js的能力对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Enter/Update/Exit模式 Update:已有元素+有数据→属性更新 Enter:有数据无元素→创建新节点 selection.data(data) → join 数据驱动DOM的核心机制

认识 D3.js

想象你请来一位王牌装修师傅,他把一张全屋家具清单往桌上一放,上面写着每件家具的尺寸、颜色和摆放位置。你不必反复叮嘱,只要清单更新,他就能自动把家具一件件摆进房间、调好角度。D3.js 正是这位“数据驱动”的装修师傅——“D3“全称 Data-Driven Documents,翻译过来就是”数据驱动的文档“,它把网页里的每个元素(div、p、SVG 里的 circle)都当作可以按数据来布置的”家具”。

传统方式下,你想画一个柱状图,得手动写一堆 HTML,再写 CSS 调样式,改动数据还得全部重来。D3 的特别之处在于:你把“家具清单”(一个数据数组)交给它,它会自动生成、更新、移除对应的“家具”(DOM 元素)。这背后的信念是一句话:文档结构 = 数据的函数。清单长什么样,房间就长什么样;数据一变,页面自动跟着变。

D3 不是开箱即用的图表库,而是一套操作 DOM 的工具箱。它提供若干核心能力:用选择集(selection)圈定元素,用 data() 绑定数据,用 enter/exit 模式管理增删,用比例尺完成数据到像素的换算,用 transition 实现过渡动画。开发者用这些积木自由组装,造出任何想象得到的图表——这正是它“框架”定位的由来。

所以说,D3 之于网页,就像王师傅之于客厅:它不挑剔你的清单从哪来(JSON、CSV 还是接口返回),只负责把清单上的每一项精确变成房间里的实体。这就是“数据驱动”四个字的真正分量。

DOM=T(Data)\displaystyle \text{DOM} = \mathcal{T}(\text{Data})
这行式子的意思是:页面结构(DOM)是由数据(Data)经过一套变换(D3 操作流水线 T)得到的。数据是输入,页面是输出。
  1. Data(数据)输入的数据集合,可以是 JSON、CSV 或后端 API 返回的数组。在装修比喻中,它就是“家具清单”。
    Data=[ d1, d2, d3, … ]\displaystyle \text{Data} = [\,d_1,\ d_2,\ d_3,\ \dots\,]
  2. T(变换函数)D3 的一整套操作流水线,包含选择集、数据绑定、增删节点、属性更新等步骤。它像师傅的双手,把清单变成实体。
    T=update∘enter∘exit\displaystyle \mathcal{T} = \text{update} \circ \text{enter} \circ \text{exit}
  3. DOM(文档对象模型)输出的页面结构,也就是最终呈现在浏览器里的图形元素。它像被精心布置好的房间,每一件家具都各就各位。
    DOM={元素节点, 属性, 样式}\displaystyle \text{DOM} = \{\text{元素节点},\ \text{属性},\ \text{样式}\}

选择集操作

王师傅走进客厅,第一步不是搬家具,而是先“找到”要动手的对象——“这面墙上那三幅画,我要统一调低 10 厘米”。D3 的选择集(selection)就是干这个的:在页面这间“大房子”里,精确圈定你想操作的那批“家具”。

D3 提供两个最常用的“找人”方法:d3.select() 只找到第一个匹配元素,像师傅只挑最左边那幅画;d3.selectAll() 找到所有匹配元素,像师傅把墙上所有画都扫一遍。它们返回的都是 Selection 对象,这个对象承上启下,是后续所有操作(绑定数据、改属性、加动画)的起点。

Selection 对象最妙的地方是“链式调用”:你可以用 .attr() 修改元素属性(比如圆的半径 r),用 .style() 修改样式(比如填充颜色),用 .text() 改文字内容。这些方法都返回自身,于是可以一路点下去——.attr().style().text()——像师傅拿着一串钥匙,开完一道门顺手开下一道。

就像师傅必须先找到墙上的三幅画才能调整高度,D3 里一切修改都从“选中最少的目标”开始。选择集是整条数据驱动流水线的入口,选错了对象,后面所有操作都无从谈起。

f=append∘enter∘data∘selectAll\displaystyle f = \text{append} \circ \text{enter} \circ \text{data} \circ \text{selectAll}
这是 D3 最经典的启动管道:从左往右看是“先选元素、再绑数据、然后进入、最后创建”。它把选择、绑定、增删串成一条流水线。
  1. selectAll先在文档中圈定所有同名目标元素,相当于师傅找到房间里“所有画框”。
    selectAll("p")→{ p1, p2, p3 }\displaystyle \text{selectAll}(\text{"p"}) \to \{\,p_1,\ p_2,\ p_3\,\}
  2. data把数据数组绑定到这些元素上,相当于给画框贴上对应的画作标签。
    data([10,20,30])→{⟨p1,10⟩,⟨p2,20⟩,⟨p3,30⟩}\displaystyle \text{data}([10,20,30]) \to \{\langle p_1,10\rangle,\langle p_2,20\rangle,\langle p_3,30\rangle\}
  3. enter发现数据比元素多时,生成“待进场”占位,相当于发现画比画框多,得准备新画框。
    enter→{占位n+1, 占位n+2}\displaystyle \text{enter} \to \{\text{占位}_{n+1},\ \text{占位}_{n+2}\}
  4. append把占位变成真实的新元素,相当于从仓库取出新画框挂上墙。
    append("rect")→新 rect 节点\displaystyle \text{append}(\text{"rect"}) \to \text{新 }\text{rect}\text{ 节点}

数据绑定

清单上的每一件家具,都要贴上对应的房间号和位置标签,师傅才知道往哪放。数据绑定就是给 DOM 元素“贴标签”——让每个元素记住自己代表哪条数据。D3 中最重要的两类标签方法是 datum() 和 data()。

selection.datum(value) 把单个值绑到所有选中元素上,相当于在每件家具上都贴一张同样的便签;selection.data(array) 则把数组的每一项依次绑到选中的每个元素上,数组有多长,标签就贴到多少个元素身上。绑定之后,元素就“记住”了自己的数据,随时可以通过回调函数取出来用。

实际写法通常长这样:.attr("r", function(d) { return d.value; })。这里的 d 就是当前元素绑定的数据,d.value 就是这条数据的某个字段。属性值由数据计算而来,这正是“数据驱动”的精髓:代码不写死数值,只写“数值从哪里来”。

贴好标签的家具才有归属,绑定好数据的元素才能真正“活”起来。但注意:绑定只是“贴标签”,并没有改变元素本身——真正让元素长出来或消失的,是下一节要讲的 enter/exit 模式。

join(D,E)={ ⟨di, ei⟩  |  i∈[1, min⁡(∣D∣, ∣E∣)] }\displaystyle \text{join}(D, E) = \left\{\, \langle d_i,\ e_i \rangle \;\middle|\; i \in [1,\ \min(|D|,\ |E|)] \,\right\}
数据与元素按顺序一一配对:数据 [10,20,30] 与三个元素配对成三对“数据-元素组合”。多出来的数据或元素不参与配对,留给 enter/exit 处理。
  1. D(数据数组)比如 [10, 20, 30, 40],代表清单上的 4 件家具。
    D=[d1, d2, d3, d4]\displaystyle D = [d_1,\ d_2,\ d_3,\ d_4]
  2. E(元素集合)页面中已有的 DOM 节点,比如 3 个 <circle>,代表房间里已有的 3 个钉子。
    E={e1, e2, e3}\displaystyle E = \{e_1,\ e_2,\ e_3\}
  3. min(|D|, |E|)取两者较小的数量作为配对上限,保证每个元素最多对接一条数据。这里配对数量是 3。
    min⁡(4, 3)=3\displaystyle \min(4,\ 3) = 3
  4. ⟨d_i, e_i⟩第 i 条数据与第 i 个元素组成的“配对组合”,表示它们绑定在一起。
    ⟨d1,e1⟩, ⟨d2,e2⟩, ⟨d3,e3⟩\displaystyle \langle d_1, e_1\rangle,\ \langle d_2, e_2\rangle,\ \langle d_3, e_3\rangle

enter/exit 模式

清单上写了要摆 5 把椅子,客厅里只有 3 把——师傅得从仓库搬 2 把出来;反过来,清单改成 2 把,多出来的 1 把就得送回仓库。D3 的 enter/exit 模式就是这套逻辑:数据与元素数量不一致时,自动分堆处理。

当 data() 绑定完成,D3 会把所有相关元素分成三堆:enter(数据多出的部分,需要新建元素)、update(数据与元素配上的部分,可以原地更新)、exit(元素多出的部分,需要删除)。你不需要手写 if/else 判断数量,D3 已经替你算好了。

经典写法是这样的:先 selectAll 选中所有柱子,再 data(newData) 绑定新数据,然后 .join("rect") 一键整合 enter 和 update,最后 .exit().remove() 把多余的柱子移除。D3 v7 的 .join() 方法让 enter/update 合二为一,但你依然需要理解背后“加椅子、撤椅子”的机制。

王师傅的清单就是“玉律”:客人多了加椅子,客人走了撤椅子,房间容量永远精确匹配清单。这正是 D3 数据驱动最优雅的瞬间——你只需更新数据,剩下的一切都由 enter/exit 模式自动完成。

∣enter∣=max⁡(∣D∣−∣E∣, 0),∣exit∣=max⁡(∣E∣−∣D∣, 0)\displaystyle |\text{enter}| = \max(|D| - |E|,\ 0), \qquad |\text{exit}| = \max(|E| - |D|,\ 0)
enter 的数量 = 数据数减元素数(结果为正时);exit 的数量 = 元素数减数据数(结果为正时)。两者一加一减,保证页面最终与数据完全对齐。
  1. |D| - |E|数据比元素多的数量。比如数据 5 个、元素 3 个,差值是 2,就是要新建的元素数。
    5−3=2 ⇒ enter=2\displaystyle 5 - 3 = 2\ \Rightarrow\ \text{enter} = 2
  2. max(……, 0)防止数值为负。如果元素比数据多,enter 数量应为 0,而不是负数。
    max⁡(2, 0)=2,max⁡(−2, 0)=0\displaystyle \max(2,\ 0) = 2,\qquad \max(-2,\ 0) = 0
  3. |E| - |D|元素比数据多的数量。比如元素 3 个、数据 1 个,差值是 2,就是要删除的元素数。
    3−1=2 ⇒ exit=2\displaystyle 3 - 1 = 2\ \Rightarrow\ \text{exit} = 2
  4. 两个公式镜像互补一个管“加椅子”,一个管“撤椅子”,共同保证界面与数据同步。
    ∣enter∣+min⁡(∣D∣,∣E∣)=∣D∣,∣exit∣+min⁡(∣D∣,∣E∣)=∣E∣\displaystyle |\text{enter}| + \min(|D|,|E|) = |D|,\qquad |\text{exit}| + \min(|D|,|E|) = |E|

比例尺与坐标轴

清单上写“3 份食材”,王师傅心里要换算成“大约 15 厘米长的蛋糕”。数据里的“3“是抽象数字,屏幕上的”15cm”才是视觉长度。比例尺(scale)就是这套换算规则,它是 D3 里最常用、最实用的工具。

D3 的 scaleLinear() 创建线性比例尺,把连续数据范围(domain)映射到输出范围(range)。比如数据范围 [0, 100] 映射到像素 [0, 500],那么 50 就落在 250 像素的位置。scaleBand() 则创建带状比例尺,把分类数据(如“周一、周二、周三”)映射到等宽柱子的 x 坐标——每个分类占据一个等宽的“格子”。

有了比例尺,坐标轴也能自动生成:d3.axisBottom(x) 会根据比例尺的 domain 和 range 计算出刻度线、刻度标签,然后画成 SVG 元素。你不需要手动标注“50 对应 250px”,比例尺和坐标轴帮你包办。

没有比例尺,师傅只能凭感觉摆放家具,高度全看心情;有了比例尺,1 份就是 5 厘米,3 份就是 15 厘米,精确无误。比例尺是 D3 把抽象数据“翻译”成人能感知的视觉形状的关键通道。

y=ymin⁡+(x−xmin⁡)(ymax⁡−ymin⁡)xmax⁡−xmin⁡\displaystyle y = y_{\min} + \frac{(x - x_{\min})(y_{\max} - y_{\min})}{x_{\max} - x_{\min}}
这是线性比例尺的标准公式:先算出 x 在数据范围内的相对位置(0 到 1 之间),再放大到像素范围。把“数据空间”翻译成“屏幕空间”。
  1. x_min, x_max数据范围(domain)的边界。比如销售额从 0 到 1000 万元。
    xmin⁡=0,xmax⁡=1000\displaystyle x_{\min} = 0,\qquad x_{\max} = 1000
  2. y_min, y_max像素范围(range)的边界。比如柱子高度从 0 到 500 像素。
    ymin⁡=0,ymax⁡=500\displaystyle y_{\min} = 0,\qquad y_{\max} = 500
  3. x - x_min当前数据在数据范围内的偏移量。比如 500 万元在 [0,1000] 内的偏移是 500。
    x−xmin⁡=500−0=500\displaystyle x - x_{\min} = 500 - 0 = 500
  4. 标准化比值偏移量除以数据总跨度,得到 0~1 之间的相对位置。
    500−01000−0=0.5\displaystyle \frac{500 - 0}{1000 - 0} = 0.5
  5. y(最终输出)相对位置放大到像素范围,得到最终的屏幕坐标。
    0+0.5×(500−0)=250\displaystyle 0 + 0.5 \times (500 - 0) = 250

过渡与交互

王师傅搬家具从来不是瞬移——他先抬起柜子,缓缓挪过客厅,再轻轻放下,整个过程大约半秒。这个“缓缓挪动”就是 D3 的过渡动画(transition)。一个好的过渡让数据变化变得肉眼可见,而不是“唰”地一下跳变。

.transition() 开启一个过渡,配合 .duration(500) 指定毫秒数,再跟一长串 .attr(),D3 会在半秒内平滑地改变属性值。比如柱子高度从 100px 变到 300px,动画的每一帧都会计算一个中间值,从 100、110、120……一路平滑走到 300。这个计算中间值的过程就是插值。

交互同样直接:.on("click", (event, d) => { ... }) 给元素绑定点击事件,回调函数里的参数 d 就是当前元素绑定的数据——点击某个柱子,立刻知道它代表哪条数据。拖拽行为(drag)和缩放行为(zoom)也都是 D3 内置的,它们让图表不再是一张静态图片,而是一个可以“上手操作”的实体。

瞬间移动让人摸不着头脑,缓缓挪动才看得清路径。过渡动画就是用“过程”让数据变化变得可感知——当柱状图随着新数据轻轻起伏,用户一眼就能看出哪些数据涨了、哪些跌了、哪些是新出现的。

v(t)=v0+(v1−v0)×t,t∈[0,1]\displaystyle v(t) = v_0 + (v_1 - v_0) \times t, \quad t \in [0, 1]
过渡动画的核心是插值:从起始值 v0 出发,按进度 t 逐步走向结束值 v1。t=0.5 时,值正好走完一半。
  1. v0(起始值)过渡开始时的旧属性值,比如柱子原来的高度 100px。
    v0=100\displaystyle v_0 = 100
  2. v1(结束值)过渡结束时的新属性值,比如柱子要变成 300px。
    v1=300\displaystyle v_1 = 300
  3. t(进度)动画时间进度,0 表示刚开始,1 表示结束。每渲染一帧,t 都会增加一点。
    t=0.5 ⇒ 动画进行到一半\displaystyle t = 0.5\ \Rightarrow\ \text{动画进行到一半}
  4. v(t)(当前值)任意时刻柱子的实际高度;t=0.5 时正好在 200px。
    v(0.5)=100+(300−100)×0.5=200\displaystyle v(0.5) = 100 + (300 - 100) \times 0.5 = 200

框架对比实战

“成品家具店”的沙发(Chart.js、ECharts)——下单就送货上门,款式固定但改不动;D3 这位“装修师傅”——什么都能做,但装修周期长。选谁,取决于你要“买成品”还是“搞定制”。

Chart.js 和 ECharts 是典型的“图表库”:传一个配置对象,几行代码就能得到标准柱状图、折线图、饼图,内置主题、动画、提示框,开箱即用。D3.js 则是“工具库”,它不提供任何预置图表,只给你操作 DOM、绑定数据、换算比例、驱动动画的积木。没有现成图,但有无限可能。

选择原则很简单:如果只做常见柱状图、折线图,且工期紧,优先 Chart.js/ECharts;如果需要大屏炫酷定制、复杂交互、与 DOM 深度集成,D3 是不二之选。D3 的包体积更大、学习曲线更陡,但自由度的天花板也最高。一个典型的 D3 柱状图实战代码如下:

D3 和数据之间没有中间商:你想画什么,数据就长成什么样。这也解释了为什么大型可视化项目、数据新闻、大屏定制场景里,D3 始终是工程师的首选——成品家具店给不了你一座量身定制的房子,但王师傅可以。

xi=σ(ci),yi=λ(vi)\displaystyle x_i = \sigma(c_i), \qquad y_i = \lambda(v_i)
一张柱状图的每个柱子,x 坐标由带状比例尺 σ 根据分类算出,y 坐标由线性比例尺 λ 根据数值算出。两条比例尺合奏出整张图表。
  1. c_i(分类值)第 i 条数据的分类,比如“周一”。
    c1=周一\displaystyle c_1 = \text{周一}
  2. σ(带状比例尺)把分类映射为等宽的 x 坐标:周一在第 1 格,周二在第 2 格。
    σ(周一)=0,σ(周二)=30\displaystyle \sigma(\text{周一}) = 0,\qquad \sigma(\text{周二}) = 30
  3. v_i(数值)第 i 条数据的数值,比如销量 42。
    v1=42\displaystyle v_1 = 42
  4. λ(线性比例尺)把数值按比例映射为 y 坐标:42 对应 210px。
    λ(42)=42×5=210\displaystyle \lambda(42) = 42 \times 5 = 210
js10 行
const svg = d3.select("#chart");const data = [4, 8, 15, 16, 23, 42]; svg.selectAll("rect")  .data(data)  .join("rect")  .attr("x", (d, i) => i * 30)  .attr("y", d => 100 - d)  .attr("width", 25)  .attr("height", d => d);

AI 应用场景

数据新闻与深度报道

阶段:数据可视化呈现

解决的问题:传统新闻报道用静态图片表达数据,数据一旦更新就要重做整张图,编辑和设计师的沟通成本极高。D3 让记者把原始数据直接绑定到图表上,数据一换,图表自动更新,同时保留定制化的视觉风格。

具体案例:某新闻网站报道城市人口迁移,用 D3 构建了一幅交互式地图。读者滑动时间轴,城市之间的迁移线条会平滑生长、加粗、变色,所有动画均由数据驱动,报道发布后编辑只需替换 CSV 数据文件即可复用整张图。

企业实时大屏

阶段:数据监控与决策支持

解决的问题:企业作战指挥大屏需要高度定制、实时刷新、多图联动,通用图表库的样式和交互很难满足。D3 的 enter/exit 模式让大屏在数据每秒变化时只更新变化的图元,而不是整屏重绘。

具体案例:某物流公司的调度中心大屏用 D3 绘制全国车辆的运行轨迹和订单热力图,数据每 3 秒刷新一次。车辆到达新城市时,代表车辆的圆点平滑移动;新增订单时,热力块从透明渐变为高亮——所有变化都在几十毫秒内完成,调度员一目了然。

私有组件库建设

阶段:前端基建与体验优化

解决的问题:业务报表需要统一设计语言,且不同业务线持续提出新图表形态;开源图表库无法覆盖所有细分场景。用 D3 从零封装自有组件库,既保住定制能力,又沉淀为团队资产。

具体案例:某 SaaS 团队用 D3 封装了内部“迷你图表库”,包含 20 多个定制图表组件,统一了配色、字体、间距规范。新业务接入时只需传入数据与配置项,组件内部用 D3 的 join 模式自动管理 DOM 增删,维护成本大幅下降。

Python 演示

python48 行
"""模拟 D3.js 的 enter/update/exit 数据连接模式""" class Selection:    """模拟 D3 Selection 的最小实现"""     def __init__(self, elements, name="dom"):        self.elements = list(elements)        self.name = name        self._data = []     def data(self, data_list):        """把数据数组绑定到现有元素上"""        self._data = list(data_list)        return self     def enter(self):        """返回数据比元素多的部分(需要新建)"""        return self._data[len(self.elements):]     def exit(self):        """返回元素比数据多的部分(需要删除)"""        return self.elements[len(self._data):]     def update(self):        """返回数据和元素配对的部分(可以更新)"""        return list(zip(self.elements[:len(self._data)],                        self._data[:len(self.elements)]))  def demo():    # 页面里已有 3 个 <circle> 元素    circles = Selection(["circle-0", "circle-1", "circle-2"])     # 新数据有 5 条 -> 多出 2 条 enter    circles.data([10, 20, 30, 40, 50])    print("enter:", circles.enter())    print("update:", circles.update())    print("exit:", circles.exit())     # 新数据只有 2 条 -> 多出 1 个元素 exit    circles.data([80, 90])    print("enter:", circles.enter())    print("update:", circles.update())    print("exit:", circles.exit())  if __name__ == "__main__":    demo()
目的:用 Python 类模拟 D3 的 data join 三阶段逻辑,帮助你从算法层面理解 enter/update/exit 的本质——它并不神秘,只是“数量差”和“配对索引”的计算。
  1. class Selection:定义一个最简 Selection 类,模拟 D3 中 selectAll 返回的选择集对象。
  2. def data(self, data_list):模拟 D3 的 data() 方法,把数据存进内部 _data 列表并返回自身,支持链式调用。
  3. return self._data[len(self.elements):]enter() 的核心:取数据数组中超出元素数量的后半段,这些就是要“新建”的部分。
  4. return self.elements[len(self._data):]exit() 的核心:取元素列表中超出数据数量的后半段,这些就是要“删除”的部分。
  5. return list(zip(self.elements[:len(self._data)], self._data[:len(self.elements)]))update() 把配对部分打包成“元素-数据”元组,模拟原地更新任务。

常见误区

误区听说 D3 很强大,应该像 ECharts 一样调用 `d3.chart()` 传个配置就能渲染出漂亮的柱状图。但翻开文档发现连个柱状图函数都没有,是不是我找错库了?

正解D3 是操作 DOM 的底层工具库,它不提供任何预置图表。你需要自己组合选择集、数据绑定、比例尺、enter/exit 来“组装”图表。它给你的是积木,不是成品乐高套装。这就像王师傅给你工具箱而不是成品家具店——上限更高,但需要自己动手。

为何常见:D3 名字带“可视化”,大家又常拿它和 Chart.js 比较,自然误以为它是开箱即用的图表库。实际上它的定位是“工具箱”而非“家具店”。

误区我调用了 `.data(arr)`,然后把数组里的值改了,页面上的图怎么纹丝不动?数据驱动不应该自动更新吗?

正解.data() 只是把数据“绑”在元素上,并不会自动增删或更新元素。你必须手动调用 .enter().append() 创建新元素、.exit().remove() 删除多余元素、.attr(...) 更新配对部分的属性。D3 v7 的 .join() 能一键整合这些步骤,但底层依然是你手动触发的。

为何常见:许多开发者习惯了 Vue/React 的响应式思维,以为“数据变了视图自动变”。D3 是命令式工具库,讲究“你让 D3 做什么它才做什么”。

误区`.data()` 和 `.datum()` 都是绑定数据,名字这么像,功能肯定一样,随便用哪个都行。

正解.datum(value) 把同一个值绑到所有被选元素上,适合绑定对象或常量;.data(array) 把数组逐项与元素配对,并且是 enter/exit 模式的前提。把 data 换成 datum 会导致 enter/exit 完全失效,因为 D3 无法感知数组长度与元素数量的差异。

为何常见:两个方法名一字之差,文档示例又常混用,初学者不读 API 细节就容易踩坑。

误区`scaleLinear` 就是画坐标轴刻度用的,不画坐标轴就不用比例尺。

正解比例尺是通用的“映射器”。数据 0~100 可以映射到像素 0~500,也可以映射到颜色范围 ['#fff', '#000'],或映射到圆饼半径 [5, 50]。任何需要“把数据范围变成视觉范围”的地方都能用比例尺,比如气泡大小、颜色深浅、描边宽度。

为何常见:教程里比例尺总出现在坐标轴的案例中,看多了容易把它和坐标轴绑定死。实际上坐标轴只是比例尺的一种输出方式。

误区D3 操作的是 DOM,DOM 节点一多浏览器就卡,所以大数据可视化还是用 canvas 库吧。

正解性能瓶颈在 SVG 的 DOM 节点数量,不在 D3 本身。D3 同样支持 canvas 渲染路径,用 d3.canvas() 渲染数万个节点毫无压力。SVG 适合中等数据量和矢量交互,Canvas 适合海量数据点,D3 两种都支持,甚至可以混用。

为何常见:D3 的 SVG 示例流传最广,大家把“SVG 慢”的锅扣到了 D3 头上,忽略了它底层渲染的可切换性。

面试题

Q1. D3.js 中“数据驱动文档”的核心含义是什么?

单选题 · easy

  • A.数据变化时页面自动响应← 正确答案
  • B.文档格式决定数据结构
  • C.只能处理服务器端数据
  • D.图表必须使用 SVG 绘制

答案:A。D3 的核心价值是把数据映射为 DOM 结构,数据一变,页面通过 enter/update/exit 自动同步。B 把因果关系说反了,C 和 D 都是对 D3 的无端限制。

Q2. d3.select("p") 与 d3.selectAll("p") 的主要区别是什么?

单选题 · easy

  • A.前者只选第一个元素← 正确答案
  • B.前者选择全部段落
  • C.后者只能选一个节点
  • D.两者返回值完全不同

答案:A。select 返回第一个匹配元素,selectAll 返回所有匹配元素并保留分组结构。B、C 说反了,D 说“完全不同”过于绝对,两者都返回 Selection 对象。

Q3. 关于 D3.js 与 Chart.js,下列说法正确的是?

单选题 · easy

  • A.D3 是低层工具库,无预置图表← 正确答案
  • B.Chart.js 能做出任意可视化
  • C.D3 必须联网才能运行
  • D.Chart.js 不支持动画效果

答案:A。D3 提供操作元素、绑定数据、比例尺等底层工具,不提供现成图表;Chart.js 只能做常见图表,不能“任意”;C 和 D 都是错误说法。

Q4. .data(arr) 与 .datum(v) 的核心差异是什么?

单选题 · mid

  • A.data 逐项绑定,datum 绑定单值← 正确答案
  • B.datum 逐项绑定,data 绑定单值
  • C.两者都执行 enter/exit 逻辑
  • D.两者属于不同版本的 API

答案:A。data(array) 把数组与元素逐一配对,支持 enter/exit 管理数量差异;datum(value) 把单个值绑到所有元素上,不涉及数量匹配。C 说反了,D 的时间线说法不对。

Q5. DOM 中已有 3 个 circle,调用 data([1,2,3,4,5]) 后,enter() 有几个待创建元素?

单选题 · mid

  • A.返回 2 个待创建元素← 正确答案
  • B.返回 3 个待创建元素
  • C.返回 5 个待创建元素
  • D.返回 0 个待创建元素

答案:A。数据 5 条多于元素 3 个,enter 数量 = 5 - 3 = 2。数据比元素多的部分需要新建元素,这正是“加椅子”的场景。

Q6. scaleLinear().domain([0,100]).range([0,500]),输入 50 的输出是多少?

单选题 · mid

  • A.输出 250,因为占比一半← 正确答案
  • B.输出 500,因为范围上界
  • C.输出 50,因为输入原样
  • D.输出 100,因为 domain 上界

答案:A。50 在 domain [0,100] 中恰占一半,映射到 range [0,500] 就是 250。比例尺做的是线性缩放,不是恒等映射。

Q7. 关于 enter/update/exit 模式,下列哪些说法正确?

多选题 · mid

  • A.数据多时用 enter 新建← 正确答案
  • B.元素多时用 exit 删除← 正确答案
  • C.配对部分属于 update← 正确答案
  • D.数据绑定后自动增删元素

答案:ABC。A、B、C 分别描述了三种状态的职责。D 错误,数据绑定不会自动增删元素,必须显式调用 .enter().append() 和 .exit().remove()。

Q8. D3 过渡动画中,数值属性变化的默认插值方式是什么?

单选题 · hard

  • A.线性插值,从旧值匀速过渡← 正确答案
  • B.非线性插值,先快后慢
  • C.每次取随机中间值
  • D.不插值,直接跳变

答案:A。D3 默认对数字属性做线性插值,即 v(t) = v0 + (v1 - v0) * t,整个动画过程匀速。非线性效果需要额外指定 easing 函数。

Q9. 哪些场景下优先选择 D3.js 而不是 ECharts?

多选题 · hard

  • A.需要高度定制化的图元← 正确答案
  • B.只需标准柱状图展示
  • C.与 DOM 深度绑定交互← 正确答案
  • D.一周内快速交付报表

答案:AC。D3 的强项是定制化、自由度高、与 DOM 深度绑定;如果只需标准图表且工期紧,Chart.js/ECharts 的开箱即用远胜于 D3 的手工组装。

Q10. D3 v7 的 selection.join("circle") 相当于哪组操作的整合?

单选题 · extreme

  • A.整合 enter、update、exit 三环节← 正确答案
  • B.只能新增元素不能删除
  • C.合并两个现有数据集
  • D.替代 transition 过渡动画

答案:A。.join(name) 是 data() 之后的一站式写法:数据多自动执行 enter().append(name),元素多自动执行 exit().remove(),配对部分原地更新。B、C、D 都与 join 的实际行为不符。