首页
/ D3 v7 快速上手:CDN、npm 与 React/Svelte 集成的完整接入指南

D3 v7 快速上手:CDN、npm 与 React/Svelte 集成的完整接入指南

2026-09-06 14:07:50作者:段琳惟

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-arrayd3-axisd3-brushd3-chordd3-colord3-contourd3-delaunayd3-dispatchd3-dragd3-dsvd3-eased3-fetchd3-forced3-formatd3-geod3-hierarchyd3-interpolated3-pathd3-polygond3-quadtreed3-randomd3-scaled3-scale-chromaticd3-selectiond3-shaped3-timed3-time-formatd3-timerd3-transitiond3-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.jsondependencies,动态 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-scalescaleUtc/scaleLinear)、d3-selectioncreate/append/call)、d3-axisaxisBottom/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.jsdist/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.jsdist/d3.min.jsjsdelivr / 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.jsonmain / module 都指向 src/index.js,所以包管理器解析到的入口正是那个 30 行再导出文件,三种导入方式最终拿到的是同一套符号表。TypeScript 类型声明由社区维护的 DefinitelyTyped 库提供(@types/d3),文档未将其内置于包中,按需安装即可。

四、D3 在 React 中

D3 的模块大致分两类:

  • 不触碰 DOM 的纯计算模块d3-scaled3-arrayd3-interpolated3-format 等)——在 React 中与普通模块无差别,可以直接在 JSX 里做纯声明式渲染;
  • 操作 selection 的模块d3-selectiond3-transitiond3-axis)——直接改写真实 DOM,会与 React 的虚拟 DOM 冲突,需要借助 ref + useEffect 把 D3 的写入限制在 React 不管理的节点内。

模式一:纯声明式(无 DOM 操作)。下面这个折线图组件只用了比例尺与 d3-shapeline,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-shaped3-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.mdd3-selection/joining.md,比例尺体系见 d3-scale.md 及其下的 band/linear/ordinal 等专题文档,图形生成器见 d3-shape/line.md 等;完整 API 总览可参考 API.md。由于 D3 v7 是纯 ESM 发布(type: "module"),若你的运行环境不支持原生 ES 模块或 import 语法,需要借助 UMD 包或自行使用打包工具做转译——这是选择加载方式时最核心的兼容性前提。

登录后查看全文
热门项目推荐
相关项目推荐