过渡动画
🎯 引言
静态图表能看,但动起来才抓人:柱子从底部“长”出来、数据更新时平滑地升降。学会这篇文章,你能掌握 transition 的用法,控制动画的时长、延迟和节奏,让图表的变化过程流畅自然。
🧱 transition 是什么
没有动画时,attr 的修改是瞬间完成的:高度从 0 直接变成 200,像开关灯一样生硬。
在 attr 前面插入 .transition(),D3 就会在指定时间内一点点地改:
rects.attr('height', 200); // 瞬间:啪的一下变成 200
rects.transition().duration(800).attr('height', 200); // 800 毫秒内:从 0 慢慢长到 200
这 800 毫秒里发生的事,可以想象成这样:
第 0 毫秒: height = 0
第 200 毫秒: height ≈ 50
第 400 毫秒: height ≈ 100
第 600 毫秒: height ≈ 150
第 800 毫秒: height = 200
D3 自动在起始值和结束值之间算出每一帧的中间值,然后逐帧更新——这就是“过渡”。
点按钮对比一下:左边柱子不用 transition 瞬间跳变,右边用 transition 平滑过渡:
🧱 动画三要素
transition() 后面可以链式配置三个参数:
rects
.transition()
.duration(800) // 时长:动画进行多久(毫秒)
.delay((d, i) => i * 100) // 延迟:第几根柱子就晚几百毫秒开始
.ease(d3.easeCubicOut) // 节奏:先快后慢
.attr('height', (d) => d);
- duration:动画时长,常用 300 到 1000 毫秒。
- delay:延迟开始。
(d, i) => i * 100的效果是第 1 根立刻动、第 2 根晚 0.1 秒、第 3 根晚 0.2 秒……柱子一根接一根出场。 - ease:节奏。
d3.easeCubicOut表示开始快、结尾慢,是最自然的运动方式(想想汽车刹车)。
选择不同的 ease 点「播放」,直观感受三种节奏的差异:
🛠 入场动画:柱子从底部长出来
经典效果,套路只有一句话:transition 前设初始状态,transition 后设结束状态。
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d) => xScale(d.day))
.attr('width', xScale.bandwidth())
.attr('fill', '#5470c6')
.attr('y', height) // 初始状态:位置在底部
.attr('height', 0) // 初始状态:高度为 0
.transition() // ↓ 以下属性平滑过渡
.duration(800)
.delay((d, i) => i * 100)
.attr('y', (d) => yScale(d.value)) // 结束状态:真实位置
.attr('height', (d) => height - yScale(d.value)); // 结束状态:真实高度
transition() 是一条分界线:它前面的 attr 是“从哪里开始”,它后面的 attr 是“到哪里去”。顺序写反,动画效果也就反了(柱子会从天上掉下来)。点「重新播放」,观看柱子依次从底部长出来:
🛠 更新动画:数据变了,柱子平滑升降
数据更新时(比如点“刷新”按钮拿到新数据),重新绑定数据再加 transition,已有的柱子就会平滑地升到新高度:
function refresh() {
const newData = [
{ day: '周一', value: 90 },
{ day: '周二', value: 160 },
{ day: '周三', value: 220 },
{ day: '周四', value: 130 },
{ day: '周五', value: 180 },
];
svg.selectAll('rect')
.data(newData) // 按位置配对,5 根柱子都落在 update 区
.transition()
.duration(600)
.attr('y', (d) => yScale(d.value))
.attr('height', (d) => height - yScale(d.value));
}
因为 5 根柱子都还在,D3 只做一件事:把每根柱子的 y 和 height 从旧值平滑过渡到新值。视觉上就是柱子“伸缩”到新高度,比瞬间跳变舒服得多。
点「刷新数据」,观察柱子平滑地升降到新高度:
🪤 注意事项
- 不要滥用动画:入场和数据更新时动一下就够,每次都动用户会眼花缭乱。
- 连续触发会打架:上一个动画没结束新数据又来了,柱子会“抽搐”。可以在新动画前加一句
selection.interrupt()打断旧动画。 - 不是所有属性都能过渡:数值(坐标、宽高)和颜色可以平滑过渡,
fill: none这种非数值属性只能瞬间切换。
🧾 小节总结
transition()插在attr前,把属性的瞬间跳变变成逐帧过渡。- 三要素:
duration时长、delay延迟、ease节奏,常用easeCubicOut。 - 入场动画套路:transition 前设初始状态(高度 0),后设结束状态(真实值)。
- 更新动画:重新
.data()绑定后接transition(),已有元素平滑过渡到新值。 - 动画点到为止;连续触发时用
interrupt()打断旧动画。
❓ 知识问答
Q1:为什么我的动画一闪而过,没有过程?
常见原因是起始状态和结束状态相同(等于没变),或者写反了。检查 transition() 前后的 attr 设置。
Q2:坐标轴能加动画吗?
可以。数据范围变化时重新生成坐标轴并加 transition,刻度会平滑移动,图表显得非常高级:axisSelection.transition().call(d3.axisLeft(newYScale))。
Q3:ease 有哪些常用值?
d3.easeLinear(匀速)、d3.easeCubicIn(先慢后快)、d3.easeCubicOut(先快后慢)、d3.easeBounceOut(落地弹跳)。日常用 easeCubicOut 最自然。
Q4:动画会影响性能吗?
几百个元素以内的 SVG 动画完全没问题。元素上万的场景才需要考虑 Canvas 方案,初学阶段不用担心。
🧪 小练习
给柱状图添加一个“刷新数据”按钮,实现更新动画:
- 点击按钮时用
Math.random()生成 5 个 0 到 200 之间的随机数。 - 重新绑定数据并加上 600 毫秒的 transition。
- 连续快速点击按钮,观察柱子平滑升降的效果。
<template>
<button @click="refresh">刷新数据</button>
<svg ref="svgRef" class="chart"></svg>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import * as d3 from 'd3';
const svgRef = ref(null);
const refresh = () => {
// 请在这里编写代码
};
onMounted(() => {
// 请在这里编写代码(画初始柱状图)
});
</script>
<style scoped>
.chart {
width: 480px;
height: 280px;
}
</style>
🎉 恭喜你已经让图表动起来了!下一篇我们给图表加上鼠标交互和缩放,让它不仅会动,还会“回应”。
