pin_drop当前位置:知识文库 ❯ 图文
数据可视化:D3.js基础 - 从入门到实践详解
教程简介
D3.js(Data-Driven Documents)是最强大的JavaScript数据可视化库,以数据驱动和底层控制著称。与Chart.js和ECharts等高层图表库不同,D3.js提供了构建可视化的基础工具集,让你能够精确控制每一个视觉元素。本教程将系统讲解D3.js的核心概念,包括选择集、数据绑定(enter/update/exit)、比例尺(scale)、坐标轴(axis)、过渡动画(transition)以及D3与SVG/Canvas的结合使用。
核心概念
D3.js的设计哲学
D3.js不是图表库,而是数据可视化工具集。它不提供"开箱即用"的图表类型,而是提供构建图表的基础模块:
数据绑定:将数据与DOM元素关联
比例尺:数据空间到视觉空间的映射
坐标轴:自动生成坐标轴组件
过渡动画:平滑的状态变化
形状生成器:路径、面积、线条等SVG路径生成
选择集(Selection)
D3的选择集是操作DOM的核心:
代码示例
// 选择元素
d3.select('#chart') // 选择单个元素
d3.selectAll('.bar') // 选择多个元素
// 链式操作
d3.select('#chart')
.append('svg')
.attr('width', 800)
.attr('height', 400)
.style('background', '#fff');数据绑定(Data Join)
数据绑定是D3最核心的概念,将数据数组与DOM元素集关联:
代码示例
const data = [10, 20, 30, 40, 50];
d3.select('svg')
.selectAll('rect')
.data(data)
.join('rect') // D3 v5+ 简化写法
.attr('x', (d, i) => i * 50)
.attr('y', d => 300 - d * 5)
.attr('width', 40)
.attr('height', d => d * 5)
.attr('fill', '#4e79a7');Enter/Update/Exit模式:
代码示例
const rects = d3.select('svg').selectAll('rect').data(data);
// Enter:新数据对应的元素
rects.enter().append('rect')
.attr('fill', '#4e79a7');
// Update:已有数据对应的元素
rects.attr('fill', '#f28e2b');
// Exit:不再需要的元素
rects.exit().remove();比例尺(Scale)
比例尺将数据域映射到视觉范围:
代码示例
// 线性比例尺
const yScale = d3.scaleLinear()
.domain([0, 100]) // 数据范围
.range([300, 0]); // 像素范围(注意反转)
// 序数比例尺(分类)
const colorScale = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#4e79a7', '#f28e2b', '#e15759']);
// 时间比例尺
const xScale = d3.scaleTime()
.domain([new Date(2024, 0, 1), new Date(2024, 11, 31)])
.range([0, 800]);
// 带宽比例尺(柱状图)
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 800])
.padding(0.2);坐标轴(Axis)
D3自动生成SVG坐标轴:
代码示例
const xAxis = d3.axisBottom(xScale)
.ticks(5)
.tickFormat(d3.format('.0f'));
const yAxis = d3.axisLeft(yScale)
.ticks(5);
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);过渡动画(Transition)
代码示例
// 基本过渡
rects.transition()
.duration(750)
.ease(d3.easeCubicOut)
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value));
// 延迟过渡
rects.transition()
.delay((d, i) => i * 50)
.duration(500)
.attr('fill', '#e15759');代码示例
示例1:D3.js柱状图(完整数据绑定)
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js柱状图</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; display: flex; justify-content: center; padding: 30px 20px; }
.chart-container { background: #fff; border-radius: 16px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); width: 100%; max-width: 900px; }
.chart-title { text-align: center; color: #2c3e50; font-size: 20px; margin-bottom: 15px; }
.controls { display: flex; gap: 8px; justify-content: center; margin-bottom: 15px; }
.btn { padding: 6px 16px; border: 1px solid #ddd; border-radius: 6px; background: #fff; color: #666; font-size: 12px; cursor: pointer; transition: all 0.2s; }
.btn:hover { border-color: #4e79a7; color: #4e79a7; }
.btn.active { background: #4e79a7; color: #fff; border-color: #4e79a7; }
svg { width: 100%; }
.bar { cursor: pointer; transition: opacity 0.2s; }
.bar:hover { opacity: 0.8; }
.axis text { font-family: -apple-system, sans-serif; font-size: 12px; fill: #888; }
.axis line, .axis path { stroke: #ddd; }
.grid line { stroke: #f0f0f0; }
.grid path { stroke: none; }
.tooltip { position: absolute; background: rgba(44,62,80,0.9); color: #fff; padding: 6px 12px; border-radius: 4px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.15s; }
</style>
</head>
<body>
<div class="chart-container">
<h2 class="chart-title">D3.js 柱状图 - 数据绑定演示</h2>
<div class="controls">
<button class="btn" onclick="updateData('sales')">销售额</button>
<button class="btn" onclick="updateData('profit')">利润</button>
<button class="btn" onclick="updateData('users')">用户数</button>
<button class="btn" onclick="addBar()">添加数据</button>
<button class="btn" onclick="removeBar()">移除数据</button>
</div>
<div id="chart"></div>
<div class="tooltip" id="tooltip"></div>
</div>
<script>
const allData = {
sales: [{name:'北京',value:218},{name:'上海',value:248},{name:'广州',value:186},{name:'深圳',value:175},{name:'成都',value:209},{name:'杭州',value:123},{name:'武汉',value:136}],
profit: [{name:'北京',value:45},{name:'上海',value:62},{name:'广州',value:38},{name:'深圳',value:52},{name:'成都',value:35},{name:'杭州',value:28},{name:'武汉',value:31}],
users: [{name:'北京',value:890},{name:'上海',value:1200},{name:'广州',value:670},{name:'深圳',value:580},{name:'成都',value:750},{name:'杭州',value:420},{name:'武汉',value:510}]
};
let currentData = [...allData.sales];
const margin = { top: 20, right: 20, bottom: 40, left: 55 };
const width = 840 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select('#chart').append('svg')
.attr('viewBox', `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// 比例尺
const xScale = d3.scaleBand().range([0, width]).padding(0.3);
const yScale = d3.scaleLinear().range([height, 0]);
const colorScale = d3.scaleOrdinal().range(['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ac']);
// 坐标轴
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale).ticks(5);
const xAxisG = svg.append('g').attr('class', 'axis').attr('transform', `translate(0,${height})`);
const yAxisG = svg.append('g').attr('class', 'axis');
// 网格线
const gridG = svg.append('g').attr('class', 'grid');
const tooltip = document.getElementById('tooltip');
function update(data) {
currentData = data;
xScale.domain(data.map(d => d.name));
yScale.domain([0, d3.max(data, d => d.value) * 1.1]);
// 更新坐标轴
xAxisG.transition().duration(500).call(xAxis);
yAxisG.transition().duration(500).call(yAxis);
// 网格线
gridG.selectAll('line').remove();
yScale.ticks(5).forEach(tick => {
gridG.append('line')
.attr('x1', 0).attr('x2', width)
.attr('y1', yScale(tick)).attr('y2', yScale(tick))
.attr('stroke', '#f0f0f0');
});
// 数据绑定 - Enter/Update/Exit
const bars = svg.selectAll('.bar').data(data, d => d.name);
// Exit
bars.exit()
.transition().duration(300)
.attr('y', height)
.attr('height', 0)
.style('opacity', 0)
.remove();
// Enter
const barsEnter = bars.enter().append('rect')
.attr('class', 'bar')
.attr('x', d => xScale(d.name))
.attr('y', height)
.attr('width', xScale.bandwidth())
.attr('height', 0)
.attr('fill', (d, i) => colorScale(i))
.attr('rx', 4)
.on('mouseenter', function(event, d) {
tooltip.style.opacity = 1;
tooltip.innerHTML = `${d.name}: ${d.value}`;
d3.select(this).attr('opacity', 0.8);
})
.on('mousemove', function(event) {
tooltip.style.left = event.pageX + 10 + 'px';
tooltip.style.top = event.pageY - 30 + 'px';
})
.on('mouseleave', function() {
tooltip.style.opacity = 0;
d3.select(this).attr('opacity', 1);
});
// Update + Enter
bars.merge(barsEnter)
.transition().duration(500)
.ease(d3.easeCubicOut)
.attr('x', d => xScale(d.name))
.attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('fill', (d, i) => colorScale(i));
// 数值标签
const labels = svg.selectAll('.val-label').data(data, d => d.name);
labels.exit().transition().duration(300).style('opacity', 0).remove();
labels.enter().append('text').attr('class', 'val-label')
.merge(labels)
.transition().duration(500)
.attr('x', d => xScale(d.name) + xScale.bandwidth() / 2)
.attr('y', d => yScale(d.value) - 8)
.attr('text-anchor', 'middle')
.attr('fill', '#666')
.attr('font-size', '12px')
.attr('font-weight', '600')
.text(d => d.value);
}
function updateData(type) {
document.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
update([...allData[type]]);
}
function addBar() {
const names = ['南京','重庆','西安','苏州','天津'];
const name = names[currentData.length % names.length];
if (currentData.find(d => d.name === name)) return;
currentData.push({ name, value: Math.round(50 + Math.random() * 200) });
update([...currentData]);
}
function removeBar() {
if (currentData.length <= 2) return;
currentData.pop();
update([...currentData]);
}
update(currentData);
</script>
</body>
</html>示例2:D3.js折线图与坐标轴
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js折线图</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #f5f7fa; display: flex; justify-content: center; padding: 30px 20px; }
.chart-container { background: #fff; border-radius: 16px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); width: 100%; max-width: 900px; }
.chart-title { text-align: center; color: #2c3e50; font-size: 20px; margin-bottom: 15px; }
svg { width: 100%; }
.axis text { font-size: 11px; fill: #888; }
.axis line, .axis path { stroke: #ddd; }
.line { fill: none; stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.area { opacity: 0.15; }
.dot { cursor: pointer; }
.dot:hover { r: 6; }
.crosshair { stroke: rgba(0,0,0,0.1); stroke-dasharray: 4 4; }
.focus-text { font-size: 12px; font-weight: 600; }
</style>
</head>
<body>
<div class="chart-container">
<h2 class="chart-title">D3.js 折线图 - 比例尺与坐标轴</h2>
<div id="lineChart"></div>
</div>
<script>
const lineData = [
{date: '2024-01', value: 30}, {date: '2024-02', value: 45},
{date: '2024-03', value: 42}, {date: '2024-04', value: 58},
{date: '2024-05', value: 65}, {date: '2024-06', value: 72},
{date: '2024-07', value: 68}, {date: '2024-08', value: 85},
{date: '2024-09', value: 78}, {date: '2024-10', value: 92},
{date: '2024-11', value: 88}, {date: '2024-12', value: 95}
];
lineData.forEach(d => d.date = d3.timeParse('%Y-%m')(d.date));
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const width = 840 - margin.left - margin.right;
const height = 380 - margin.top - margin.bottom;
const svg = d3.select('#lineChart').append('svg')
.attr('viewBox', `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
// 比例尺
const xScale = d3.scaleTime()
.domain(d3.extent(lineData, d => d.date))
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(lineData, d => d.value) * 1.15])
.range([height, 0]);
// 坐标轴
svg.append('g').attr('class', 'axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale).ticks(6).tickFormat(d3.timeFormat('%m月')));
svg.append('g').attr('class', 'axis')
.call(d3.axisLeft(yScale).ticks(5));
// 网格线
svg.append('g').selectAll('line')
.data(yScale.ticks(5)).join('line')
.attr('x1', 0).attr('x2', width)
.attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
.attr('stroke', '#f0f0f0');
// 面积生成器
const area = d3.area()
.x(d => xScale(d.date))
.y0(height)
.y1(d => yScale(d.value))
.curve(d3.curveMonotoneX);
svg.append('path')
.datum(lineData)
.attr('class', 'area')
.attr('d', area)
.attr('fill', '#4e79a7');
// 线条生成器
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
// 线条绘制动画
const path = svg.append('path')
.datum(lineData)
.attr('class', 'line')
.attr('d', line)
.attr('stroke', '#4e79a7');
const totalLength = path.node().getTotalLength();
path.attr('stroke-dasharray', totalLength)
.attr('stroke-dashoffset', totalLength)
.transition().duration(1500).ease(d3.easeCubicOut)
.attr('stroke-dashoffset', 0);
// 数据点
svg.selectAll('.dot')
.data(lineData).join('circle')
.attr('class', 'dot')
.attr('cx', d => xScale(d.date))
.attr('cy', d => yScale(d.value))
.attr('r', 4)
.attr('fill', '#fff')
.attr('stroke', '#4e79a7')
.attr('stroke-width', 2)
.style('opacity', 0)
.transition().delay(1500).duration(300)
.style('opacity', 1);
// 交互:十字准线
const focus = svg.append('g').style('display', 'none');
focus.append('line').attr('class', 'crosshair').attr('y1', 0).attr('y2', height);
focus.append('circle').attr('r', 5).attr('fill', '#4e79a7').attr('stroke', '#fff').attr('stroke-width', 2);
focus.append('text').attr('class', 'focus-text').attr('dy', '-10');
svg.append('rect')
.attr('width', width).attr('height', height)
.attr('fill', 'none').attr('pointer-events', 'all')
.on('mousemove', function(event) {
const [mx] = d3.pointer(event);
const x0 = xScale.invert(mx);
const i = d3.bisector(d => d.date).left(lineData, x0, 1);
const d0 = lineData[i - 1], d1 = lineData[i];
if (!d1) return;
const d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus.style('display', null);
focus.select('line').attr('x1', xScale(d.date)).attr('x2', xScale(d.date));
focus.select('circle').attr('cx', xScale(d.date)).attr('cy', yScale(d.value));
focus.select('text').attr('x', xScale(d.date)).attr('y', yScale(d.value))
.attr('text-anchor', 'middle')
.text(`${d3.timeFormat('%m月')(d.date)}: ${d.value}`);
})
.on('mouseleave', () => focus.style('display', 'none'));
</script>
</body>
</html>示例3:D3.js过渡动画与交互
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js过渡动画</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #f5f7fa; display: flex; justify-content: center; padding: 30px 20px; }
.container { background: #fff; border-radius: 16px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); width: 100%; max-width: 900px; }
h2 { text-align: center; color: #2c3e50; margin-bottom: 15px; }
svg { width: 100%; }
.controls { display: flex; gap: 8px; justify-content: center; margin-bottom: 15px; flex-wrap: wrap; }
.btn { padding: 8px 18px; border: none; border-radius: 8px; background: #4e79a7; color: #fff; font-size: 13px; cursor: pointer; }
.btn:hover { background: #3d6a96; }
</style>
</head>
<body>
<div class="container">
<h2>D3.js 过渡动画演示</h2>
<div class="controls">
<button class="btn" onclick="shuffle()">随机数据</button>
<button class="btn" onclick="sortAsc()">升序排列</button>
<button class="btn" onclick="sortDesc()">降序排列</button>
</div>
<div id="animChart"></div>
</div>
<script>
let data = d3.range(15).map(i => ({ id: i, value: Math.round(Math.random() * 80 + 20) }));
const colorScale = d3.scaleOrdinal(d3.schemeTableau10);
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 840 - margin.left - margin.right;
const height = 350 - margin.top - margin.bottom;
const svg = d3.select('#animChart').append('svg')
.attr('viewBox', `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleBand().range([0, width]).padding(0.25);
const yScale = d3.scaleLinear().range([height, 0]);
svg.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${height})`);
svg.append('g').attr('class', 'y-axis');
function render(data) {
xScale.domain(data.map((_, i) => i));
yScale.domain([0, 100]);
svg.select('.x-axis').call(d3.axisBottom(xScale).tickFormat(''));
svg.select('.y-axis').call(d3.axisLeft(yScale).ticks(5));
const bars = svg.selectAll('.bar').data(data, d => d.id);
bars.enter().append('rect')
.attr('class', 'bar')
.attr('x', (_, i) => xScale(i))
.attr('y', height)
.attr('width', xScale.bandwidth())
.attr('height', 0)
.attr('fill', d => colorScale(d.id))
.attr('rx', 4)
.merge(bars)
.transition().duration(600).ease(d3.easeCubicOut)
.attr('x', (_, i) => xScale(i))
.attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('fill', d => colorScale(d.id));
bars.exit().transition().duration(300).attr('height', 0).attr('y', height).remove();
// 数值标签
const labels = svg.selectAll('.label').data(data, d => d.id);
labels.enter().append('text').attr('class', 'label')
.attr('text-anchor', 'middle').attr('fill', '#666').attr('font-size', '11px').attr('font-weight', '600')
.merge(labels)
.transition().duration(600).ease(d3.easeCubicOut)
.attr('x', (_, i) => xScale(i) + xScale.bandwidth() / 2)
.attr('y', d => yScale(d.value) - 6)
.text(d => d.value);
labels.exit().remove();
}
function shuffle() {
data = d3.range(15).map(i => ({ id: i, value: Math.round(Math.random() * 80 + 20) }));
render(data);
}
function sortAsc() {
data = [...data].sort((a, b) => a.value - b.value);
render(data);
}
function sortDesc() {
data = [...data].sort((a, b) => b.value - a.value);
render(data);
}
render(data);
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
数据键函数:数据绑定时使用key函数(
.data(data, d => d.id))确保元素与数据正确关联。过渡性能:避免同时运行过多过渡动画,控制过渡元素数量。
比例尺范围:Y轴比例尺的range注意反转(
[height, 0]),因为SVG坐标系Y轴向下。坐标轴样式:通过CSS统一控制坐标轴样式,而非逐个设置属性。
事件处理:D3 v6+使用
function(event, d)参数顺序,与v5不同。响应式:使用viewBox实现SVG响应式,监听resize重绘。
内存管理:移除不需要的元素时使用
.exit().remove(),避免DOM膨胀。模块化引入:生产环境使用按需引入,减小包体积。
代码规范示例
代码示例
// 好的做法:D3图表封装
function createBarChart(container, options) {
const { margin, width, height } = options;
const svg = d3.select(container).append('svg')
.attr('viewBox', `0 0 ${width} ${height}`)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleBand().range([0, width - margin.left - margin.right]).padding(0.3);
const yScale = d3.scaleLinear().range([height - margin.top - margin.bottom, 0]);
function update(data) {
xScale.domain(data.map(d => d.name));
yScale.domain([0, d3.max(data, d => d.value)]);
svg.selectAll('.bar')
.data(data, d => d.name)
.join(
enter => enter.append('rect').attr('class', 'bar').attr('y', height).attr('height', 0),
update => update,
exit => exit.transition().duration(300).attr('height', 0).remove()
)
.transition().duration(500)
.attr('x', d => xScale(d.name))
.attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', d => height - margin.top - margin.bottom - yScale(d.value));
}
return { update };
}常见问题与解决方案
问题1:数据更新时元素错位
原因:未使用key函数,D3按索引匹配数据。
解决方案:使用.data(data, d => d.id)确保按数据标识匹配。
问题2:过渡动画不流畅
原因:同时运行的过渡过多。
解决方案:使用.interrupt()中断旧过渡,减少同时过渡的元素数。
问题3:坐标轴标签重叠
解决方案:调整ticks数量、旋转标签、使用.tickFormat()格式化。
问题4:SVG文本不换行
解决方案:使用或手动分行。
总结
本教程系统讲解了D3.js的基础知识:
选择集:select/selectAll、链式操作
数据绑定:enter/update/exit模式、join方法
比例尺:linear、ordinal、time、band等
坐标轴:自动生成、样式控制
过渡动画:transition、ease、delay
D3与SVG:路径生成器、形状生成器
交互:鼠标事件、十字准线、tooltip
D3.js是数据可视化的瑞士军刀,掌握其核心概念后可以构建任意定制化的可视化效果。
常见问题
什么是D3.js基础?
D3.js基础是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习D3.js基础的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
D3.js基础有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
D3.js基础适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
D3.js基础的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别