D3 简介

过渡动画

掌握 transition 过渡动画的用法,控制时长、延迟和缓动,让图表数据变化平滑生动。

🎯 引言

静态图表能看,但动起来才抓人:柱子从底部“长”出来、数据更新时平滑地升降。学会这篇文章,你能掌握 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 瞬间跳变,右边平滑过渡

🧱 动画三要素

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 只做一件事:把每根柱子的 yheight 从旧值平滑过渡到新值。视觉上就是柱子“伸缩”到新高度,比瞬间跳变舒服得多。

点「刷新数据」,观察柱子平滑地升降到新高度:


🪤 注意事项

  • 不要滥用动画:入场和数据更新时动一下就够,每次都动用户会眼花缭乱。
  • 连续触发会打架:上一个动画没结束新数据又来了,柱子会“抽搐”。可以在新动画前加一句 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 方案,初学阶段不用担心。


🧪 小练习

给柱状图添加一个“刷新数据”按钮,实现更新动画:

  1. 点击按钮时用 Math.random() 生成 5 个 0 到 200 之间的随机数。
  2. 重新绑定数据并加上 600 毫秒的 transition。
  3. 连续快速点击按钮,观察柱子平滑升降的效果。
views/AnimatedChart.vue
<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>

🎉 恭喜你已经让图表动起来了!下一篇我们给图表加上鼠标交互和缩放,让它不仅会动,还会“回应”。