【前端数据可视化】D3.js入门:打造炫酷的数据可视化效果

前言

大家好,我是cannonmonster01!上一篇我们聊了ECharts,今天咱们来看看数据可视化领域的"瑞士军刀"——D3.js。如果说ECharts是傻瓜相机,那D3.js就是专业单反,虽然学习曲线陡峭,但一旦掌握,就能创造出令人惊叹的可视化效果。

什么是D3.js

D3.js(Data-Driven Documents)是一个基于Web标准的JavaScript库,用于操作文档的可视化。它的核心思想是:

  • 数据驱动:数据决定DOM的呈现
  • 声明式编程:描述"是什么"而非"怎么做"
  • 链式调用:方法可以链式组合,代码优雅

环境搭建

引入方式

<!-- 方式1:直接引入CDN -->
<script src="https://d3js.org/d3.v7.min.js"></script>

<!-- 方式2:npm安装 -->
<script type="module">
import * as d3 from 'https://cdn.skypack.dev/d3@7';
</script>

创建第一个D3图表

<!DOCTYPE html>
<html>
<head>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
  <svg width="600" height="400"></svg>
  
  <script>
    const svg = d3.select('svg');
    
    const data = [4, 8, 15, 16, 23, 42];
    
    const rects = svg.selectAll('rect')
      .data(data)
      .enter()
      .append('rect')
      .attr('x', (d, i) => i * 100)
      .attr('y', d => 400 - d * 10)
      .attr('width', 80)
      .attr('height', d => d * 10)
      .attr('fill', '#5470c6');
  </script>
</body>
</html>

D3核心概念

选择器与数据绑定

D3的选择器语法类似于CSS选择器:

// 选择所有p元素
d3.selectAll('p')
  .style('color', 'red');

// 选择第一个p元素
d3.select('p')
  .style('font-size', '20px');

数据绑定是D3的核心:

const data = [1, 2, 3, 4, 5];

const selection = d3.select('body')
  .selectAll('div')
  .data(data);

// enter()返回缺少的元素
selection.enter()
  .append('div')
  .text(d => `数值: ${d}`);

// exit()返回多余的元素
selection.exit()
  .remove();

比例尺(Scale)

比例尺用于将数据映射到可视空间:

// 线性比例尺
const xScale = d3.scaleLinear()
  .domain([0, 100])  // 数据范围
  .range([0, 500]);  // 像素范围

console.log(xScale(50));  // 输出: 250

// 序数比例尺
const colorScale = d3.scaleOrdinal()
  .domain(['A', 'B', 'C'])
  .range(['red', 'green', 'blue']);

console.log(colorScale('B'));  // 输出: 'green'

坐标轴(Axis)

D3提供了坐标轴生成器:

const xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, 500]);

const xAxis = d3.axisBottom(xScale)
  .ticks(5)
  .tickFormat(d => `${d}%`);

svg.append('g')
  .attr('transform', 'translate(50, 350)')
  .call(xAxis);

实战案例:交互式折线图

让我们创建一个交互式折线图:

const svg = d3.select('svg');
const margin = {top: 20, right: 30, bottom: 40, left: 50};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

const g = svg.append('g')
  .attr('transform', `translate(${margin.left},${margin.top})`);

const data = [
  {date: '2023-01', value: 45},
  {date: '2023-02', value: 52},
  {date: '2023-03', value: 38},
  {date: '2023-04', value: 65},
  {date: '2023-05', value: 72},
  {date: '2023-06', value: 58}
];

const xScale = d3.scaleBand()
  .domain(data.map(d => d.date))
  .range([0, width])
  .padding(0.1);

const yScale = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)])
  .range([height, 0]);

const line = d3.line()
  .x(d => xScale(d.date) + xScale.bandwidth() / 2)
  .y(d => yScale(d.value))
  .curve(d3.curveMonotoneX);

g.append('path')
  .datum(data)
  .attr('fill', 'none')
  .attr('stroke', '#5470c6')
  .attr('stroke-width', 2)
  .attr('d', line);

g.selectAll('.dot')
  .data(data)
  .enter().append('circle')
  .attr('class', 'dot')
  .attr('cx', d => xScale(d.date) + xScale.bandwidth() / 2)
  .attr('cy', d => yScale(d.value))
  .attr('r', 5)
  .attr('fill', '#5470c6')
  .on('mouseover', function(event, d) {
    d3.select(this)
      .transition()
      .duration(200)
      .attr('r', 8)
      .attr('fill', '#91cc75');
    
    tooltip.transition()
      .duration(200)
      .style('opacity', .9);
    tooltip.html(`日期: ${d.date}<br/>数值: ${d.value}`)
      .style('left', (event.pageX) + 'px')
      .style('top', (event.pageY - 28) + 'px');
  })
  .on('mouseout', function() {
    d3.select(this)
      .transition()
      .duration(200)
      .attr('r', 5)
      .attr('fill', '#5470c6');
    
    tooltip.transition()
      .duration(500)
      .style('opacity', 0);
  });

const tooltip = d3.select('body').append('div')
  .attr('class', 'tooltip')
  .style('opacity', 0);

数据加载与处理

D3支持多种数据格式:

// 加载CSV
d3.csv('data.csv').then(data => {
  console.log(data);
});

// 加载JSON
d3.json('data.json').then(data => {
  console.log(data);
});

// 加载TSV
d3.tsv('data.tsv').then(data => {
  console.log(data);
});

数据转换:

// 过滤数据
const filteredData = data.filter(d => d.value > 50);

// 排序数据
const sortedData = data.sort((a, b) => a.value - b.value);

// 聚合数据
const groupedData = d3.group(data, d => d.category);

动画与过渡效果

D3的过渡效果非常强大:

d3.selectAll('rect')
  .transition()
  .duration(1000)
  .delay((d, i) => i * 100)
  .attr('height', d => d * 10)
  .attr('fill', '#91cc75');

复杂动画:

d3.select('circle')
  .transition()
  .duration(750)
  .ease(d3.easeBounce)
  .attr('cx', 300)
  .transition()
  .duration(500)
  .attr('cy', 200);

实战案例:力导向图

力导向图是D3的经典应用:

const nodes = [
  {id: 'A', group: 1},
  {id: 'B', group: 1},
  {id: 'C', group: 2},
  {id: 'D', group: 2},
  {id: 'E', group: 3}
];

const links = [
  {source: 'A', target: 'B'},
  {source: 'B', target: 'C'},
  {source: 'C', target: 'D'},
  {source: 'D', target: 'E'},
  {source: 'E', target: 'A'}
];

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 link = svg.append('g')
  .selectAll('line')
  .data(links)
  .enter().append('line')
  .attr('stroke', '#999')
  .attr('stroke-width', 1);

const node = svg.append('g')
  .selectAll('circle')
  .data(nodes)
  .enter().append('circle')
  .attr('r', 5)
  .attr('fill', d => colorScale(d.group))
  .call(d3.drag()
    .on('start', dragstarted)
    .on('drag', dragged)
    .on('end', dragended));

simulation.on('tick', () => {
  link
    .attr('x1', d => d.source.x)
    .attr('y1', d => d.source.y)
    .attr('x2', d => d.target.x)
    .attr('y2', d => d.target.y);
  
  node
    .attr('cx', d => d.x)
    .attr('cy', d => d.y);
});

function dragstarted(event, d) {
  if (!event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(event, d) {
  d.fx = event.x;
  d.fy = event.y;
}

function dragended(event, d) {
  if (!event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

常见问题与解决方案

Q1: D3和ECharts该怎么选?

  • ECharts适合快速开发,开箱即用
  • D3适合高度定制化的复杂可视化

Q2: 如何处理大数据量?

  • 使用Web Worker进行数据处理
  • 采用渐进渲染策略
  • 考虑使用Canvas代替SVG

Q3: 如何响应式调整图表?

function resize() {
  const width = window.innerWidth;
  const height = window.innerHeight;
  
  svg.attr('width', width).attr('height', height);
  // 更新比例尺和元素位置
}

window.addEventListener('resize', resize);

总结

D3.js是一个功能强大的数据可视化库,虽然学习曲线较陡,但掌握后能实现各种炫酷效果。今天我们学习了:

  1. D3的基本概念和选择器
  2. 数据绑定和比例尺
  3. 坐标轴和图表创建
  4. 动画和过渡效果
  5. 力导向图的实现

D3的生态系统非常丰富,还有很多高级功能等待你去探索。下一篇我们将聊聊图表性能优化的技巧!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐