读懂 D3:Data-Driven Documents 的低层工具箱设计哲学与工程实现
D3(Data-Driven Documents,数据驱动文档)是一个基于 Web 标准构建的免费开源 JavaScript 数据可视化库。本文以官方文档 docs/what-is-d3.md 为主线,完整梳理"D3 是什么、为什么是低层工具箱、为什么面向动态可视化"这五个核心命题,并结合当前仓库的 package.json、src/index.js、rollup.config.js 与测试源码,说明 D3 7.x 版本(当前为 7.9.0)的 30 个模块如何组织、构建与分发,读完你能建立起从概念到源码层面的 D3 全局认知。
D3 是什么
D3(或 D3.js) 是一个用于数据可视化的免费、开源 JavaScript 库。它采用基于 Web 标准的低层设计,为创作动态、数据驱动的图形提供了极大的灵活性。D3 十多年来支撑了大量具有开创性、屡获殊荣的可视化作品,成为更高层级图表库的基础构件,并在全球范围内培育了活跃的数据实践者社区。这一点在 README.md 的开头段落中有同样的表述。
D3 的历史可以追溯到 2011 年,由 Mike Bostock 创建;他与 Jeff Heer、Vadim Ogievetsky 在斯坦福大学共同发表了 D3 学术论文。Jason Davies 在 2011 至 2013 年间对 D3 贡献巨大,尤其是地理投影系统部分;Philippe Rivière 自 2016 年起成为 D3 及其文档的主要贡献者。如今 Mike Bostock 与 Philippe Rivière 共同维护 D3 及其姊妹库 Observable Plot。社区相关的更多背景可以参考 docs/community.md。
从当前仓库 package.json 可以看到几个关键事实:
- 包名为
d3,描述为 "Data-Driven Documents",当前版本 7.9.0; - 采用 ISC 许可证,作者为 Mike Bostock;
- 声明
"type": "module",即 D3 本体是 ES Module,且exports字段同时提供 UMD 入口(./dist/d3.min.js)与默认 ESM 入口(./src/index.js),这意味着 D3 同时面向<script>全局加载和现代模块系统两种使用方式; - 运行环境要求 Node
>=12。
D3 是一个低层工具箱,而不是图表库
官方文档开宗明义:D3 不是传统意义上的图表库,它没有"图表(chart)"的概念。用 D3 可视化数据时,你实际上是在组合一组原语(primitives)。
以制作一张堆叠面积图(stacked area chart)为例,涉及的原语组合包括:
| 原语 | 用途 | 对应文档 |
|---|---|---|
| CSV 解析器 | 加载数据 | docs/d3-dsv.md |
| 时间比例尺(time scale) | 水平位置 x | docs/d3-scale/time.md |
| 线性比例尺(linear scale) | 垂直位置 y | docs/d3-scale/linear.md |
| 序数比例尺 + 分类色板 | 颜色编码 | docs/d3-scale/ordinal.md、docs/d3-scale-chromatic/categorical.md |
| 堆叠布局(stack layout) | 排列数值 | docs/d3-shape/stack.md |
| 面积形状 + 线性曲线 | 生成 SVG path 数据 | docs/d3-shape/area.md、docs/d3-shape/curve.md |
| 坐标轴(axes) | 说明位置编码 | docs/d3-axis.md |
| 选择(selections) | 创建 SVG 元素 | docs/d3-selection.md |
一次性消化这些内容确实不少,但每个部件都可以独立使用,你可以逐个学习后再拼装到一起。D3 不是单一的整体(monolith),而是一套 30 个相互独立的库(模块)组成的工具箱;把它们捆绑在一起发行只是为了方便,而非技术上的必要。
从源码验证"30 个模块"
打开 src/index.js,整个文件只有 30 行再导出语句,逐一对应 30 个子模块:
export * from "d3-array";
export * from "d3-axis";
export * from "d3-brush";
// ... 共 30 行 ...
export * from "d3-transition";
export * from "d3-zoom";
这些再导出与 package.json 中 dependencies 列出的 30 个包完全对应,每个子包都有独立的语义版本范围(例如 d3-array: ^3.2.4、d3-scale: ^4.0.2、d3-delaunay: ^6.0.4)。也就是说,d3 包本身只是一个"聚合壳",真正的实现分散在各子模块仓库中。
这个契约还有测试保障。test/d3-test.js 遍历 package.json 的每一个依赖模块,动态导入后断言该模块的每个导出(除 version 外)都存在于 d3 命名空间下:
for (const moduleName in packageData.dependencies) {
it(`d3 exports everything from ${moduleName}`, async () => {
const module = await import(moduleName);
for (const propertyName in module) {
if (propertyName !== "version") {
assert(propertyName in d3, `${moduleName} exports ${propertyName}`);
}
}
});
}
这解释了为什么你可以既写 import * as d3 from "d3",也可以只装子包按需引入,如 import {mean, median} from "d3-array"(见 docs/getting-started.md)。
另外值得注意的是,本仓库文档站本身就是 D3 用法的实证:docs/what-is-d3.md 的脚本部分使用 d3.tree().nodeSize([1, 1]) 与 d3.stratify().path(...) 把 VitePress 侧边栏构建成一棵树,再用 Plot.tree 渲染出 D3 文档结构的树状导航图,是"层级数据 + 树布局"的典型小型应用。
官方文档同时给出了一条务实建议:除非你确实需要 D3 的低层控制能力,否则可以考虑其高层姊妹库 Observable Plot——同一个直方图,D3 可能需要 50 行代码,Plot 一行即可完成;两者也可以组合使用。
D3 是灵活的
正因为 D3 没有上层"图表"抽象,即使是一张基础图表也可能需要几十行代码。但反过来看,所有部件都摆在你面前,你对发生的一切拥有完全的控制权,可以把可视化精确调整到想要的样子。D3 对你的数据没有任何默认表现——最终呈现的只是你自己写出的代码(或从示例中复制来的代码)。
官方文档把 D3 定位为"自己动手做一切的替代方案",而不是高层图表库的替代品:如果你不满意现有工具、正打算用 SVG、Canvas(甚至 WebGL)自己画图表,那么 D3 工具箱里几乎肯定有能帮到你、又不束缚创造力的现成部件。
D3 与 Web 标准协同工作
D3 不引入任何新的图形表示方式,而是直接配合 SVG、Canvas 等 Web 标准使用。"D3"这个名字是 data-driven documents 的缩写,其中 documents 指表示网页内容的 DOM(Document Object Model)标准。
从源码结构看,D3 的 30 个模块可以粗分为两类:
- 直接操作 DOM 的模块:d3-selection、d3-transition、d3-axis、d3-brush、d3-drag、d3-zoom 等;
- 只操作数据的模块:d3-scale、d3-shape、d3-array、d3-force、d3-hierarchy、d3-geo 等,它们不触碰 DOM,因此在任何 JavaScript 环境(包括 React 组件渲染期)中都可以安全使用。
拥抱 Web 标准带来诸多好处:
- 可以用外部样式表改变图表外观,甚至响应媒体查询(响应式图表、深色模式);
- 可以用浏览器的调试器和元素检查器审查代码行为;
- D3 是同步、命令式(synchronous, imperative)的求值模型——调用
selection.attr会立即修改 DOM——相比带复杂异步运行时(如虚拟 DOM diff)的框架,往往更容易调试。
D3 也可以与 React、Vue、Svelte 等框架配合使用,docs/getting-started.md 给出了完整建议:不操作 DOM 的模块(如 d3-scale、d3-array)可以在框架里以纯声明式方式使用;而操作选择的模块(d3-selection、d3-transition、d3-axis)会与框架的虚拟 DOM 竞争,需要借助 useRef/useEffect(React)或响应式语句(Svelte)把真实 DOM 节点交给 D3 管理。
D3 面向定制(bespoke)可视化
D3 让事情变得"可行",但不一定"容易"——即便是本该简单的事情,往往也不简单。借用 Amanda Cox 的话:"如果你认为为一根柱状图写一百行代码是完全正常的,那就用 D3。"
- 适合:需要最大表达力的定制可视化。例如《纽约时报》《The Pudding》这类媒体机构,单张图形可能面向百万读者,且有一支编辑团队共同推进视觉表达的边界;
- 不适合:临时搭建内部仪表盘或一次性分析。不要被炫技级示例带偏——其中很多实现了巨大工程量。如果你的时间有限,用更高层的工具(如 Observable Plot)往往能得到更好的可视化或分析结果。
D3 面向动态可视化:data join
D3 最具创新性的概念是数据连接(data join):给定一组数据和一组 DOM 元素,data join 允许你为 entering(进入)、updating(更新)、exiting(退出)三种状态的元素分别执行不同操作。如果你只做静态图表(不动画、不响应交互),这个概念可能显得费解甚至奇怪——因为静态场景确实用不到它。
data join 存在的意义在于:让你精确控制数据变化时发生什么,并据此更新显示。这种直接控制带来两个好处——极高的更新性能(只触碰需要变化的元素和属性,无需对 DOM 做 diff)以及状态之间平滑的动画过渡。D3 在动态、交互式可视化领域表现出色。
最小可运行示例:用 join 生成表格
docs/d3-selection/joining.md 给出了经典的最小示例:把一个数字矩阵渲染成 HTML 表格,其中数据函数是恒等函数(每行返回矩阵中对应的行):
const matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
d3.select("body")
.append("table")
.selectAll("tr")
.data(matrix)
.join("tr")
.selectAll("td")
.data(d => d)
.join("td")
.text(d => d);
几个关键机制(均来自 docs/d3-selection/joining.md):
selection.data(data, key)把数据绑定到所选元素,返回 update 选择,并在其上定义 enter 与 exit 选择。数据被赋值给元素后存储在__data__属性上,因此数据是"粘性的",重新选择时依然可用;- key 函数:默认按索引一一对应;指定 key 函数后按字符串标识匹配,重复 key 的元素进入 exit 选择、重复 key 的数据进入 enter 选择;
selection.join(enter, update, exit)是"通用更新模式"(general update pattern)的便捷替代,字符串简写.join("circle")等价于enter => enter.append("circle")+update => update+exit => exit.remove()。
对动态数据场景,推荐用 key 函数最小化 DOM 变动。例如为不同数据项指定稳定的 name 作为 key:
const data = [
{name: "Locke", number: 4},
{name: "Reyes", number: 8},
{name: "Ford", number: 15},
// ...
];
d3.selectAll("div")
.data(data, function(d) { return d ? d.name : this.id; })
.text(d => d.number);
此外,join 的三个回调内还可以创建 transition,从而对 enter/update/exit 三个阶段分别做动画,这是 D3 动态可视化"精确控制每个阶段"的又一延伸点,详见 docs/d3-transition.md。
工程实现:D3 7.x 如何构建与分发
理解了概念后,可以再看一眼当前仓库如何把 30 个模块打包成发布产物,这直接决定了你在项目中"怎么装 D3"。
rollup.config.js 以 bundle.js 为入口,输出三种产物到 dist/:
- UMD 版
d3.js:format: "umd"、全局名d3,供<script>标签直接加载(对应package.json中的exports."umd"与unpkg/jsdelivr字段); - ESM 版
d3.mjs:format: "esm",供现代打包器使用; - 压缩版
d3.min.js:额外挂接 terser 插件(保留InternMap、InternSet标识符不被混淆)。
package.json 的 files 字段只发布 dist/d3.js、dist/d3.min.js 与 src/**/*.js,prepublishOnly 脚本在发布前会重新执行 rollup -c 生成这三份产物。发布后,npm install d3 得到的是 ESM 入口(./src/index.js),即上一节那 30 行再导出;而文档站的 prebuild.sh 会把构建好的 dist/d3.js 与 dist/d3.min.js 复制为 docs/public/d3.v7.js / d3.v7.min.js 供页面本地加载——这就是 docs/getting-started.md 中"UMD + local"用法(本地 d3.js 脚本标签)的产物来源。
一个可复制的最小起点
docs/getting-started.md 提供了空白图表模板,完整体现了"声明比例尺 → 创建 SVG → 调用坐标轴"的标准套路(以 ESM + CDN 为例):
<!DOCTYPE html>
<div id="container"></div>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
// 声明图表尺寸与边距。
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// 声明 x(水平位置)比例尺。
const x = d3.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// 声明 y(垂直位置)比例尺。
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// 创建 SVG 容器。
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// 添加 x 轴。
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// 添加 y 轴。
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
// 追加 SVG 元素。
container.append(svg.node());
</script>
在 Node 项目中则通过包管理器安装:npm install d3 / yarn add d3 / pnpm add d3,然后 import * as d3 from "d3";也可以按符号引入(import {select, selectAll} from "d3")或直接安装子模块(import {mean, median} from "d3-array")。
小结
回到 docs/what-is-d3.md 的核心命题:
- D3 是低层工具箱——30 个可独立使用的模块(见 src/index.js),通过组合原语而非调用"图表 API"完成可视化;
- D3 是灵活的——没有默认表现层,所有部件对你透明,适合替代"纯手工"的 SVG/Canvas 开发;
- D3 与 Web 协同——直接作用于 DOM 与 SVG/Canvas,同步命令式模型便于调试,可与 React、Vue、Svelte 配合;
- D3 面向定制——表达力换代码量,适合高价值、可精雕细琢的作品,对一次性分析则偏重;
- D3 面向动态——data join 的 enter/update/exit 三阶段模型是精确、高性能、可动画更新的核心机制。
掌握了这五点,再配合各模块文档(docs/d3-*.md)与官方示例,就可以按需取用工具箱中的具体部件,而不是被"学完整个 D3"的压力吓退。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00