D3.js — 不是图表库,是写图表库的乐高
是什么
D3(Data-Driven Documents)不是 Excel 那种”选数据→选图表”的工具,而是画图表的乐高底层。日常类比:你想搭一栋房子,Chart.js 给你”客厅模板/卧室模板”,d3 给你砖头、木板、钉子——更费力,但能搭任何形状。
最小代码:
const x = d3.scaleLinear().domain([0, 100]).range([0, 800])x(50) // → 400这一行做了什么?把”数据范围 0-100”映射到”屏幕像素 0-800”。x(50) 自动算出 400。
d3 的所有 API 都是这种”小函数组合”——30+ 个独立子包(selection / scale / shape / array / hierarchy / force / geo / brush / zoom / drag / transition / color / interpolate / format …)每个解决一件事,按需 import。Observable Plot、nivo、Recharts、visx 都是搭在它上面的高层封装。
为什么重要
不理解 d3,下面这些事都没法解释:
- 为什么 nivo / Recharts / visx / Observable Plot 这些图表库底层都是 d3——它们只是 d3 的高层封装
- 为什么 Bostock 自己说 “d3 takes about 6 months to feel productive”——它的核心概念 data join 真的反直觉
- 为什么 React 项目里用 d3 总有”两套 DOM 打架”的感觉——d3 设计于 2011 年,那时还没 React
- 为什么纽约时报 / FT / 538 那些”长得不像普通图表”的数据新闻几乎都用 d3——它给你画任何形状的自由
核心要点
d3 的所有 API 表面可归结为 三件套:
-
Selection(选择):
d3.select(".foo")像 jQuery 的$()但加了”数据绑定”——每个 DOM 节点身上挂一个数据元node.__data__。所有 attr / style / on 都是链式调用。 -
Scale(标尺):
scaleLinear / scaleTime / scaleBand是域到值域的函数。类比体温计:水银柱长度(数据 36-40°C)映射到刻度位置(像素 0-800)。13 种 scale 覆盖线性 / 对数 / 时间 / 序数。 -
Shape(路径生成器):
d3.line() / d3.arc() / d3.pie()接收数据数组,返回一个 SVG path 字符串。它只生成字符串,不碰 DOM——把 path 画上去要你自己svg.append("path").attr("d", ...)。
加上 data join(下一段案例 1)这第四个抽象,d3 80% 的图都画得出来。
实践案例
案例 1:data join 的 enter / update / exit
svg.selectAll("circle") .data([10, 20, 30, 40]) // 把数组绑到 DOM .join("circle") // enter+update+exit 一步 .attr("cx", d => d * 10) .attr("cy", 50) .attr("r", 5)data(array) 把数组和现有 DOM 节点按 key 配对,分三类:
- enter:数组里有但 DOM 没有 → 新建节点
- update:两边都有 → 更新 attr
- exit:DOM 有但数组没有 → 删除节点
数组变了再调一次同样的代码,d3 自动算 diff。这就是 React key + diff 的远房亲戚——只是 d3 早 5 年就有了。
案例 2:scaleLinear 的函数式映射
const x = d3.scaleLinear() .domain([0, 100]) // 数据范围 .range([40, 780]) // 像素范围 .clamp(true) // 超出 domain 的值压回边界
x(50) // → 410,线性插值x.invert(410) // → 50,反向算回数据值scale 是 JS “函数即对象”的优雅例子:x 既能当函数调(x(50)),又能当对象配置(x.domain([...]))。颜色也能插:range(["red", "blue"]) 时 x(0.5) 返回 "rgb(128, 0, 128)"——零额外配置。
案例 3:d3 helper + React 渲染(visx / Recharts 路线)
function LineChart({ data }) { const x = d3.scaleLinear().domain([0, 100]).range([0, 800]) const y = d3.scaleLinear().domain([0, 1]).range([400, 0]) const line = d3.line().x(d => x(d.t)).y(d => y(d.v)) return ( <svg width={800} height={400}> <path d={line(data)} stroke="steelblue" fill="none" /> {data.map(d => <circle key={d.id} cx={x(d.t)} cy={y(d.v)} r={3} />)} </svg> )}只用 d3 的纯函数部分(scale / shape),DOM 操作交给 React。这是 visx / Recharts / nivo 都选的路线——避开 d3 直接写 DOM 和 React 打架的坑。
scale 和 line 都是普通 JS 函数,没碰任何 DOM API;React 拿它们生成的字符串当 props 用。两套系统井水不犯河水。
踩过的坑
-
不传 key function:默认 d3 按数组下标配对节点。数组中间插入元素时,所有后续节点错位重绑,动画乱跳。修:
.data(arr, d => d.id),传 key function。一旦遇过一次就再也不会忘。 -
import * as d3让 tree-shake 失效:拉全量 ~250KB。子包按需 import 能压到 30KB,但 webpack/vite 默认配置经常 tree-shake 不动,建议显式import { scaleLinear } from "d3-scale"而不是from "d3"。 -
直接和 React 同操作 DOM:d3 偷偷加节点,React 下次 render 看不见就抹掉。要么 React 只渲染空容器、d3 接管内部(牺牲 SSR),要么走案例 3 的 helper 路线。两套系统不共用同一片 DOM 是底线。
-
没有现成 Chart 抽象:d3 给你 line / arc / pie,但没有”折线图”这个完整概念——轴、网格、tooltip、legend 都要自己拼。每个团队第一次用 d3 都会重新造一个 Chart 抽象,半年后才意识到 Plot / Recharts 早就帮你做了。
适用 vs 不适用场景
适用:
- 需要画”非标准”图表(流向图、桑基图、力导向图、自定义动画)
- 数据新闻 / 论文级定制可视化(NYT、FT、538 的复杂图)
- 当 helper 用:scale / shape / hierarchy / force 这些纯函数部分
- Observable notebook 里探索性数据分析
不适用:
- 标准 dashboard 折线图 / 饼图——用 Chart.js / ECharts / Recharts 更省事
- 大数据量(10 万+ 点)实时渲染——d3 默认 SVG,要 Canvas 或 WebGL
- 纯 SSR 场景——d3 假设
window.document存在,Node 端要 jsdom polyfill - 完全可序列化的图表配置(保存到 DB)——用 Vega-Lite 的 JSON spec
历史小故事(可跳过)
- 2009 Protovis:Stanford VIS group(Bostock + Heer)的前身,声明式 SVG DSL,CPU 渲染慢
- 2011 D3 v1:Bostock 一作的 InfoVis 论文,砍掉 Protovis 的”声明式”换成”直接操作 DOM”
- 2016 D3 v4:拆模块化,30+ 子包独立发版、独立 changelog
- 2017 Observable:Bostock 离开 NYT 全职做 reactive notebook 平台,d3 在上面成一等公民
- 2021 D3 v7:当前主线,全 ESM、TS 类型完善(@types/d3-*)
- 2022 Observable Plot:Bostock 推出高层 API,承认 d3 对 90% 用户太低层
学到什么
- 可视化不是图表,是数据 → 像素的映射——d3 把这个映射拆成三件套(select / scale / shape)让你自由组合
- API 设计可以用”函数即对象”——
x(50)和x.domain([...])同一个东西,链式 setter 让配置和调用统一 - state 可以挂在 DOM 上:
node.__data__是 d3 的核心 trick——不要外部 Map,下次 select 同一节点直接拿到数据 - 乐高底层 vs 整装产品是两个市场:d3 永远不会自带”Chart”,那是 Plot / Recharts 的活
- early adopter 的代价:d3 设计于 2011,没赶上 React/Vue 时代——和现代框架共存是这类”早期 DOM 直写库”共同的迁移成本
延伸阅读
- 视频教程:Curran Kelleher — D3 完整课程(freeCodeCamp 上 6 小时把三件套讲透)
- 官方 gallery:Observable @d3 collection(几十个可改的示例 notebook)
- Bostock 原论文:D3: Data-Driven Documents (InfoVis 2011)
- 高层替代:Observable Plot 文档(同作者,标准统计图首选)
- playwright —— 测 d3 渲染时常用的浏览器自动化工具
关联
- gsap —— 动画库,
d3-timer和它的 RAF 调度思路类似 - lottie —— SVG/Canvas 动画运行时,对照 d3-geo 的 path 输出
- framer-motion —— React 动画方案,对比”d3 直接写 DOM” vs “React 描述式”
- react-spring —— 物理弹簧动画,对照
d3-force的 velocity Verlet 数值积分 - motion-one —— WAAPI 动画库,对照
d3-transition走 RAF 的实现差异 - playwright —— 测可视化常用,截图比对 d3 渲染输出
反向链接
- 3d-force-graph —— 3d-force-graph — 把网络拓扑搬进三维空间
- altair —— Altair — Python 上的 Vega-Lite 绑定
- amcharts5 —— amCharts 5 — TypeScript 重写的商业级图表库
- antv-f2 —— AntV F2 — 移动端 Canvas 图表,G2 同语法的轻量子集
- antv-g2 —— AntV G2 — 把 Grammar of Graphics 写成 JavaScript
- antv-g6 —— AntV G6 — 把”关系数据”画成会自己摆位置的图
- antv-x6 —— AntV X6 — 把 mxGraph 的图编辑思路搬到 TypeScript
- apexcharts —— ApexCharts — 自带响应式与注解的 SVG 图表库
- babylonjs —— Babylon.js — 微软开源的企业级 Web 3D 引擎
- billboard-js —— billboard.js — c3.js 的 TypeScript 继任者
- bokeh —— Bokeh — 浏览器端交互式 Python 图,可挂 Server 做实时数据流
- cesium —— CesiumJS — 浏览器里的三维地球与时间动画
- chart-js —— Chart.js — Canvas 渲染入门级图表
- chartist —— Chartist — 极简 SVG 图表
- cytoscape-js —— Cytoscape.js — 浏览器里画图(节点 + 边)的图论库
- dhtmlx-gantt —— DHTMLX Gantt — 给企业级排期用的全功能甘特组件
- echarts —— Apache ECharts — 给一个 JSON 就能画图的可视化库
- fabric-js —— Fabric.js — 给 Canvas 加一层”对象模型”,让画布图形可以拖
- flowchart-js —— flowchart.js — 文本生成流程图
- framer-motion —— Framer Motion — React 声明式动画
- frappe-gantt —— Frappe Gantt — 200 行 SVG 写出的甘特图
- glide-data-grid —— glide-data-grid — Canvas 画出来的百万行表格
- graphology —— Graphology — 浏览器里的图数据结构与算法库
- gsap —— GSAP — GreenSock 高性能动画
- handsontable —— Handsontable — 浏览器里的 Excel
- i18next —— i18next — 让一份 JS 代码同时讲几十种语言
- kepler-gl —— kepler.gl — 拖拽式百万点 GIS 探索界面
- konva —— Konva — 给 HTML5 Canvas 装一棵会响应的节点树
- ky —— ky — 把浏览器自带的 fetch 包成顺手工具
- leaflet —— Leaflet — 轻量交互式地图
- mapbox-gl-js —— Mapbox GL JS — 矢量瓦片 + WebGL 客户端渲染地图
- maplibre-gl —— MapLibre GL JS — Mapbox v1 时代的社区分叉
- mermaid —— Mermaid — 用文本写图,code review 友好的图表语言
- motion-one —— Motion One — 把动画交给浏览器自己跑
- observable-framework —— Observable Framework — 编译期跑数据,浏览器只看结果
- observable-plot —— Observable Plot — 你说想看哪两列的关系,库自己画图
- openlayers —— OpenLayers — 全功能 GIS 前端
- pixi —— PixiJS — 浏览器里画 2D 的高性能 GPU 引擎
- playcanvas —— PlayCanvas — 浏览器里跑的 3D 游戏引擎
- playwright —— Playwright — 跨浏览器自动化测试
- plotly-js —— Plotly.js — 一个 JSON 描述任何图表的浏览器全家桶
- react-hook-form —— react-hook-form — input 不进 React state 也能写表单
- react-spring —— react-spring — 用真实弹簧的物理写网页动画
- recharts —— Recharts — 用 JSX 直接拼出图表的 React 组件库
- regl —— regl — 函数式 WebGL 封装
- sigma-js —— Sigma.js — 上万节点仍流畅的 WebGL 图渲染器
- timelinejs —— TimelineJS — 把 Google Sheet 一键变成新闻时间线
- vega —— Vega — 整张图就是一棵 JSON
- vega-lite —— Vega-Lite — 用 JSON 三段式画复合图
- vis-network —— vis-network — barnesHut 物理引擎驱动的网络图
- vis-timeline —— vis-timeline — 时间轴 / 日程 / 历史事件三合一组件
- visx —— visx — 把 d3 拆成 30 块乐高的 React 可视化原语
- zod —— Zod — TypeScript-first schema 验证