Back to Blog
· 7 min read

数据可视化与 D3.js

数据可视化

为什么需要数据可视化

人类大脑处理图像的速度比处理文本快 60,000 倍。在数据爆炸的时代,可视化是理解海量信息的关键手段。

文字描述: “2024年第一季度销售额分别是 120万、180万、210万”

可视化后:

{/* 纯 SVG 柱状图 */}

{/* Y轴 */} {/* X轴 */} {/* 柱状图 */}

{/* 标签 */} 1月 2月 3月

{/* 数值标签 */} 120万 180万 210万

一眼就能看出增长趋势,这就是可视化的力量。

可视化核心概念

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 可视化库,但学习曲线较陡。建议从简单图表开始,逐步掌握数据绑定和交互实现。

相关阅读