Data Visualization
Why Data Visualization Matters
The human brain processes images 60,000 times faster than text. In an era of data explosion, visualization is a key tool for understanding massive amounts of information.
Text description: “Q1 2024 sales were 1.2 million, 1.8 million, 2.1 million respectively”
After visualization:
{/* Pure SVG bar chart */}
{/* Labels */}
{/* Value labels */}
You can see the growth trend at a glance. That’s the power of visualization.
Core Visualization Concepts
1. Data Processing
// 典型数据处理流程
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. Visual Channels
Visual channels are the bridge that maps data to graphical attributes:
| Visual Channel | Suitable Data Type | Example |
|---|---|---|
| Position | Quantitative/Ordinal | Scatter plot coordinates |
| Length | Quantitative | Bar chart height |
| Angle | Quantitative | Pie chart sectors |
| Area | Quantitative | Bubble chart size |
| Color | Categorical/Quantitative | Category distinction/Heatmap |
| Shape | Categorical | Different marker types |
3. Chart Selection
flowchart TD
A[数据类型] --> B{比较类型}
B -->|趋势| C[折线图]
B -->|占比| D[饼图/环形图]
B -->|排名| E[条形图]
B -->|分布| F[直方图/密度图]
B -->|关联| G[散点图]
B -->|地理| H[地图]
Layered Architecture
Modern frontend visualization systems use a layered architecture, with clear responsibilities at each layer:
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
Data Layer
// 数据层示例:数据获取与清洗
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); // 排序
}
Visual Layer
// 视觉层:创建 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));
Interaction Layer
// 交互层:工具提示
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 Core Mechanism
Enter-Update-Exit Pattern
This is D3’s most fundamental data-driven mechanism:
graph TD
A[数据数组] --> B{比较}
B -->|新数据更多| C[ENTER<br/>创建新元素]
B -->|数量相同| D[UPDATE<br/>更新属性]
B -->|数据更少| E[EXIT<br/>移除元素]
C --> F[渲染新图形]
D --> G[更新现有图形]
E --> H[删除多余图形]
Complete Example: Dynamic Bar Chart
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 provides several types of scales:
| Scale | Purpose | Example |
|---|---|---|
| scaleLinear | Linear mapping | value → pixel position |
| scaleTime | Time mapping | Date → pixel position |
| scaleBand | Band mapping | category → bar width |
| scaleOrdinal | Ordinal mapping | category → color |
| scaleSequential | Sequential gradient | value → color gradient |
// 线性比例尺
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 includes several built-in layout algorithms:
// 力导向图布局
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
Example 1: Line chart
// 基础折线图
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');
Example 2: Interactive map
// 使用 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));
});
Example 3: Data dashboard
// 创建仪表盘组件
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);
}
Performance optimization
1. Use Canvas instead of SVG
For large datasets (> 10,000 points), SVG performance degrades:
// 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. Virtual scrolling
Render only the data in the visible area:
// 只渲染可见范围内的数据
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
Move data processing to a 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);
Technology choices
| Scenario | Recommended approach |
|---|---|
| Simple charts | D3, ECharts |
| Complex interactions | D3.js |
| Rapid development | ECharts, Chart.js |
| Geographic visualization | D3.js + TopoJSON |
| Large datasets | Deck.gl, Canvas |
| React ecosystem | Recharts, Visx |
Summary
Data visualization is a key technique for turning data into intuitive graphics:
- Core workflow: data processing → visual mapping → interaction design
- D3 core: the ENTER-UPDATE-EXIT pattern enables data-driven rendering
- Layered architecture: data layer → visual layer → interaction layer
- Design principles: clarity first, goal-oriented, responsive adaptation
- Performance: use Canvas for large datasets, SVG for interaction
D3.js is the most powerful web visualization library, but it has a steep learning curve. It’s best to start with simple charts and gradually master data binding and interaction implementation.