D3.js 常见图表 #

本章将介绍如何使用 D3.js 实现各种常见的图表类型,每个示例都包含完整的代码和详细说明。

图表基础结构 #

通用模板 #

javascript
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

const svg = d3.select('#chart')
  .append('svg')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom);

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

柱状图 #

垂直柱状图 #

javascript
const data = [
  { category: 'A', value: 30 },
  { category: 'B', value: 80 },
  { category: 'C', value: 45 },
  { category: 'D', value: 60 },
  { category: 'E', value: 20 }
];

const x = d3.scaleBand()
  .domain(data.map(d => d.category))
  .range([0, width])
  .padding(0.2);

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

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

g.selectAll('.bar')
  .data(data)
  .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')
  .transition()
  .duration(500)
  .attr('y', d => y(d.value))
  .attr('height', d => height - y(d.value));

水平柱状图 #

javascript
const x = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)])
  .nice()
  .range([0, width]);

const y = d3.scaleBand()
  .domain(data.map(d => d.category))
  .range([0, height])
  .padding(0.2);

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

g.selectAll('.bar')
  .data(data)
  .enter()
  .append('rect')
  .attr('class', 'bar')
  .attr('x', 0)
  .attr('y', d => y(d.category))
  .attr('width', d => x(d.value))
  .attr('height', y.bandwidth())
  .attr('fill', 'steelblue');

分组柱状图 #

javascript
const data = [
  { category: 'A', value1: 30, value2: 40 },
  { category: 'B', value1: 80, value2: 60 },
  { category: 'C', value1: 45, value2: 50 }
];

const x0 = d3.scaleBand()
  .domain(data.map(d => d.category))
  .range([0, width])
  .padding(0.2);

const x1 = d3.scaleBand()
  .domain(['value1', 'value2'])
  .range([0, x0.bandwidth()])
  .padding(0.1);

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => Math.max(d.value1, d.value2))])
  .nice()
  .range([height, 0]);

const colors = d3.scaleOrdinal()
  .domain(['value1', 'value2'])
  .range(['steelblue', 'orange']);

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x0));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

const categories = g.selectAll('.category')
  .data(data)
  .enter()
  .append('g')
  .attr('class', 'category')
  .attr('transform', d => `translate(${x0(d.category)}, 0)`);

categories.selectAll('rect')
  .data(d => ['value1', 'value2'].map(key => ({ key, value: d[key] })))
  .enter()
  .append('rect')
  .attr('x', d => x1(d.key))
  .attr('y', d => y(d.value))
  .attr('width', x1.bandwidth())
  .attr('height', d => height - y(d.value))
  .attr('fill', d => colors(d.key));

堆叠柱状图 #

javascript
const keys = ['value1', 'value2'];
const stack = d3.stack().keys(keys);
const stackedData = stack(data);

const x = d3.scaleBand()
  .domain(data.map(d => d.category))
  .range([0, width])
  .padding(0.2);

const y = d3.scaleLinear()
  .domain([0, d3.max(stackedData, d => d3.max(d, d => d[1]))])
  .nice()
  .range([height, 0]);

const colors = d3.scaleOrdinal()
  .domain(keys)
  .range(['steelblue', 'orange']);

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

g.selectAll('.serie')
  .data(stackedData)
  .enter()
  .append('g')
  .attr('class', 'serie')
  .attr('fill', d => colors(d.key))
  .selectAll('rect')
  .data(d => d)
  .enter()
  .append('rect')
  .attr('x', d => x(d.data.category))
  .attr('y', d => y(d[1]))
  .attr('height', d => y(d[0]) - y(d[1]))
  .attr('width', x.bandwidth());

折线图 #

基本折线图 #

javascript
const data = [
  { date: new Date(2020, 0, 1), value: 10 },
  { date: new Date(2020, 1, 1), value: 20 },
  { date: new Date(2020, 2, 1), value: 15 },
  { date: new Date(2020, 3, 1), value: 25 },
  { date: new Date(2020, 4, 1), value: 18 }
];

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)])
  .nice()
  .range([height, 0]);

const line = d3.line()
  .x(d => x(d.date))
  .y(d => y(d.value))
  .curve(d3.curveMonotoneX);

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x).ticks(5));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

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

g.selectAll('.dot')
  .data(data)
  .enter()
  .append('circle')
  .attr('class', 'dot')
  .attr('cx', d => x(d.date))
  .attr('cy', d => y(d.value))
  .attr('r', 4)
  .attr('fill', 'steelblue');

多系列折线图 #

javascript
const data = [
  { date: new Date(2020, 0, 1), series1: 10, series2: 15 },
  { date: new Date(2020, 1, 1), series1: 20, series2: 25 },
  { date: new Date(2020, 2, 1), series1: 15, series2: 20 }
];

const keys = ['series1', 'series2'];
const colors = d3.scaleOrdinal()
  .domain(keys)
  .range(['steelblue', 'orange']);

const x = d3.scaleTime()
  .domain(d3.extent(data, d => d.date))
  .range([0, width]);

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => Math.max(d.series1, d.series2))])
  .nice()
  .range([height, 0]);

keys.forEach(key => {
  const line = d3.line()
    .x(d => x(d.date))
    .y(d => y(d[key]))
    .curve(d3.curveMonotoneX);
  
  g.append('path')
    .datum(data)
    .attr('fill', 'none')
    .attr('stroke', colors(key))
    .attr('stroke-width', 2)
    .attr('d', line);
});

面积图 #

基本面积图 #

javascript
const area = d3.area()
  .x(d => x(d.date))
  .y0(height)
  .y1(d => y(d.value))
  .curve(d3.curveMonotoneX);

g.append('path')
  .datum(data)
  .attr('fill', 'steelblue')
  .attr('fill-opacity', 0.5)
  .attr('d', area);

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

堆叠面积图 #

javascript
const keys = ['series1', 'series2'];
const stack = d3.stack().keys(keys);
const stackedData = stack(data);

const area = d3.area()
  .x(d => x(d.data.date))
  .y0(d => y(d[0]))
  .y1(d => y(d[1]))
  .curve(d3.curveMonotoneX);

g.selectAll('.area')
  .data(stackedData)
  .enter()
  .append('path')
  .attr('class', 'area')
  .attr('fill', d => colors(d.key))
  .attr('fill-opacity', 0.7)
  .attr('d', area);

散点图 #

基本散点图 #

javascript
const data = [
  { x: 10, y: 20, size: 5, category: 'A' },
  { x: 30, y: 40, size: 10, category: 'B' },
  { x: 50, y: 60, size: 15, category: 'A' },
  { x: 70, y: 30, size: 8, category: 'B' }
];

const x = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.x)])
  .nice()
  .range([0, width]);

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

const size = d3.scaleSqrt()
  .domain([0, d3.max(data, d => d.size)])
  .range([2, 20]);

const color = d3.scaleOrdinal()
  .domain(['A', 'B'])
  .range(['steelblue', 'orange']);

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

g.selectAll('.point')
  .data(data)
  .enter()
  .append('circle')
  .attr('class', 'point')
  .attr('cx', d => x(d.x))
  .attr('cy', d => y(d.y))
  .attr('r', d => size(d.size))
  .attr('fill', d => color(d.category))
  .attr('opacity', 0.7);

带回归线的散点图 #

javascript
const linearRegression = data => {
  const n = data.length;
  let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
  
  data.forEach(d => {
    sumX += d.x;
    sumY += d.y;
    sumXY += d.x * d.y;
    sumX2 += d.x * d.x;
  });
  
  const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
  const intercept = (sumY - slope * sumX) / n;
  
  return { slope, intercept };
};

const regression = linearRegression(data);

g.append('line')
  .attr('x1', x(0))
  .attr('y1', y(regression.intercept))
  .attr('x2', x(d3.max(data, d => d.x)))
  .attr('y2', y(regression.slope * d3.max(data, d => d.x) + regression.intercept))
  .attr('stroke', 'red')
  .attr('stroke-width', 2)
  .attr('stroke-dasharray', '5,5');

饼图 #

基本饼图 #

javascript
const data = [
  { name: 'A', value: 30 },
  { name: 'B', value: 80 },
  { name: 'C', value: 45 },
  { name: 'D', value: 60 }
];

const radius = Math.min(width, height) / 2;

const pie = d3.pie()
  .value(d => d.value)
  .sort(null);

const arc = d3.arc()
  .innerRadius(0)
  .outerRadius(radius);

const color = d3.scaleOrdinal()
  .domain(data.map(d => d.name))
  .range(d3.schemeCategory10);

const svg = d3.select('#chart')
  .append('svg')
  .attr('width', width)
  .attr('height', height);

const g = svg.append('g')
  .attr('transform', `translate(${width / 2}, ${height / 2})`);

const arcs = g.selectAll('.arc')
  .data(pie(data))
  .enter()
  .append('g')
  .attr('class', 'arc');

arcs.append('path')
  .attr('d', arc)
  .attr('fill', d => color(d.data.name))
  .attr('stroke', 'white')
  .attr('stroke-width', 2);

arcs.append('text')
  .attr('transform', d => `translate(${arc.centroid(d)})`)
  .attr('text-anchor', 'middle')
  .attr('dy', '0.35em')
  .text(d => d.data.name);

环形图 #

javascript
const arc = d3.arc()
  .innerRadius(radius * 0.5)
  .outerRadius(radius);

带标签的饼图 #

javascript
const outerArc = d3.arc()
  .innerRadius(radius * 0.9)
  .outerRadius(radius * 0.9);

arcs.append('polyline')
  .attr('points', d => {
    const pos = outerArc.centroid(d);
    pos[0] = radius * (d.startAngle + d.endAngle < Math.PI ? 1 : -1);
    return [arc.centroid(d), outerArc.centroid(d), pos];
  })
  .attr('fill', 'none')
  .attr('stroke', 'black');

arcs.append('text')
  .attr('transform', d => {
    const pos = outerArc.centroid(d);
    pos[0] = radius * 0.95 * (d.startAngle + d.endAngle < Math.PI ? 1 : -1);
    return `translate(${pos})`;
  })
  .attr('text-anchor', d => d.startAngle + d.endAngle < Math.PI ? 'start' : 'end')
  .text(d => `${d.data.name}: ${d.data.value}`);

直方图 #

javascript
const data = d3.range(1000).map(d3.randomNormal(50, 10));

const x = d3.scaleLinear()
  .domain(d3.extent(data))
  .nice()
  .range([0, width]);

const bins = d3.bin()
  .domain(x.domain())
  .thresholds(20)
  (data);

const y = d3.scaleLinear()
  .domain([0, d3.max(bins, d => d.length)])
  .nice()
  .range([height, 0]);

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x));

g.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(y));

g.selectAll('.bar')
  .data(bins)
  .enter()
  .append('rect')
  .attr('class', 'bar')
  .attr('x', d => x(d.x0) + 1)
  .attr('y', d => y(d.length))
  .attr('width', d => Math.max(0, x(d.x1) - x(d.x0) - 1))
  .attr('height', d => height - y(d.length))
  .attr('fill', 'steelblue');

图表最佳实践 #

1. 响应式设计 #

javascript
function resize() {
  const width = container.node().clientWidth - margin.left - margin.right;
  const height = container.node().clientHeight - margin.top - margin.bottom;
  
  svg.attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom);
  
  x.range([0, width]);
  y.range([height, 0]);
  
  g.select('.x-axis').call(d3.axisBottom(x));
  g.select('.y-axis').call(d3.axisLeft(y));
  
  g.selectAll('.bar')
    .attr('x', d => x(d.category))
    .attr('y', d => y(d.value))
    .attr('height', d => height - y(d.value));
}

d3.select(window).on('resize', resize);

2. 动画效果 #

javascript
g.selectAll('.bar')
  .data(data)
  .enter()
  .append('rect')
  .attr('y', height)
  .attr('height', 0)
  .transition()
  .duration(500)
  .delay((d, i) => i * 50)
  .attr('y', d => y(d.value))
  .attr('height', d => height - y(d.value));

3. 交互提示 #

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

g.selectAll('.bar')
  .data(data)
  .enter()
  .append('rect')
  .on('mouseover', function(event, d) {
    tooltip.transition()
      .duration(200)
      .style('opacity', 0.9);
    tooltip.html(`Category: ${d.category}<br>Value: ${d.value}`)
      .style('left', (event.pageX + 10) + 'px')
      .style('top', (event.pageY - 28) + 'px');
  })
  .on('mouseout', function() {
    tooltip.transition()
      .duration(500)
      .style('opacity', 0);
  });

下一步 #

现在你已经掌握了常见图表的实现,接下来学习 高级主题,了解性能优化、模块化等高级技巧!

最后更新:2026-03-28