跳到主要内容

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.jsECharts
定位底层可视化工具库开箱即用的图表库
学习曲线
定制化极高(自由绑定)中等(配置驱动)
图表类型需自己组合内置 30+ 种
渲染方式主要 SVGCanvas/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。

相关链接