交互与缩放
🎯 引言
图表能看能动,还差最后一步:能交互。鼠标悬停时高亮柱子、显示数值提示框、滚轮缩放图表,这些都是真实项目的标配。学会这篇文章,你能掌握 D3 的事件绑定、tooltip 实现和 zoom 缩放,让图表真正“活”起来。
🧱 绑定事件 .on()
D3 用 .on() 给元素绑定事件,和原生 JS 的 addEventListener 很像:
svg.selectAll('rect').on('mouseenter', function (event, d) {
console.log(d); // { day: '周一', value: 120 } —— 这根柱子绑定的数据
});
事件回调会收到两个参数:
event:浏览器原生事件对象,鼠标位置等信息都在里面。d:这个元素绑定的数据。悬停哪根柱子,d就是哪根柱子的数据。
想修改“当前被悬停的这一根”柱子,通过 this 拿到它:
svg.selectAll('rect')
.on('mouseenter', function (event, d) {
d3.select(this).attr('fill', 'orange'); // 只改变当前这根
})
.on('mouseleave', function (event, d) {
d3.select(this).attr('fill', '#5470c6'); // 移开后还原
});
- 回调参数是
(event, d),事件在前、数据在后。网上老教程写的(d, i)是 D3 v5 及以前的写法,v7 已经改了,照抄会拿到错误的数据。 - 想用
this拿当前元素,必须用function普通函数,不能用箭头函数——箭头函数没有自己的this。
把鼠标悬停到柱子上,试试高亮效果:
把鼠标悬停到柱子上试试
🛠 实现 tooltip 提示框
tooltip 是最常见的交互:悬停柱子时,鼠标旁边显示一个数值小卡片。实现思路是用一个绝对定位的 div,三个事件分工配合:
| 事件 | 干什么 |
|---|---|
mouseenter | 显示 tooltip,填入数据 |
mousemove | 让 tooltip 跟着鼠标走 |
mouseleave | 隐藏 tooltip |
<template>
<div class="wrapper">
<svg ref="svgRef" class="chart"></svg>
<!-- tooltip 容器,默认隐藏 -->
<div ref="tooltipRef" class="tooltip"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import * as d3 from 'd3';
const svgRef = ref(null);
const tooltipRef = ref(null);
onMounted(() => {
const svg = d3.select(svgRef.value);
const tooltip = d3.select(tooltipRef.value);
// ……比例尺和画柱子的代码同上一篇,此处省略,重点是事件部分
svg.selectAll('rect')
.on('mouseenter', function (event, d) {
tooltip.style('opacity', 1).text(`${d.day}:${d.value}`); // 显示数值
})
.on('mousemove', function (event, d) {
// offsetX/offsetY 是鼠标相对容器的位置,让提示框跟在鼠标右下方
tooltip
.style('left', event.offsetX + 10 + 'px')
.style('top', event.offsetY + 10 + 'px');
})
.on('mouseleave', function (event, d) {
tooltip.style('opacity', 0); // 隐藏
});
});
</script>
<style scoped>
.wrapper {
position: relative; /* 让 tooltip 相对容器定位 */
}
.tooltip {
position: absolute;
opacity: 0; /* 默认隐藏 */
pointer-events: none; /* 关键:tooltip 自己不响应鼠标 */
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
</style>
pointer-events: none 是关键细节:没有它,tooltip 会挡住鼠标,柱子反复触发“进入/离开”,提示框就会疯狂闪烁。
把鼠标悬停到下面的柱子上,试试真实的 tooltip 效果:
🧱 d3.zoom 缩放平移
数据点多或者做大屏时,用户会想滚轮放大看细节、按住拖拽平移。D3 的 zoom 开箱即用:
const g = svg.append('g'); // 所有图形画在这个 g 里
const zoom = d3
.zoom()
.scaleExtent([0.5, 8]) // 缩放范围:最小 0.5 倍、最大 8 倍
.on('zoom', (event) => {
g.attr('transform', event.transform); // 把缩放结果应用到 g 上
});
d3.select(svgRef.value).call(zoom);
原理很直白:zoom 捕获滚轮和拖拽操作,换算成一个 transform(平移 + 缩放),回调里把它应用到装图形的 g 上,整个图表就跟着缩放平移了。手机上的双指缩放、单指拖动也自动支持。
scaleExtent 一定要设置,否则用户可以无限缩小,图表一下子就“飞没了”。下面是一个可以缩放平移的散点图,滚轮缩放、按住拖拽试试:
滚轮缩放,按住拖拽平移(范围 0.5 ~ 8 倍)
🧾 小节总结
.on()绑定事件,回调参数是(event, d):事件在前、数据在后。- 用普通
function写回调,通过d3.select(this)操作当前元素。 - tooltip 三件套:
mouseenter显示、mousemove跟随、mouseleave隐藏,记得pointer-events: none。 d3.zoom()实现滚轮缩放和拖拽平移,scaleExtent限制缩放范围。- zoom 回调里把
event.transform应用到装图形的g容器上。
❓ 知识问答
Q1:tooltip 为什么不用 SVG 的 title 元素?
title 是浏览器原生提示,样式无法定制、显示还有延迟。用 div 自己实现可以任意美化,这也是主流图表库的做法。
Q2:点击事件能拿到数据吗?
能,.on('click', function (event, d) {}) 里的 d 就是该元素绑定的数据,常用于点击柱子跳转详情页。
Q3:zoom 缩放后坐标轴文字会跟着变形吗?
会,因为坐标轴也在被变换的 g 里。想保持刻度文字清晰,需要在回调里重新计算比例尺并重绘坐标轴,属于进阶内容,初学先掌握整体缩放。
Q4:事件里的 d 是 undefined 是怎么回事?
两种常见原因:用了箭头函数导致参数错位,或者照抄了 v5 老教程的 (d, i) 写法。v7 的正确写法是 function (event, d)。
🧪 小练习
给之前的柱状图加上完整交互:
- 悬停柱子时高亮为橙色,并在 tooltip 中显示“星期 + 数值”。
- tooltip 跟随鼠标移动,离开后隐藏。
- 给图表加上
d3.zoom,缩放范围限制在 0.8 到 5 倍。
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d) => xScale(d.day))
.attr('y', (d) => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', (d) => height - yScale(d.value))
.attr('fill', '#5470c6');
// 请在这里编写代码(事件绑定 + tooltip + zoom)
🎉 恭喜你已经掌握 D3 的交互技能!下一篇我们把所有知识收拢,在 Vue3 项目中封装一个可复用的图表组件。
