跳转到内容

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 表面可归结为 三件套

  1. Selection(选择)d3.select(".foo") 像 jQuery 的 $() 但加了”数据绑定”——每个 DOM 节点身上挂一个数据元 node.__data__。所有 attr / style / on 都是链式调用。

  2. Scale(标尺)scaleLinear / scaleTime / scaleBand域到值域的函数。类比体温计:水银柱长度(数据 36-40°C)映射到刻度位置(像素 0-800)。13 种 scale 覆盖线性 / 对数 / 时间 / 序数。

  3. 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 用。两套系统井水不犯河水。

踩过的坑

  1. 不传 key function:默认 d3 按数组下标配对节点。数组中间插入元素时,所有后续节点错位重绑,动画乱跳。修:.data(arr, d => d.id),传 key function。一旦遇过一次就再也不会忘。

  2. import * as d3 让 tree-shake 失效:拉全量 ~250KB。子包按需 import 能压到 30KB,但 webpack/vite 默认配置经常 tree-shake 不动,建议显式 import { scaleLinear } from "d3-scale" 而不是 from "d3"

  3. 直接和 React 同操作 DOM:d3 偷偷加节点,React 下次 render 看不见就抹掉。要么 React 只渲染空容器、d3 接管内部(牺牲 SSR),要么走案例 3 的 helper 路线。两套系统不共用同一片 DOM 是底线。

  4. 没有现成 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% 用户太低层

学到什么

  1. 可视化不是图表,是数据 → 像素的映射——d3 把这个映射拆成三件套(select / scale / shape)让你自由组合
  2. API 设计可以用”函数即对象”——x(50)x.domain([...]) 同一个东西,链式 setter 让配置和调用统一
  3. state 可以挂在 DOM 上node.__data__ 是 d3 的核心 trick——不要外部 Map,下次 select 同一节点直接拿到数据
  4. 乐高底层 vs 整装产品是两个市场:d3 永远不会自带”Chart”,那是 Plot / Recharts 的活
  5. early adopter 的代价:d3 设计于 2011,没赶上 React/Vue 时代——和现代框架共存是这类”早期 DOM 直写库”共同的迁移成本

延伸阅读

关联

  • 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 验证