- D3 通过 select/selectAll 操作 DOM,用 data() 将数组绑定到元素
- enter/exit/join 模式处理新增、更新和删除的数据元素
- 比例尺(scales)是核心:scaleLinear 用于连续数据,scaleBand 用于类别
- 使用 transition() 和 duration() 添加平滑动画效果
- 在 React 中使用 useRef 获取容器引用,useEffect 中运行 D3 代码
- SVG viewBox 属性实现响应式图表,配合 ResizeObserver 动态调整
- D3-geo 支持 GeoJSON 地图,d3-force 支持力导向网络图
1. 什么是 D3.js
D3.js(Data-Driven Documents)是一个用于创建数据驱动的交互式可视化的 JavaScript 库。由 Mike Bostock 于 2011 年创建,它不是一个图表库——而是一个低级别的可视化工具集,让你完全控制从数据到像素的每一步转换。D3 使用 SVG、HTML 和 CSS 渲染可视化,在所有现代浏览器中都能运行。
与 Chart.js 或 Recharts 等高级图表库不同,D3 不提供预制的图表组件。它提供的是构建任何可视化所需的基本工具:DOM 操作、数据绑定、比例尺、形状生成器、布局算法和过渡动画。这种灵活性使 D3 成为 New York Times、Observable 和数千个数据可视化项目的首选。
2. 安装与设置
通过 npm 安装
npm install d3
# or
yarn add d3
# TypeScript types
npm install @types/d3 --save-dev导入方式
// Import everything
import * as d3 from "d3";
// Import only what you need (smaller bundle)
import { select, selectAll } from "d3-selection";
import { scaleLinear, scaleBand } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis";
import { line, area, pie, arc } from "d3-shape";
import { transition } from "d3-transition";CDN 方式
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script>
// d3 is available as a global
d3.select("body").append("h1").text("Hello D3!");
</script>3. 选择集:select 与 selectAll
选择集是 D3 的核心。d3.select() 选择第一个匹配的 DOM 元素,d3.selectAll() 选择所有匹配元素。选择集支持链式调用来修改属性、样式和内容。
// Select a single element
const svg = d3.select("#chart");
// Select all matching elements
const circles = d3.selectAll("circle");
// Chain attribute and style modifications
d3.selectAll("rect")
.attr("width", 50)
.attr("height", 30)
.attr("fill", "#3b82f6")
.style("opacity", 0.8);
// Append new elements
d3.select("#chart")
.append("svg")
.attr("width", 600)
.attr("height", 400)
.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 40)
.attr("fill", "tomato");4. 数据绑定:data / enter / exit / join
数据绑定是 D3 最强大的特性。data() 方法将数组与 DOM 元素关联。当数据项多于元素时,enter() 处理新增项;当元素多于数据时,exit() 处理删除项;update 选择集处理已有元素的更新。
经典 enter/exit 模式
const data = [10, 20, 35, 50, 25];
// Select all rects (initially none exist)
const bars = svg.selectAll("rect")
.data(data);
// Enter: create new elements for new data
bars.enter()
.append("rect")
.attr("x", (d, i) => i * 60)
.attr("y", (d) => 200 - d * 3)
.attr("width", 50)
.attr("height", (d) => d * 3)
.attr("fill", "#3b82f6");
// Exit: remove elements without data
bars.exit().remove();现代 join 方法(D3 v5+)
const data = [10, 20, 35, 50, 25];
// join() handles enter, update, and exit in one call
svg.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => i * 60)
.attr("y", (d) => 200 - d * 3)
.attr("width", 50)
.attr("height", (d) => d * 3)
.attr("fill", "#3b82f6");
// join() with enter/update/exit callbacks
svg.selectAll("rect")
.data(data)
.join(
(enter) => enter.append("rect")
.attr("fill", "green"),
(update) => update
.attr("fill", "blue"),
(exit) => exit
.attr("fill", "red")
.transition().duration(500)
.attr("opacity", 0)
.remove()
);5. 比例尺(Scales)
比例尺将数据值(domain)映射到视觉值(range),如像素位置、颜色或大小。它们是构建图表的核心工具。
线性比例尺 (scaleLinear)
// Map data 0-100 to pixels 0-500
const xScale = d3.scaleLinear()
.domain([0, 100]) // data range
.range([0, 500]); // pixel range
xScale(50); // 250
xScale(100); // 500
xScale.invert(250); // 50 (reverse lookup)序数比例尺 (scaleBand)
// Map categories to equal-width bands
const xScale = d3.scaleBand()
.domain(["Mon", "Tue", "Wed", "Thu", "Fri"])
.range([0, 500])
.padding(0.2); // gap between bands
xScale("Wed"); // 200 (pixel position)
xScale.bandwidth(); // 80 (width of each band)时间比例尺与对数比例尺
// Time scale
const timeScale = d3.scaleTime()
.domain([new Date("2025-01-01"), new Date("2025-12-31")])
.range([0, 800]);
// Log scale (useful for data spanning orders of magnitude)
const logScale = d3.scaleLog()
.domain([1, 10000])
.range([0, 500]);
// Color scale
const colorScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(["A", "B", "C", "D"]);6. 坐标轴
D3 坐标轴生成器自动从比例尺创建 SVG 刻度线、标签和参考线。使用 axisBottom、axisLeft、axisTop 和 axisRight 设置方向。
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
const xScale = d3.scaleLinear()
.domain([0, 100]).range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, 500]).range([height, 0]);
// Add bottom axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale).ticks(10));
// Add left axis
svg.append("g")
.call(d3.axisLeft(yScale)
.ticks(5)
.tickFormat(d => "\$" + d));7. SVG 基础
D3 主要使用 SVG 进行渲染。理解核心 SVG 元素对于使用 D3 至关重要。SVG 坐标系原点在左上角,y 轴向下递增。
<!-- Core SVG elements used in D3 -->
<svg width="400" height="300">
<!-- Rectangle: x, y, width, height -->
<rect x="10" y="10" width="80" height="50"
fill="#3b82f6" rx="4" />
<!-- Circle: cx, cy, r -->
<circle cx="200" cy="100" r="30"
fill="#ef4444" stroke="#000" stroke-width="2" />
<!-- Line: x1, y1, x2, y2 -->
<line x1="10" y1="200" x2="390" y2="200"
stroke="#94a3b8" stroke-width="1" />
<!-- Path: the most powerful SVG element -->
<path d="M10 250 L100 200 L200 230 L300 180"
fill="none" stroke="#10b981" stroke-width="2" />
<!-- Text -->
<text x="200" y="290" text-anchor="middle"
font-size="14">Label</text>
<!-- Group: transform applies to all children -->
<g transform="translate(50, 50)">
<rect width="20" height="20" fill="#8b5cf6" />
</g>
</svg>8. 柱状图
柱状图是最常见的 D3 图表类型。下面是一个完整的垂直柱状图示例,包含比例尺、坐标轴和数据标签。
const data = [
{ label: "React", value: 42 },
{ label: "Vue", value: 30 },
{ label: "Angular", value: 22 },
{ label: "Svelte", value: 18 },
{ label: "Solid", value: 12 },
];
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const width = 500 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select("#bar-chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
const x = d3.scaleBand()
.domain(data.map((d) => d.label))
.range([0, width])
.padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([height, 0]);
// Axes
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g").call(d3.axisLeft(y));
// Bars
svg.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => x(d.label))
.attr("y", (d) => y(d.value))
.attr("width", x.bandwidth())
.attr("height", (d) => height - y(d.value))
.attr("fill", "#3b82f6")
.attr("rx", 4);9. 折线图
折线图使用 d3.line() 生成器将数据点转换为 SVG 路径。适合展示随时间变化的趋势数据。
const data = [
{ date: new Date("2025-01"), value: 120 },
{ date: new Date("2025-02"), value: 180 },
{ date: new Date("2025-03"), value: 150 },
{ date: new Date("2025-04"), value: 220 },
{ date: new Date("2025-05"), value: 280 },
{ date: new Date("2025-06"), value: 250 },
];
const x = d3.scaleTime()
.domain(d3.extent(data, (d) => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([height, 0]);
// Line generator
const lineGen = d3.line()
.x((d) => x(d.date))
.y((d) => y(d.value))
.curve(d3.curveMonotoneX); // smooth curve
// Draw the line
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#3b82f6")
.attr("stroke-width", 2.5)
.attr("d", lineGen);
// Add data points
svg.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", (d) => x(d.date))
.attr("cy", (d) => y(d.value))
.attr("r", 4)
.attr("fill", "#3b82f6");10. 饼图和环形图
饼图使用 d3.pie() 计算角度,d3.arc() 生成弧形路径。设置 innerRadius > 0 即可变为环形图。
const data = [
{ label: "Desktop", value: 55 },
{ label: "Mobile", value: 35 },
{ label: "Tablet", value: 10 },
];
const radius = 150;
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Pie layout computes start/end angles
const pieLayout = d3.pie()
.value((d) => d.value)
.sort(null);
// Arc generator
const arcGen = d3.arc()
.innerRadius(60) // 0 for pie, >0 for donut
.outerRadius(radius);
const g = svg.append("g")
.attr("transform",
"translate(" + (width / 2) + "," + (height / 2) + ")");
// Draw arcs
g.selectAll("path")
.data(pieLayout(data))
.join("path")
.attr("d", arcGen)
.attr("fill", (d, i) => color(i))
.attr("stroke", "#fff")
.attr("stroke-width", 2);
// Add labels
g.selectAll("text")
.data(pieLayout(data))
.join("text")
.attr("transform", (d) =>
"translate(" + arcGen.centroid(d) + ")")
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text((d) => d.data.label);11. 过渡与动画
D3 的 transition() 方法在属性变化之间创建平滑的动画效果。支持自定义持续时间、延迟、缓动函数和链式过渡。
// Basic transition
svg.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => x(d.label))
.attr("y", height) // start from bottom
.attr("width", x.bandwidth())
.attr("height", 0) // start with zero height
.attr("fill", "#3b82f6")
.transition() // begin transition
.duration(800) // 800ms
.delay((d, i) => i * 100) // stagger bars
.ease(d3.easeCubicOut) // easing function
.attr("y", (d) => y(d.value)) // animate to final y
.attr("height", (d) => height - y(d.value));
// Chained transitions
d3.select("circle")
.transition()
.duration(500)
.attr("r", 50)
.attr("fill", "red")
.transition() // chain another
.duration(500)
.attr("r", 20)
.attr("fill", "blue");12. 事件处理
D3 使用 .on() 方法监听 DOM 事件。回调函数接收事件对象和绑定的数据,可以创建悬停高亮、工具提示和点击交互。
// Hover highlight
svg.selectAll("rect")
.on("mouseenter", function (event, d) {
d3.select(this)
.attr("fill", "#2563eb")
.attr("opacity", 0.8);
})
.on("mouseleave", function (event, d) {
d3.select(this)
.attr("fill", "#3b82f6")
.attr("opacity", 1);
});
// Tooltip
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "#1e293b")
.style("color", "#f1f5f9")
.style("padding", "8px 12px")
.style("border-radius", "6px")
.style("font-size", "13px")
.style("pointer-events", "none")
.style("opacity", 0);
svg.selectAll("circle")
.on("mouseover", function (event, d) {
tooltip
.style("opacity", 1)
.html("Value: " + d.value)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function () {
tooltip.style("opacity", 0);
});13. 响应式图表
使用 SVG 的 viewBox 属性让图表自动缩放,配合 ResizeObserver 实现真正的响应式设计。
// Method 1: viewBox (simplest)
const svg = d3.select("#chart")
.append("svg")
.attr("viewBox", "0 0 600 400")
.style("width", "100%")
.style("height", "auto");
// Chart scales to container automatically
// Method 2: ResizeObserver (dynamic redraw)
function createChart(container) {
const observer = new ResizeObserver((entries) => {
const { width } = entries[0].contentRect;
const height = width * 0.6; // aspect ratio
// Clear and redraw
d3.select(container).selectAll("*").remove();
const svg = d3.select(container)
.append("svg")
.attr("width", width)
.attr("height", height);
// Recalculate scales with new dimensions
const x = d3.scaleLinear()
.domain([0, 100])
.range([40, width - 20]);
// ... draw chart with new dimensions
});
observer.observe(container);
}14. D3 与 React 集成
在 React 中使用 D3 的推荐方式是用 useRef 获取容器引用,在 useEffect 中运行 D3 代码。React 管理组件生命周期,D3 负责 SVG 渲染。
import React, { useRef, useEffect } from "react";
import * as d3 from "d3";
function BarChart({ data }) {
const svgRef = useRef(null);
useEffect(() => {
if (!data || !svgRef.current) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove(); // clear previous
const width = 500, height = 300;
const margin = { top: 20, right: 20,
bottom: 30, left: 40 };
const innerW = width - margin.left - margin.right;
const innerH = height - margin.top - margin.bottom;
const g = svg
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform",
"translate(" + margin.left + ","
+ margin.top + ")");
const x = d3.scaleBand()
.domain(data.map((d) => d.name))
.range([0, innerW]).padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.val)])
.range([innerH, 0]);
g.append("g")
.attr("transform", "translate(0," + innerH + ")")
.call(d3.axisBottom(x));
g.append("g").call(d3.axisLeft(y));
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => x(d.name))
.attr("y", (d) => y(d.val))
.attr("width", x.bandwidth())
.attr("height", (d) => innerH - y(d.val))
.attr("fill", "#3b82f6");
}, [data]);
return <svg ref={svgRef} />;
}方法二:React 渲染 SVG + D3 工具
function BarChartReact({ data }) {
const width = 500, height = 300;
const margin = { top: 20, right: 20,
bottom: 30, left: 40 };
const x = d3.scaleBand()
.domain(data.map((d) => d.name))
.range([margin.left, width - margin.right])
.padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.val)])
.range([height - margin.bottom, margin.top]);
return (
<svg width={width} height={height}>
{data.map((d, i) => (
<rect
key={i}
x={x(d.name)}
y={y(d.val)}
width={x.bandwidth()}
height={y(0) - y(d.val)}
fill="#3b82f6"
/>
))}
</svg>
);
}15. GeoJSON 与地图
D3 内置强大的地理投影和路径生成器,可以将 GeoJSON/TopoJSON 数据渲染为交互式地图。支持数十种投影方式。
// Load and render a world map
const projection = d3.geoNaturalEarth1()
.scale(150)
.translate([width / 2, height / 2]);
const pathGen = d3.geoPath().projection(projection);
// Load GeoJSON data
d3.json("https://unpkg.com/world-atlas@2"
+ "/countries-110m.json")
.then((world) => {
const countries = topojson.feature(
world, world.objects.countries
);
svg.selectAll("path")
.data(countries.features)
.join("path")
.attr("d", pathGen)
.attr("fill", "#cbd5e1")
.attr("stroke", "#64748b")
.attr("stroke-width", 0.5);
});
// Choropleth: color by data value
const colorScale = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateBlues);
svg.selectAll("path")
.attr("fill", (d) =>
colorScale(dataMap.get(d.id) || 0));16. 力导向图
力导向图使用物理模拟来布局网络节点。d3-force 模块提供多种力:链接力、电荷力、中心力和碰撞力。
const nodes = [
{ id: "A" }, { id: "B" }, { id: "C" },
{ id: "D" }, { id: "E" },
];
const links = [
{ source: "A", target: "B" },
{ source: "A", target: "C" },
{ source: "B", target: "D" },
{ source: "C", target: "E" },
];
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links)
.id((d) => d.id).distance(100))
.force("charge", d3.forceManyBody()
.strength(-200))
.force("center", d3.forceCenter(
width / 2, height / 2))
.force("collision", d3.forceCollide(20));
// Draw links
const link = svg.selectAll("line")
.data(links)
.join("line")
.attr("stroke", "#94a3b8")
.attr("stroke-width", 2);
// Draw nodes
const node = svg.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 15)
.attr("fill", "#3b82f6")
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragging)
.on("end", dragEnd));
// Update positions on each tick
simulation.on("tick", () => {
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
node
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y);
});
function dragStart(event, d) {
if (!event.active)
simulation.alphaTarget(0.3).restart();
d.fx = d.x; d.fy = d.y;
}
function dragging(event, d) {
d.fx = event.x; d.fy = event.y;
}
function dragEnd(event, d) {
if (!event.active)
simulation.alphaTarget(0);
d.fx = null; d.fy = null;
}17. D3.js 最佳实践
- 使用 margin 约定:定义 margin 对象(top/right/bottom/left),从 SVG 尺寸中减去 margin 计算内部绘图区域。所有图表都应遵循此模式。
- 优先使用 join() 而非 enter/exit:D3 v5+ 的 join() 方法更简洁,自动处理进入、更新和退出选择集,减少样板代码。
- 按需导入模块:不要导入整个 d3 库。只导入你需要的模块(如 d3-scale、d3-selection),可以显著减小打包体积。
- 使用 viewBox 实现响应式:设置 SVG 的 viewBox 属性而非固定 width/height,让图表自动适应容器大小。
- 为 key 使用数据标识:在 data() 中传入 key 函数(如 d => d.id),确保数据更新时元素正确匹配,而不是依赖索引。
- 清理定时器和事件监听:在 React useEffect 的清理函数中停止 simulation、取消 transition 和移除事件监听,防止内存泄漏。
- 合理使用过渡动画:动画应增强理解而非分散注意力。持续时间保持在 200-800ms 之间,避免在大量元素上同时触发复杂动画。
- 为可访问性添加 ARIA 属性:为 SVG 添加 role="img"、aria-label 描述,为数据点添加 title 元素,确保屏幕阅读器可以理解图表内容。
18. 常用 D3 模块速查
| 模块 | 用途 | 关键 API |
|---|---|---|
| d3-selection | 选择和操作 DOM 元素 | select, selectAll, append, attr, style, on |
| d3-scale | 将数据映射到视觉通道 | scaleLinear, scaleBand, scaleTime, scaleOrdinal, scaleLog |
| d3-axis | 生成坐标轴 | axisBottom, axisLeft, axisTop, axisRight |
| d3-shape | 形状生成器 | line, area, arc, pie, stack, symbol |
| d3-transition | 动画过渡 | transition, duration, delay, ease |
| d3-array | 数组统计工具 | min, max, extent, mean, sum, group, rollup |
| d3-fetch | 数据加载 | json, csv, tsv, text, xml |
| d3-geo | 地理投影和路径 | geoPath, geoMercator, geoNaturalEarth1, geoAlbersUsa |
| d3-force | 力导向模拟 | forceSimulation, forceLink, forceManyBody, forceCenter |
| d3-zoom | 缩放和平移 | zoom, zoomTransform, zoomIdentity |
| d3-drag | 拖拽交互 | drag, dragDisable, dragEnable |
| d3-color | 颜色操作 | rgb, hsl, lab, interpolateRgb, schemeCategory10 |
常见问题
D3.js 是什么?用来做什么?
D3.js(Data-Driven Documents)是一个 JavaScript 可视化库,用于在浏览器中使用 SVG、HTML 和 CSS 创建动态交互式数据可视化。它将数据绑定到 DOM 元素并应用数据驱动的变换,用于创建图表、地图、图形和仪表板。
如何安装 D3.js?
通过 npm install d3 安装并使用 import * as d3 from "d3" 导入。也可以按需导入单独模块如 d3-scale 或 d3-selection。或者从 CDN 加载 script 标签。
D3 的 select 和 selectAll 有什么区别?
d3.select() 选择第一个匹配的 DOM 元素,d3.selectAll() 选择所有匹配元素。两者都接受 CSS 选择器,返回的选择集支持链式调用来修改属性、样式和绑定数据。
D3 数据绑定的 enter/update/exit 是什么?
enter() 处理新数据项(需要创建新元素),update 选择集处理已有元素的数据更新,exit() 处理不再有对应数据的元素(需要移除)。D3 v7 的 join() 方法将三者合一,简化了代码。
D3 比例尺是什么?为什么重要?
比例尺将数据值(domain)映射到视觉值(range)。scaleLinear 映射连续数值,scaleBand 映射类别,scaleTime 映射日期,scaleLog 使用对数映射。它们是将原始数据转换为适配图表尺寸的坐标的核心工具。
如何在 React 中使用 D3?
推荐使用 useRef 获取 SVG 或 div 容器的引用,然后在 useEffect 中运行 D3 代码操作该容器。React 管理组件生命周期,D3 负责 SVG 渲染。对于简单图表,也可以只用 D3 的比例尺工具,由 React 直接渲染 SVG 元素。
如何创建响应式 D3 图表?
使用 SVG viewBox 属性代替固定 width/height,设置 viewBox="0 0 width height" 并用 CSS 设置宽度 100%。或用 ResizeObserver 检测容器大小变化并重绘。
D3 能创建地图吗?
是的。使用 d3-geo 投影(geoMercator、geoNaturalEarth1 等)将经纬度转换为屏幕坐标,加载 GeoJSON/TopoJSON 数据,用 geoPath 渲染为 SVG 路径。支持等值线地图、点地图和自定义投影。
D3.js 是 Web 数据可视化的黄金标准。掌握选择集、数据绑定、比例尺和过渡动画四个核心概念后,你就能构建任何类型的可视化。从柱状图开始,逐步挑战折线图、饼图、地图和力导向图。在 React 中使用 useRef + useEffect 模式集成。