D3 v7 快速上手:CDN、npm 与 React/Svelte 集成的完整接入指南
D3(Data-Driven Documents)是一个运行在任何 JavaScript 环境中的数据可视化底层库。本篇基于官方入门文档 getting-started.md 与仓库源码,带你完整走通 D3 v7 的四种接入方式(在线环境、原生 HTML、npm、React/Svelte),并解释每种方式背后的模块组织与打包机制,读完即可在自己的项目中复制一套可运行的空白图表骨架。
一、D3 的运行形态:30 个子模块的聚合层
理解 D3 接入方式之前,先理解 D3 包本身的结构。从 package.json 可以看到,当前仓库版本为 7.9.0("type": "module",要求 Node >= 12),dependencies 中声明了 30 个子模块:d3-array、d3-axis、d3-brush、d3-chord、d3-color、d3-contour、d3-delaunay、d3-dispatch、d3-drag、d3-dsv、d3-ease、d3-fetch、d3-force、d3-format、d3-geo、d3-hierarchy、d3-interpolate、d3-path、d3-polygon、d3-quadtree、d3-random、d3-scale、d3-scale-chromatic、d3-selection、d3-shape、d3-time、d3-time-format、d3-timer、d3-transition、d3-zoom。
D3 主包并不重复实现这些功能,src/index.js 仅由 30 行 export * from 组成,把每个子模块的全部导出平铺到 d3 命名空间下;bundle.js 再补上从 package.json 导出的 version 字段。这也解释了为什么入门文档中 d3.scaleUtc()、d3.axisBottom()、d3.line() 都能直接从 d3 根对象访问。
测试用例 test/d3-test.js 用一条自动化断言守住了这一契约:遍历 package.json 的 dependencies,动态 import 每个子模块,并断言其每个导出(除 version 外)都出现在 d3 命名空间中——从源码结构看,D3 主包的角色就是“全量子模块的再导出(re-export)层”,而不是独立的功能实现。
这个结构直接决定了后文的三种加载策略:可以整包引入(获得全部 30 个子模块的符号),也可以只从 CDN 或 npm 单独引入某个子模块。
二、第一张图:比例尺 + 坐标轴的空白图表骨架
无论使用哪种加载方式,入门文档给出的第一个示例都是同一个:用 d3.create 创建一个 640×400 的 SVG 容器,声明 x/y 两个比例尺,再用 d3.axisBottom / d3.axisLeft 挂上坐标轴,得到一个可以填充数据的空白图表。这也是文档内交互组件 ExampleBlankChart.vue 中实际渲染的代码。
核心代码(Observable 单元形式,完整继承自原文档):
{
// Declare the chart dimensions and margins.
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// Declare the x (horizontal position) scale.
const x = d3.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// Add the y-axis.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
// Return the SVG element.
return svg.node();
}
几个值得注意的细节:
- 边距约定(margin convention):x 轴的范围是
[marginLeft, width - marginRight],y 轴是[height - marginBottom, marginTop](注意 y 轴起点在下、终点在上,符合 SVG 坐标系),坐标轴<g>通过transform="translate(...)"平移到边距边界上; d3.create("svg")创建的是一个尚未插入文档的 SVG 元素,需要显式返回(Observable)或container.append(svg.node())(原生 HTML);- 该示例同时涉及三个子模块:d3-scale(
scaleUtc/scaleLinear)、d3-selection(create/append/call)、d3-axis(axisBottom/axisLeft)。
2.1 在线体验:Observable 环境
官方文档推荐的入门路径是在 Observable 笔记本中使用:D3 作为 Observable 标准库的一部分默认可用,只需让单元返回生成的 DOM 元素即可渲染。除了上面的空白图表,文档列出了五个可 fork 的入门模板:面积图(Area chart)、柱状图(Bar chart)、环形图(Donut chart)、直方图(Histogram)、折线图(Line chart),并在 Observable 的 D3 gallery 中收录了数百个可 fork 的笔记本作为起步参考。点击 + 新建单元并输入 “d3” 可以过滤出内置的 D3 片段,Observable 还提供样例数据集、CSV/JSON 上传等便利功能供练习。
2.2 原生 HTML:三种加载方式
在原生 HTML 页面中,文档给出三种等价写法,示例逻辑与上面的空白图表完全一致,区别仅在 D3 的引入方式。
方式一:ESM + CDN(文档推荐)
<!DOCTYPE html>
<div id="container"></div>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
// Declare the chart dimensions and margins.
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// Declare the x (horizontal position) scale.
const x = d3.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// Add the y-axis.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
// Append the SVG element.
container.append(svg.node());
</script>
方式二:UMD + CDN
UMD 包以普通 <script> 加载时会挂出全局 d3 对象,适合无法使用 ES 模块的旧环境:
<!DOCTYPE html>
<div id="container"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script type="module">
// Declare the chart dimensions and margins.
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// Declare the x (horizontal position) scale.
const x = d3.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// Add the y-axis.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
// Append the SVG element.
container.append(svg.node());
</script>
方式三:UMD + 本地文件(离线场景)
<!DOCTYPE html>
<div id="container"></div>
<script src="d3.js"></script>
<script type="module">
// Declare the chart dimensions and margins.
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// Declare the x (horizontal position) scale.
const x = d3.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// Add the y-axis.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
// Append the SVG element.
container.append(svg.node());
</script>
说明:官方文档页面上的
d3.v7.js/d3.v7.min.js下载链接指向由构建流程生成的 UMD 包——prebuild.sh 会在文档构建时把dist/d3.js和dist/d3.min.js复制为docs/public/d3.v7.js/docs/public/d3.v7.min.js,因此这两个文件并不直接提交在源码树中。调试时使用非压缩版,生产环境使用压缩版以获得更快的加载性能。
UMD 全局 d3 从何而来? 从 rollup.config.js 可以看到,构建产物 dist/d3.js 采用 format: "umd"、全局变量名为 d3,并附加了版本与版权 banner;同一配置还额外产出 dist/d3.mjs(ESM 格式)和 dist/d3.min.js(经 terser 压缩)。package.json 中的 files 字段确认发布包携带 dist/d3.js 与 dist/d3.min.js,jsdelivr / unpkg 字段与 exports["umd"] 都指向 dist/d3.min.js——这就是上述 CDN 与 UMD 加载方式背后的产物。
2.3 只加载需要的子模块
如果只需要力导向图,不必引入整个 d3 聚合包,可以直接从 CDN 按需导入单个子模块的具名导出:
<script type="module">
import {forceSimulation, forceCollide, forceX} from "https://cdn.jsdelivr.net/npm/d3-force@3/+esm";
const nodes = [{}, {}];
const simulation = forceSimulation(nodes)
.force("x", forceX())
.force("collide", forceCollide(5))
.on("tick", () => console.log(nodes[0].x));
</script>
注意这里导入的是独立的 d3-force@3 包而不是 d3@7,子模块在 npm 上按各自的 v3 线发布(与主包 v7 并存),各子模块的最低版本约束以 package.json 中的 dependencies 为准。
三、从 npm 安装
如果你的应用基于 Node 构建(Vite、webpack 等打包环境),用任意包管理器安装整包:
# yarn
yarn add d3
# npm
npm install d3
# pnpm
pnpm add d3
安装后有三种导入粒度:
// 1. 整体导入:获得 30 个子模块的全部符号(最常见)
import * as d3 from "d3";
// 2. 具名导入:只取需要的符号,便于打包器摇树
import {select, selectAll} from "d3";
// 3. 子模块直连:直接依赖 d3-array 等独立包
import {mean, median} from "d3-array";
从源码结构看,package.json 将 main / module 都指向 src/index.js,所以包管理器解析到的入口正是那个 30 行再导出文件,三种导入方式最终拿到的是同一套符号表。TypeScript 类型声明由社区维护的 DefinitelyTyped 库提供(@types/d3),文档未将其内置于包中,按需安装即可。
四、D3 在 React 中
D3 的模块大致分两类:
- 不触碰 DOM 的纯计算模块(d3-scale、d3-array、d3-interpolate、d3-format 等)——在 React 中与普通模块无差别,可以直接在 JSX 里做纯声明式渲染;
- 操作 selection 的模块(d3-selection、d3-transition、d3-axis)——直接改写真实 DOM,会与 React 的虚拟 DOM 冲突,需要借助 ref +
useEffect把 D3 的写入限制在 React 不管理的节点内。
模式一:纯声明式(无 DOM 操作)。下面这个折线图组件只用了比例尺与 d3-shape 的 line,SVG 元素全部由 React 渲染:
import * as d3 from "d3";
export default function LinePlot({
data,
width = 640,
height = 400,
marginTop = 20,
marginRight = 20,
marginBottom = 20,
marginLeft = 20
}) {
const x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
const y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
const line = d3.line((d, i) => x(i), y);
return (
<svg width={width} height={height}>
<path fill="none" stroke="currentColor" strokeWidth="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" strokeWidth="1.5">
{data.map((d, i) => (<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />))}
</g>
</svg>
);
}
模式二:ref + useEffect(需要 DOM 操作的坐标轴)。给两个 <g> 挂 ref,在 effect 里把 D3 选集交给坐标轴:
import * as d3 from "d3";
import {useRef, useEffect} from "react";
export default function LinePlot({
data,
width = 640,
height = 400,
marginTop = 20,
marginRight = 20,
marginBottom = 30,
marginLeft = 40
}) {
const gx = useRef();
const gy = useRef();
const x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
const y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
const line = d3.line((d, i) => x(i), y);
useEffect(() => void d3.select(gx.current).call(d3.axisBottom(x)), [gx, x]);
useEffect(() => void d3.select(gy.current).call(d3.axisLeft(y)), [gy, y]);
return (
<svg width={width} height={height}>
<g ref={gx} transform={`translate(0,${height - marginBottom})`} />
<g ref={gy} transform={`translate(${marginLeft},0)`} />
<path fill="none" stroke="currentColor" strokeWidth="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" strokeWidth="1.5">
{data.map((d, i) => (<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />))}
</g>
</svg>
);
}
关键点:useEffect 的依赖数组传入 [gx, x] / [gy, y],数据变化导致比例尺重建时坐标轴随之重绘;而 <path> 与 <circle> 仍由 React 声明,D3 只负责 d3.axisBottom / d3.axisLeft 内部生成的刻度与标签——两者各管各的 DOM 子树,互不覆盖。
五、D3 在 Svelte 中
Svelte 的策略与 React 相同:优先只用不操作 DOM 的模块做纯渲染,需要 DOM 操作时再借 bind:this 把节点交给 D3。
模式一:纯声明式折线图(使用 d3-shape 与 d3-scale):
<script>
import * as d3 from 'd3';
export let data;
export let width = 640;
export let height = 400;
export let marginTop = 20;
export let marginRight = 20;
export let marginBottom = 20;
export let marginLeft = 20;
$: x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
$: y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
$: line = d3.line((d, i) => x(i), y);
</script>
<svg width={width} height={height}>
<path fill="none" stroke="currentColor" stroke-width="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" stroke-width="1.5">
{#each data as d, i}
<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />
{/each}
</g>
</svg>
模式二:响应式语句驱动动态坐标轴。Svelte 的 $: 响应式语句与 D3 的数据联结(data join)天然契合——数据一变,语句重算,坐标轴自动更新:
<script>
import * as d3 from 'd3';
export let data;
export let width = 640;
export let height = 400;
export let marginTop = 20;
export let marginRight = 20;
export let marginBottom = 30;
export let marginLeft = 40;
let gx;
let gy;
$: x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
$: y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
$: line = d3.line((d, i) => x(i), y);
$: d3.select(gy).call(d3.axisLeft(y));
$: d3.select(gx).call(d3.axisBottom(x));
</script>
<svg width={width} height={height}>
<g bind:this={gx} transform={`translate(0,${height - marginBottom})`} />
<g bind:this={gy} transform={`translate(${marginLeft},0)`} />
<path fill="none" stroke="currentColor" stroke-width="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" stroke-width="1.5">
{#each data as d, i}
<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />
{/each}
</g>
</svg>
与 React 版对比:Svelte 不需要 useEffect 来声明副作用边界,bind:this + $: 语句本身就承担了这个职责,因此框架内使用 D3 的“DOM 操作型”模块时,Svelte 的样板代码更少。
六、选型小结与后续路径
| 场景 | 推荐方式 | 依据 |
|---|---|---|
| 快速试验、教学演示 | Observable 在线笔记本 | D3 默认内置于其标准库,单元返回 DOM 即渲染 |
| 静态页面、一次性嵌入 | ESM + CDN(d3@7/+esm) |
官方推荐;无构建步骤,<script type="module"> 直用 |
| 旧环境 / 无模块支持 | UMD + CDN(全局 d3) |
dist/d3.js 为 UMD 格式,加载即挂全局 |
| 离线 / 内网环境 | 本地 UMD 文件 | 非压缩版调试、压缩版生产 |
| Node 构建应用 | npm install d3 + ES 导入 |
入口即 src/index.js 再导出层 |
| React / Svelte | 纯计算模块声明式渲染;selection 类模块走 ref / bind:this |
避免 D3 与虚拟 DOM 争抢节点 |
入门之后,可按主题沿仓库文档深入:选择与数据联结见 d3-selection/selecting.md 与 d3-selection/joining.md,比例尺体系见 d3-scale.md 及其下的 band/linear/ordinal 等专题文档,图形生成器见 d3-shape/line.md 等;完整 API 总览可参考 API.md。由于 D3 v7 是纯 ESM 发布(type: "module"),若你的运行环境不支持原生 ES 模块或 import 语法,需要借助 UMD 包或自行使用打包工具做转译——这是选择加载方式时最核心的兼容性前提。
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 StartedRust0625
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