跳转至

过渡与动画 — 从定时器到多过渡编排

这一章讲什么: 数据更新配上平滑动画,是 D3 最招牌的体验。 本章把动画体系拆成三层:定时器(裸时钟,想干嘛干嘛)、过渡(属性自动插值的声明式动画)、 编排(多个过渡并行/串行/打断的规则),最后一节把 SVG/CSS/Canvas 的原生动画能力都拉进来合奏。 读完全章你会理解:过渡不是「特效」,是数据状态的视觉迁移。

1. 顶层全景:三层动画体系

  ③ 编排层:命名过渡、链式过渡、interrupt()、end() promise —— 多过渡的交通规则
  ② 过渡层:selection.transition() + delay/duration/ease + attr/style 自动插值
           + 生命周期事件(start/end/interrupt/cancel)
  ① 定时器层:d3.timeout(延时一次)/ d3.timer(每帧)/ d3.interval(定间隔)
           —— 都基于 requestAnimationFrame,~60 FPS

  图说:底层给时钟,中层给插值,上层给秩序。90% 的图表动画只用中层。

2. 定时器:d3-timer

三个函数对应三种节奏(回调都收到已流逝的毫秒数)1:

函数 节奏 停法
d3.timeout(cb, delay) delay 后执行一次 不用停
d3.timer(cb, delay?) delay 后每帧执行(~60 次/秒) timer.stop()
d3.interval(cb, delay?) delay 后每 delay 毫秒执行一次 timer.stop()
  // 2 秒后每 100 毫秒打印一次(原书示例)
  d3.timeout(() => d3.interval(e => console.log(e), 100), 2000);

  // d3.timer 动画:跑到 5 秒自己停
  let timer = d3.timer(elapsed => {
    /* 每帧画图 */
    if (elapsed >= 5000) timer.stop();
  }, 2000);

与原生 setTimeout/setInterval/requestAnimationFrame 的区别,原书给了两点: 更简单的 API、保证不在后台标签页里空转(省电省资源);多数场合可以直接互换2。 一个坑:restart() 快捷方法只对 d3.timer() 真正好用,对 d3.interval() 会带上 「已经流逝的时间」作为延迟3。

3. 过渡:选择器 + 插值 + 时钟

3.1 最小过渡与三旋钮

过渡永远挂在元素上:selection.transition() 从选择器出发,或 d3.transition() 造一个 全局的再 selectAll 进去。默认参数:延迟 0、时长 250ms、缓动 cubic in-out(两头慢中间快)4:

  d3.selectAll("h1").transition().delay(10000).duration(5000)
    .style("color", "blue");

  // 三旋钮都可传回调:逐元素的延迟 → 波浪效果
  .transition().duration(1000).delay((d,i) => i*50)

注意过渡不是选择器:不能 append、不能 join(transition().append() 直接失败), 但 select/selectAll/filter/each/call 都有过渡版;.selection() 可以换回选择器5。

3.2 缓动:d3-ease 的 28 个函数

缓动(easing)决定「时间进度 → 动画进度」的映射:线性就是匀速;cubic 两头慢中间快; elastic/bounce 会过冲回弹。命名规律:easeXxxIn(起步慢)、easeXxxOut(收尾慢)、 easeXxxInOut(两头慢);光杆 easeXxx 是官方认为最常用形态(一般是 InOut; elastic/bounce 例外,默认 Out)6:

  .ease(d3.easeLinear)                  // 匀速
  .ease(d3.easeCubic)                   // 默认同款
  .ease(d3.easeElastic.period(0.1))     // 弹性,period/amplitude 可调

3.3 attr/style/text/remove 的过渡语义

过渡版的 attr/style 会自动挑插值器:数字用 interpolateNumber、颜色用 interpolateRgb、 字符串里嵌数字用 interpolateString——所以 attr("r", 100) 和 style("fill", "blue") 都不需要你操心中间帧7。两个例外行为要记:

  • text() 不插值——过渡一开始就把文本换成终值(想要滚动数字得用 textTween);
  • remove() 在过渡结束时删除元素,适合离场动画(变透明→移除)。

CSS 类切换(classed)过渡管不了,用 CSS transition 补齐:两个类里都写 transition: all 2s,D3 只负责切类8。

3.4 自定义补间:tween 家族

想精确控制中间帧,用 attrTween / styleTween / textTween / tween。 结构统一是外层工厂 + 内层插值器:外层每个元素运行一次(取初值、建插值器), 内层每帧运行(吃 t∈[0,1],吐这一帧的值)9:

  .attrTween("r", function(d,i) {          // 外层:每个元素一次
    const initialR = +d3.select(this).attr("r");
    return function(t) {                  // 内层:每帧
      return d3.interpolate(initialR, 130)(t);
    };
  });

  // 嵌套箭头函数的紧凑版:数字滚动
  .textTween(() => t => `Step ${1 + t*9}`)

  // tween():不限于单个属性,每帧随便干嘛(返回值没用)
  .tween("_", function() {
    const fill_0 = d3.select(this).style("fill");
    const fillInterpolator = d3.interpolate(fill_0, "blue");
    return function(t) {
      d3.select(this).style("fill", fillInterpolator(t));
      /* 还能顺手改文本、算亮度、联动别的元素 */
    };
  });

3.5 生命周期:事件与 promise

过渡的生命周期:scheduled(已排期)→ 等 delay → running(每帧调插值器, t: 0→1)→ end;途中被新过渡顶替则 interrupt(已开跑)或 cancel(还没开跑)10。

  .on("start", (d,i) => console.log("Started " + i))   // 四种: start/end/interrupt/cancel
  .on("end",   (d,i) => console.log("Ended " + i));

  transition.end()                        // → Promise
    .then(() => …)                        // 正常结束
    .catch(() => …);                      // 被打断/取消(也会吞掉代码错误,小心)
  // async 函数里可以直接:await transition.end();

transition.on 只认这四个事件——'click' 之类的原生事件会报错; 选择器与过渡的 on 可以在同一条链上各挂各的11。

3.6 多过渡的交通规则

同一选择器上连续排多个过渡,后一个会把前一个打断(或取消)——前面的全白搭, 只有最后一个跑完。三件套解法12:

目标 手法
并行不干扰 命名:transition("a") / transition("b"),同名才互斥
严格串行 链式:t.transition() 排在前一个结束后,继承时长缓动
精确时刻 延迟算总账:delay 写成前面所有 delay+duration 之和
  // 并行:三个过渡同时动同一个圆的 r / transform / fill
  d3.select("circle").transition("a")…attr("r", 100);
  d3.select("circle").transition("b")…attr("transform", …);
  d3.select("circle").transition("c")…style("fill", "yellow");

  // 串行:长大 → 移动 → 变色,各 2 秒
  circle.transition().delay(500).duration(2000).attr("r", 100)
    .transition().duration(2000).attr("transform", `translate(600,500)`)
    .transition().duration(2000).style("fill", "yellow");

复用配置的小技巧:把配置好的过渡存成工厂函数,transition(getTransition()) 即继承其 时长/缓动/延迟,仍可局部覆盖13。

暂停与恢复:过渡没有 pause,但可以用 interrupt() + 记时间手动续: 'interrupt' 事件里记下已流逝时长,恢复时用 duration(总时长 - 已流逝) 新开过渡。 线性缓动下这个近似够用14。

文本过渡两条路:链式过渡做淡入淡出(先 opacity→0 再换文本 opacity→1); 真·交叉溶解要叠两个元素,一个淡出删除、一个淡入15。

4. 组合技:SVG / CSS / Canvas 一起上

过渡管不了的动画,原书给了一组「外挂」:

自画线(stroke-dasharray 技巧)。 把虚线 pattern 的「 dash 长度 = 线的总长」, 再把 dashoffset 从总长过渡到 0——线就从左到右「画」出来了。总长用 this.getTotalLength() 取16:

  .style("stroke-dasharray",  function() { return this.getTotalLength() })
  .style("stroke-dashoffset", function() { return this.getTotalLength() })
  .transition().duration(3000).style("stroke-dashoffset", 0)

SVG animateMotion 沿路径移动。 任何元素挂 <animateMotion><mpath href="#线"/></animateMotion> 就沿线走;配 D3 的做法是在过渡的 'start' 事件里生成它,时长设成一样、缓动用线性 (两边的缓动算法对不齐)。结束记得删掉动画元素并 svg.node().setCurrentTime(0) 归零时间轴17。

CSS 动画联动。 @keyframes 无限循环(旋转的星星)或 .running 类触发 CSS transition; D3 只负责加类。原书提醒:CSS 与 D3 的缓动曲线对不齐,精确同步要用线性18。

Canvas 逐帧作画。 animateMotion 移动一个看不见的圆,每帧用 getCTM() 取它的变换矩阵, 往 Canvas 上画一小段——曲线就是这么「刷」出来的。SVG 版会在 DOM 里堆积海量小矩形 (「insane amount of objects」),Canvas 版没这个问题,但拐弯多会糊19。

5. 在线教程:GDP 竞速柱状图

1970–2022 各国 GDP 的竞速柱状图(racing bar chart):柱子随时间增长/缩短、上下换位—— 本章全部技术(过渡、链式、定时器、自定义补间)的大合奏,成品在 Examples/Bar-chart-race-GDP20。

6. 作者的判断与证据

判断 性质 依据
「过渡为短更新设计,不是长跑动画」 设计定位 原书明说:结束后即失效,长动画该用定时器14
「restart() 只真适用于 d3.timer()」 版本缺陷披露 原书以 Important note 披露 interval 的 restart 延迟 bug3
「CSS/SVG 动画与 D3 难精确同步」 实践警告 缓动体系不同,例子都用线性近似18

7. 边界与局限

  • d3.active(node) 取元素上的活动过渡,本章只在方法表里列了,没有展开用例5。
  • 过渡的性能(大选择器 + 每帧插值的开销)没有定量讨论;Canvas 一节给了替代方向。
  • 暂停/恢复是手工作坊式实现(记时长 + 重开),没有官方 API。
  • pause/resume 方案明说不精确:「can miss a few frames」14。

判断(我们的,不是书里的): 本章最有普适性的一条是「命名空间式的互斥」设计: 同元素上的过渡按名字互斥,无名过渡共享一个默认命名—— 一个过渡管位置、一个管颜色,各动各的,互不踩脚。 这是处理「共享资源上的并发修改」的最小可用协议,思想可以直接搬到自己的状态管理里。 如果错,会错在: 协议只规定了互斥,没有规定优先级与抢占策略—— 链式过渡里「子过渡不随父过渡一起被取消」这类细则(第 766–770 段)说明实际语义比 「同名互斥」复杂,搬用时如果只记四个字会踩坑。

8. 可带走的

本章主线一行收束: selection.transition().delay(i*50).duration(1000).ease(d3.easeCubic) 后面跟 attr/style 自动插值;多个并行用 transition("名字"),串行用 .transition() 链接, 结束后要做事用 .on("end", …) 或 await t.end()。

  1. 定时器三件套:timeout(一次)、timer(每帧)、interval(定间隔),都自动跳过后台标签页;
  2. 过渡默认 0 延迟、250ms、cubic in-out;三个旋钮都能传回调做逐元素错峰;
  3. 过渡不是选择器:不能 append/join;.selection() 换回去;
  4. attr/style 自动选插值器;text 不插值(要 textTween);remove 在结束时删除;
  5. 自定义补间 = 外层取初值(一次)+ 内层每帧吐值;tween() 每帧随便干嘛;
  6. 生命周期四事件:start/end/interrupt/cancel;end() 返回 promise,可 await;
  7. 同一选择器多过渡默认互踩:要并行就命名,要串行就链式,要精确就延迟算总账;
  8. 类切换走 CSS transition,D3 只切类;
  9. 自画线 = dasharray/dashoffset 都设成 getTotalLength(),offset 过渡到 0;
  10. 沿路径移动 = SVG animateMotion + mpath,与线性缓动的过渡同步;
  11. 竞速柱状图是全章技术的大合奏,教程值得照做一遍。

9. 原文地图

主题 原书章 原文位置
定时器三函数 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:31(搜「d3.now()」) · text/20-ch13-chapter-13-transitions-and-animation.txt:76(搜「d3.timeout(function」) · text/20-ch13-chapter-13-transitions-and-animation.txt:101(搜「let timer = d3.timer」) · text/20-ch13-chapter-13-transitions-and-animation.txt:118(搜「d3.interval(callback, 100)」)
restart 注意事项 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:148(搜「documented restart() method」)
过渡创建与默认参数 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:172(搜「d3.transition()」) · text/20-ch13-chapter-13-transitions-and-animation.txt:248(搜「zero delay, a 250 ms duration」)
过渡不是选择器 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:222(搜「FAILS!」) · text/20-ch13-chapter-13-transitions-and-animation.txt:227(搜「WORKS!」)
delay/duration/ease 方法 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:254(搜「delay(value)」) · text/20-ch13-chapter-13-transitions-and-animation.txt:270(搜「t1.delay()」)
缓动家族 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:284(搜「Easing」) · text/20-ch13-chapter-13-transitions-and-animation.txt:296(搜「28 interpolators」) · text/20-ch13-chapter-13-transitions-and-animation.txt:314(搜「easeElastic」)
attr/style/text/remove Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:333(搜「attr(name,value)」) · text/20-ch13-chapter-13-transitions-and-animation.txt:339(搜「text(value)」) · text/20-ch13-chapter-13-transitions-and-animation.txt:398(搜「style("opacity", 0).remove()」)
CSS 类切换 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:403(搜「CSS transitions」) · text/20-ch13-chapter-13-transitions-and-animation.txt:424(搜「classed() is not supported」)
自定义补间 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:444(搜「attrTween(name,function)」) · text/20-ch13-chapter-13-transitions-and-animation.txt:465(搜「attrTween("r"」) · text/20-ch13-chapter-13-transitions-and-animation.txt:492(搜「Step ${1 + t*9}」) · text/20-ch13-chapter-13-transitions-and-animation.txt:504(搜「tween("_"」)
生命周期与事件 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:551(搜「life cycle」) · text/20-ch13-chapter-13-transitions-and-animation.txt:572(搜「'start', 'end', 'interrupt', or 'cancel'」) · text/20-ch13-chapter-13-transitions-and-animation.txt:614(搜「end() method」)
并发与命名 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:651(搜「Concurrent transitions」) · text/20-ch13-chapter-13-transitions-and-animation.txt:690(搜「interrupts any ongoing transition」) · text/20-ch13-chapter-13-transitions-and-animation.txt:708(搜「unique name」)
链式过渡 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:730(搜「Chained transitions」) · text/20-ch13-chapter-13-transitions-and-animation.txt:749(搜「grows for two seconds」)
文本交叉溶解 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:772(搜「Text transitions」) · text/20-ch13-chapter-13-transitions-and-animation.txt:797(搜「crossfade」)
配置复用与暂停恢复 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:823(搜「getTransition()」) · text/20-ch13-chapter-13-transitions-and-animation.txt:838(搜「Pausing and resuming」) · text/20-ch13-chapter-13-transitions-and-animation.txt:883(搜「duration(duration - saved.t)」)
自画线 dasharray Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:919(搜「stroke-dasharray」) · text/20-ch13-chapter-13-transitions-and-animation.txt:947(搜「getTotalLength()」)
animateMotion 与 textPath Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:959(搜「animateMotion」) · text/20-ch13-chapter-13-transitions-and-animation.txt:1020(搜「'start', function(d,i)」) · text/20-ch13-chapter-13-transitions-and-animation.txt:1040(搜「setCurrentTime(0)」) · text/20-ch13-chapter-13-transitions-and-animation.txt:1055(搜「textPath」)
CSS 动画联动 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:1075(搜「CSS animations」) · text/20-ch13-chapter-13-transitions-and-animation.txt:1139(搜「differences in easing functions」)
Canvas 逐帧作画 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:1142(搜「Animating a line on a Canvas」) · text/20-ch13-chapter-13-transitions-and-animation.txt:1190(搜「getCTM()」) · text/20-ch13-chapter-13-transitions-and-animation.txt:1218(搜「insane amount of objects」)
竞速柱状图教程 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:1271(搜「Racing bar chart」)
章小结 Transitions and Animation text/20-ch13-chapter-13-transitions-and-animation.txt:1281(搜「Summary」)

10. 竞速柱状图教程的内部结构(加厚:全书技术的合龙点)

章末的 GDP 竞速柱状图(1970–2022,Examples/Bar-chart-race-GDP)值得拆开看, 因为它把前面十二章的工具全部用上,且每一环都是本章的概念:

  数据   每年一行:rows(year, country, gdp),宽表转 tidy 后按年 group
           │
  时钟   d3.interval(每 100ms 推一年)→ 不是 d3.timer(每帧),因为数据是离散的年度
           │
  重排   每年取前 N 名 → sortData() → 名次即 y 坐标(band 尺的 domain 重设)
           │
  更新   重绑 + 两级过渡:名次变化走 transition().duration(900),
         柱子宽度走 attr("width", …)——同一元素上两个命名过渡并行(本章 §3.6)
           │
  入场/离场 join 三分:新入榜国家从底部升起,出榜国家 fade + remove()

  图说:interval 当节拍器、join 管增减、命名过渡管并行——本章三件套一个不缺。

两个容易翻车的细节。其一,年份标签是文本——不能直接插值,教程用 textTween 做数字滚动; 其二,排名跳变时柱子是「先移动再变长」还是同时,取决于两个过渡的 duration 差—— 让移动稍慢(900ms > interval 的 750ms 有效节拍),视觉才有「追上」的动感。 这个「过渡时长略长于数据节拍」的配比,是一切竞速类动画的通用调法。

练习 13.6/13.7 是排错练习,正好反向验证本章重点:形状要先全部到位再动第二组(等待 end)、 transform 与属性过渡要并行(命名)——两题分别对应「链式没等完」和「同名互踩」两个经典 bug。


  1. 出处:「Transitions and Animation」第 26–47 段(text/20-ch13-chapter-13-transitions-and-animation.txt:31,搜「d3.now()」;:35,搜「~60 FPS」)与第 72–145 段(:76,搜「d3.timeout(function」;:101,搜「let timer = d3.timer」;:117,搜「d3.interval(callback, 100)」)。 ↩

  2. 出处:「Transitions and Animation」第 88、108、145 段(text/20-ch13-chapter-13-transitions-and-animation.txt:88,搜「native setTimeout()」;:112,搜「guaranteed not to run in the background」)。 ↩

  3. 出处:「Transitions and Animation」第 147–150 段(text/20-ch13-chapter-13-transitions-and-animation.txt:148,搜「documented restart() method」)。 ↩↩

  4. 出处:「Transitions and Animation」第 165–198 段(text/20-ch13-chapter-13-transitions-and-animation.txt:172,搜「d3.transition()」;:248,搜「zero delay, a 250 ms duration」)。 ↩

  5. 出处:「Transitions and Animation」第 219–243 段(text/20-ch13-chapter-13-transitions-and-animation.txt:222,搜「FAILS!」;:227,搜「WORKS!」)。方法表照原书 Table 13.3 转写。 ↩↩

  6. 出处:「Transitions and Animation」第 284–320 段(text/20-ch13-chapter-13-transitions-and-animation.txt:296,搜「28 interpolators」;:300,搜「alias to its most popular behavior」;:314,搜「easeElastic.period(.1)」)。 ↩

  7. 出处:「Transitions and Animation」第 322–401 段(text/20-ch13-chapter-13-transitions-and-animation.txt:333,搜「d3.interpolateNumber()」;:339,搜「text(value)」;:342,搜「removes each element」)。 ↩

  8. 出处:「Transitions and Animation」第 403–436 段(text/20-ch13-chapter-13-transitions-and-animation.txt:424,搜「classed() is not supported」;:429,搜「transition: all 2s」)。 ↩

  9. 出处:「Transitions and Animation」第 438–545 段(text/20-ch13-chapter-13-transitions-and-animation.txt:465,搜「attrTween("r"」;:483,搜「textTween(function」;:492,搜「Step ${1 + t*9}」;:504,搜「tween("_"」)。「外层一次、内层每帧」的结构说明见第 455–460 段。 ↩

  10. 出处:「Transitions and Animation」第 549–630 段(text/20-ch13-chapter-13-transitions-and-animation.txt:552,搜「consists of several steps」;:572,搜「'start', 'end', 'interrupt', or 'cancel'」;:596,搜「If a scheduled transition has not started」)。 ↩

  11. 出处:「Transitions and Animation」第 597–600 段(text/20-ch13-chapter-13-transitions-and-animation.txt:598,搜「transition-related events」)。 ↩

  12. 出处:「Transitions and Animation」第 642–770 段(text/20-ch13-chapter-13-transitions-and-animation.txt:670,搜「Only the last transition happens」;:690,搜「interrupts any ongoing transition」;:708,搜「unique name」;:752,搜「grows for two seconds」)。 ↩

  13. 出处:「Transitions and Animation」第 818–836 段(text/20-ch13-chapter-13-transitions-and-animation.txt:823,搜「getTransition()」;:832,搜「transition(getTransition())」)。 ↩

  14. 出处:「Transitions and Animation」第 838–889 段(text/20-ch13-chapter-13-transitions-and-animation.txt:839,搜「designed for short updates」;:883,搜「duration(duration - saved.t)」;:888,搜「miss a few frames」)。 ↩↩↩

  15. 出处:「Transitions and Animation」第 772–816 段(text/20-ch13-chapter-13-transitions-and-animation.txt:780,搜「fade-in followed by a fade-out」;:797,搜「stacked elements」)。 ↩

  16. 出处:「Transitions and Animation」第 908–953 段(text/20-ch13-chapter-13-transitions-and-animation.txt:919,搜「stroke-dasharray」;:939,搜「stroke-dashoffset, 0」;:947,搜「getTotalLength()」)。 ↩

  17. 出处:「Transitions and Animation」第 955–1073 段(text/20-ch13-chapter-13-transitions-and-animation.txt:957,搜「<animateMotion> element」;:1020,搜「'start', function(d,i)」;:1040,搜「setCurrentTime(0)」;:1055,搜「textPath」)。 ↩

  18. 出处:「Transitions and Animation」第 1075–1140 段(text/20-ch13-chapter-13-transitions-and-animation.txt:1085,搜「keyframes spin」;:1139,搜「differences in easing functions」)。 ↩↩

  19. 出处:「Transitions and Animation」第 1142–1254 段(text/20-ch13-chapter-13-transitions-and-animation.txt:1190,搜「getCTM()」;:1218,搜「insane amount of objects」;:1237,搜「context.canvas」)。 ↩

  20. 出处:「Transitions and Animation」第 1271–1279 段(text/20-ch13-chapter-13-transitions-and-animation.txt:1271,搜「Racing bar chart」)。 ↩