· 7 min read
数据可视化与 D3.js
数据可视化
为什么需要数据可视化
人类大脑处理图像的速度比处理文本快 60,000 倍。在数据爆炸的时代,可视化是理解海量信息的关键手段。
文字描述: “2024年第一季度销售额分别是 120万、180万、210万”
可视化后:
{/* 纯 SVG 柱状图 */}
{/* 标签 */}
{/* 数值标签 */}
一眼就能看出增长趋势,这就是可视化的力量。
可视化核心概念
1. 数据处理
// 典型数据处理流程
const rawData = [
{ date: '2024-01', sales: 120000, category: '电子产品' },
{ date: '2024-02', sales: 180000, category: '电子产品' },
{ date: '2024-03', sales: 210000, category: '服装' },
];
// 数据清洗:处理缺失值
const cleanedData = rawData.filter(d => d.sales != null);
// 数据转换:聚合
const aggregated = d3.rollup(
rawData,
v => d3.sum(v, d => d.sales),
d => d.category
);
2. 视觉通道
视觉通道是将数据映射到图形属性的桥梁:
| 视觉通道 | 适用数据类型 | 示例 |
|---|---|---|
| 位置 | 定量/有序 | 散点图坐标 |
| 长度 | 定量 | 条形图高度 |
| 角度 | 定量 | 饼图扇区 |
| 面积 | 定量 | 气泡图大小 |
| 颜色 | 分类/定量 | 类别区分/热力图 |
| 形状 | 分类 | 不同标记类型 |
3. 图形选择
flowchart TD
A[数据类型] --> B{比较类型}
B -->|趋势| C[折线图]
B -->|占比| D[饼图/环形图]
B -->|排名| E[条形图]
B -->|分布| F[直方图/密度图]
B -->|关联| G[散点图]
B -->|地理| H[地图]
分层架构
现代前端可视化系统采用分层架构,每层职责清晰:
graph LR
subgraph "数据层 [Data]"
direction TB
I[数据获取] --> J[数据清洗] --> K[数据转换]
end
subgraph "视觉层 [Visual]"
direction TB
E[比例尺] --> F[坐标轴] --> G[图形元素]
E --> H[布局算法]
end
subgraph "交互层 [Interaction]"
direction TB
B[事件处理] --> C[动画引擎] --> D[工具提示]
end
subgraph "表现层 [Presentation]"
direction TB
P[用户界面]
end
K -->|数据流| E
G -->|触发| B
C -->|更新| P
H -.->|计算布局| G
数据层
// 数据层示例:数据获取与清洗
async function loadData() {
const response = await fetch('/api/sales');
const raw = await response.json();
return raw
.filter(d => d.value !== null) // 清洗
.map(d => ({ // 转换
...d,
date: new Date(d.date),
value: +d.value
}))
.sort((a, b) => a.date - b.date); // 排序
}
视觉层
// 视觉层:创建 SVG 画布
const svg = d3.select('#chart')
.append('svg')
.attr('width', 800)
.attr('height', 400);
// 创建比例尺
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, 800]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([400, 0]);
// 创建坐标轴
svg.append('g')
.call(d3.axisBottom(xScale));
svg.append('g')
.call(d3.axisLeft(yScale));
交互层
// 交互层:工具提示
const tooltip = d3.select('body')
.append('div')
.style('position', 'absolute')
.style('visibility', 'hidden')
.style('background', 'white')
.style('padding', '8px')
.style('border', '1px solid #ccc');
// 绑定事件
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.on('mouseover', function(event, d) {
d3.select(this).attr('r', 8);
tooltip
.style('visibility', 'visible')
.html(`日期: ${d.date}<br/>销售额: ${d.value}`);
})
.on('mousemove', function(event) {
tooltip
.style('top', (event.pageY - 10) + 'px')
.style('left', (event.pageX + 10) + 'px');
})
.on('mouseout', function() {
d3.select(this).attr('r', 5);
tooltip.style('visibility', 'hidden');
});
D3.js 核心机制
Enter-Update-Exit 模式
这是 D3 最核心的数据驱动机制:
graph TD
A[数据数组] --> B{比较}
B -->|新数据更多| C[ENTER<br/>创建新元素]
B -->|数量相同| D[UPDATE<br/>更新属性]
B -->|数据更少| E[EXIT<br/>移除元素]
C --> F[渲染新图形]
D --> G[更新现有图形]
E --> H[删除多余图形]
完整示例:动态条形图
function updateBarChart(newData) {
const svg = d3.select('#bar-chart');
// 定义比例尺
const x = d3.scaleBand()
.domain(newData.map(d => d.category))
.range([0, width])
.padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(newData, d => d.value)])
.range([height, 0]);
// ENTER: 创建新元素
const bars = svg.selectAll('.bar')
.data(newData, d => d.category);
bars.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', d => x(d.category))
.attr('y', height) // 从底部开始
.attr('width', x.bandwidth())
.attr('height', 0)
.attr('fill', 'steelblue')
.merge(bars) // 合并 UPDATE
.transition()
.duration(500)
.attr('y', d => y(d.value))
.attr('height', d => height - y(d.value));
// EXIT: 移除元素
bars.exit()
.transition()
.duration(500)
.attr('y', height)
.attr('height', 0)
.remove();
}
比例尺 (Scales)
D3 提供多种比例尺:
| 比例尺 | 用途 | 示例 |
|---|---|---|
| scaleLinear | 线性映射 | 数值 → 像素位置 |
| scaleTime | 时间映射 | Date → 像素位置 |
| scaleBand | 区间映射 | 分类 → 条形宽度 |
| scaleOrdinal | 分类映射 | 分类 → 颜色 |
| scaleSequential | 连续渐变 | 数值 → 颜色渐变 |
// 线性比例尺
const linear = d3.scaleLinear()
.domain([0, 100]) // 数据范围
.range([0, 500]); // 输出范围
linear(0); // 0
linear(50); // 250
linear(100); // 500
// 颜色比例尺
const color = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#1f77b4', '#ff7f0e', '#2ca02c']);
color('A'); // '#1f77b4'
color('B'); // '#ff7f0e'
布局算法 (Layouts)
D3 内置多种布局算法:
// 力导向图布局
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
// 饼图布局
const pie = d3.pie().value(d => d.value);
const arc = d3.arc().innerRadius(0).outerRadius(radius);
const arcs = svg.selectAll('arc')
.data(pie(data))
.enter()
.append('g')
.attr('transform', `translate(${width/2}, ${height/2})`);
arcs.append('path')
.attr('d', arc)
.attr('fill', d => color(d.data.category));
Demo
示例 1:折线图
// 基础折线图
const margin = { top: 20, right: 30, bottom: 30, left: 40 };
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select('#line-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.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]);
// 添加路径
const line = d3.line()
.x(d => x(d.date))
.y(d => y(d.value))
.curve(d3.curveMonotoneX); // 平滑曲线
svg.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2)
.attr('d', line);
// 添加数据点
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => x(d.date))
.attr('cy', d => y(d.value))
.attr('r', 4)
.attr('fill', 'steelblue');
示例 2:交互式地图
// 使用 GeoJSON 绘制地图
const projection = d3.geoMercator()
.scale(100)
.center([0, 20])
.translate([width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
svg.selectAll('path')
.data(geoData.features)
.enter()
.append('path')
.attr('d', path)
.attr('fill', d => color(d.properties.region))
.attr('stroke', '#fff')
.attr('stroke-width', 0.5)
.on('mouseover', function(event, d) {
d3.select(this).attr('fill', 'orange');
tooltip.text(d.properties.name);
})
.on('mouseout', function(event, d) {
d3.select(this).attr('fill', color(d.properties.region));
});
示例 3:数据仪表盘
// 创建仪表盘组件
function Gauge(value, min, max, label) {
const svg = d3.select(`#gauge-${label}`)
.append('svg')
.attr('width', 200)
.attr('height', 120);
const angle = d3.scaleLinear()
.domain([min, max])
.range([-Math.PI / 2, Math.PI / 2]);
const arc = d3.arc()
.innerRadius(60)
.outerRadius(80)
.startAngle(-Math.PI / 2);
// 背景弧
svg.append('path')
.datum({ endAngle: Math.PI / 2 })
.attr('d', arc)
.attr('transform', 'translate(100, 100)');
// 值弧
svg.append('path')
.datum({ endAngle: angle(value) })
.attr('d', arc)
.attr('fill', 'green')
.attr('transform', 'translate(100, 100)');
// 标签
svg.append('text')
.attr('x', 100)
.attr('y', 100)
.attr('text-anchor', 'middle')
.text(value);
}
性能优化
1. 使用 Canvas 替代 SVG
对于大数据集(> 10,000 点),SVG 性能会下降:
// SVG: 适合 < 1000 点
const svg = d3.select('body').append('svg');
// Canvas: 适合 > 1000 点
const canvas = d3.select('body').append('canvas')
.attr('width', width)
.attr('height', height);
const context = canvas.node().getContext('2d');
// 使用 D3 比例尺,但用 Canvas 绘制
data.forEach(d => {
context.beginPath();
context.arc(x(d.x), y(d.y), r, 0, 2 * Math.PI);
context.fill();
});
2. 虚拟滚动
只渲染可见区域的数据:
// 只渲染可见范围内的数据
const visibleData = data.filter(d =>
x(d.date) >= 0 && x(d.date) <= width
);
svg.selectAll('circle')
.data(visibleData, d => d.id)
.join('circle')
.attr('cx', d => x(d.date))
.attr('cy', d => y(d.value));
3. Web Workers
将数据处理放到 Web Worker:
// worker.js
self.onmessage = function(e) {
const processed = heavyDataProcessing(e.data);
self.postMessage(processed);
};
// 主线程
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
updateChart(e.data);
};
worker.postMessage(rawData);
技术选型
| 场景 | 推荐方案 |
|---|---|
| 简单图表 | D3、ECharts |
| 复杂交互 | D3.js |
| 快速开发 | ECharts、Chart.js |
| 地理可视化 | D3.js + TopoJSON |
| 大数据 | Deck.gl、Canvas |
| React 生态 | Recharts、Visx |
总结
数据可视化是将数据转化为直观图形的重要技术:
- 核心流程: 数据处理 → 视觉映射 → 交互设计
- D3 核心: ENTER-UPDATE-EXIT 机制实现数据驱动
- 分层架构: 数据层 → 视觉层 → 交互层
- 设计原则: 清晰性优先、目标导向、响应式适配
- 性能: 大数据用 Canvas,交互用 SVG
D3.js 是最强大的 Web 可视化库,但学习曲线较陡。建议从简单图表开始,逐步掌握数据绑定和交互实现。