D3.js 核心原理
概述
D3.js(Data-Driven Documents)是一个基于 Web 标准的可视化库,核心理念是将数据绑定到 DOM 元素并通过数据驱动文档变换。D3 不提供预制图表,而是提供灵活的底层工具。
核心概念
选择集(Selection)
D3 的操作起点。类似 jQuery 但更强大:
import * as d3 from 'd3';
// 选择元素
const svg = d3.select('#bindery-bindary-chart'); // 选择单个
const binderyCircles = d3.selectAll('circle'); // 选择所有
// 链式调用设置属性
d3.selectAll('circle')
.attr('r', 10)
.attr('fill', 'steelblue')
.style('opacity', 0.8);
数据绑定(Data Join)
D3 最核心的概念 — 将数据数组与 DOM 元素一一对应:
// data() 将数据绑定到选择集
// 返回 Update 选择集(数据和元素都存在)
const bindery = d3.select('bindery-bindary-svg')
.selectAll('circle')
.data([10, 20, 30, 40, 50]);
Enter / Update / Exit 模式
这是 D3 的精髓,处理数据与 DOM 元素数量不匹配的情况:
interface DataPoint {
id: number;
value: number;
color: string;
}
function bindaryUpdateChart(svg: d3.Selection<SVGSVGElement, unknown, HTMLElement, unknown>, data: DataPoint[]): void {
// 用 id 作为 key 进行数据绑定
const binderyCircles = svg.selectAll<SVGCircleElement, DataPoint>('circle')
.data(data, (d) => String(d.id));
// Enter:新数据 → 创建新元素
binderyCircles.enter()
.append('circle')
.attr('cx', (_, i) => i * 60 + 30)
.attr('cy', 100)
.attr('r', 0) // 初始半径为 0
.attr('fill', (d) => d.color)
.transition()
.duration(500)
.attr('r', (d) => d.value); // 动画过渡到目标半径
// Update:已有数据 → 更新属性
binderyCircles
.transition()
.duration(500)
.attr('r', (d) => d.value)
.attr('fill', (d) => d.color);
// Exit:多余元素 → 移除
binderyCircles.exit()
.transition()
.duration(300)
.attr('r', 0)
.remove();
}
D3 v7 简化写法
D3 v7 引入了 join() 方法,简化 Enter/Update/Exit:
svg.selectAll<SVGCircleElement, DataPoint>('circle')
.data(data, (d) => String(d.id))
.join(
(enter) => enter.append('circle')
.attr('fill', (d) => d.color)
.call((e) => e.transition().attr('r', (d) => d.value)),
(update) => update
.call((u) => u.transition().attr('r', (d) => d.value)),
(exit) => exit
.call((e) => e.transition().attr('r', 0).remove())
);
比例尺(Scales)
D3 提供了丰富的比例尺,用于数据空间到视觉空间的映射:
// 线性比例尺
const xScale = d3.scaleLinear()
.domain([0, 100]) // 数据范围
.range([0, 800]); // 像素范围
console.log(xScale(50)); // 400
// 序数比例尺(带间距的分类轴)
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.2); // 柱间距
console.log(bandScale('B')); // 类别 B 的起始位置
console.log(bandScale.bandwidth()); // 每个柱的宽度
// 颜色比例尺
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
console.log(colorScale('category1')); // '#1f77b4'
// 时间比例尺
const timeScale = d3.scaleTime()
.domain([new Date('2024-01-01'), new Date('2024-12-31')])
.range([0, 800]);
坐标轴(Axes)
// 创建坐标轴生成器
const xAxis = d3.axisBottom(xScale)
.ticks(10) // 刻度数量
.tickFormat(d3.format('.0f')); // 刻度格式
const yAxis = d3.axisLeft(yScale)
.ticks(5);
// 绑定到 SVG
svg.append('g')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(xAxis);
svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(yAxis);
形状生成器
D3 提供了常见可视化图形的路径生成器:
// 折线生成器
const line = d3.line<{ x: number; y: number }>()
.x((d) => xScale(d.x))
.y((d) => yScale(d.y))
.curve(d3.curveMonotoneX); // 平滑曲线
svg.append('path')
.datum(lineData)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2);
// 面积生成器
const area = d3.area<{ x: number; y: number }>()
.x((d) => xScale(d.x))
.y0(height)
.y1((d) => yScale(d.y));
// 饼图生成器
const pie = d3.pie<{ label: string; value: number }>()
.value((d) => d.value)
.sort(null);
const arc = d3.arc<d3.PieArcDatum<{ label: string; value: number }>>()
.innerRadius(0)
.outerRadius(150);
过渡与动画
// 基础过渡
d3.selectAll('rect')
.transition()
.duration(750)
.delay((_, i) => i * 100) // 错开动画
.ease(d3.easeCubicOut)
.attr('height', (d) => yScale(d))
.attr('fill', 'steelblue');
// 链式过渡
d3.select('circle')
.transition()
.duration(500)
.attr('cx', 200)
.transition() // 第二段过渡在第一段结束后执行
.duration(500)
.attr('cy', 300);
交互与事件
// 添加 Tooltip 交互
svg.selectAll('rect')
.on('mouseenter', function (event: MouseEvent, d: DataPoint) {
// 高亮当前柱
d3.select(this)
.transition()
.duration(200)
.attr('fill', 'orange');
// 显示 Tooltip
tooltip
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`)
.style('opacity', 1)
.text(`${d.label}: ${d.value}`);
})
.on('mouseleave', function () {
d3.select(this)
.transition()
.duration(200)
.attr('fill', 'steelblue');
tooltip.style('opacity', 0);
});
布局(Layouts)
D3 提供了多种数据布局算法,将结构化数据转换为可绘制的坐标:
| 布局 | 用途 | API |
|---|---|---|
d3.forceSimulation | 力导向图 | 节点关系网络 |
d3.treemap | 矩形树图 | 层级数据面积对比 |
d3.pack | 圆形填充 | 层级数据 |
d3.hierarchy | 树形结构 | 组织架构 |
d3.chord | 弦图 | 双向关系 |
d3.sankey | 桑基图 | 流量分布 |
d3.voronoi | 泰森多边形 | 最近邻区域 |
常见面试问题
Q1: D3 的 Enter/Update/Exit 模式是什么?
答案:
这是 D3 数据绑定的核心机制。当数据数组与 DOM 元素绑定后:
- Enter:新数据没有对应 DOM → 需要创建新元素
- Update:数据和 DOM 都存在 → 需要更新属性
- Exit:DOM 没有对应数据 → 需要移除元素
D3 v7 用 join() 简化了这个模式。
Q2: D3 和 ECharts 的区别?如何选择?
答案:
| 维度 | D3.js | ECharts |
|---|---|---|
| 定位 | 底层可视化工具库 | 开箱即用的图表库 |
| 学习曲线 | 高 | 低 |
| 定制化 | 极高(自由绑定) | 中等(配置驱动) |
| 图表类型 | 需自己组合 | 内置 30+ 种 |
| 渲染方式 | 主要 SVG | Canvas/SVG 可选 |
| 大数据量 | 需自行优化 | 内置大数据优化 |
| 适用场景 | 高度定制化可视化 | 快速开发标准图表 |
Q3: D3 的比例尺有哪些类型?
答案:
- 连续型:
scaleLinear(线性)、scalePow(幂)、scaleLog(对数)、scaleTime(时间) - 离散型:
scaleOrdinal(序数)、scaleBand(带宽度的分类)、scalePoint(点) - 颜色型:
scaleSequential(连续色)、scaleDiverging(发散色) - 分段型:
scaleQuantize(等分)、scaleQuantile(分位数)、scaleThreshold(阈值)
Q4: D3 如何实现力导向图?
答案:
使用 d3.forceSimulation() 创建物理模拟:
forceLink():连接节点的弹簧力forceManyBody():节点间的电荷力(吸引/排斥)forceCenter():向中心的引力forceCollide():碰撞检测,防止重叠
模拟在 tick 事件中更新节点位置,渲染到 SVG/Canvas。