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