过渡与动画 — 从定时器到多过渡编排¶
这一章讲什么: 数据更新配上平滑动画,是 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()。
- 定时器三件套:timeout(一次)、timer(每帧)、interval(定间隔),都自动跳过后台标签页;
- 过渡默认 0 延迟、250ms、cubic in-out;三个旋钮都能传回调做逐元素错峰;
- 过渡不是选择器:不能 append/join;
.selection()换回去; - attr/style 自动选插值器;text 不插值(要 textTween);remove 在结束时删除;
- 自定义补间 = 外层取初值(一次)+ 内层每帧吐值;tween() 每帧随便干嘛;
- 生命周期四事件:start/end/interrupt/cancel;
end()返回 promise,可 await; - 同一选择器多过渡默认互踩:要并行就命名,要串行就链式,要精确就延迟算总账;
- 类切换走 CSS transition,D3 只切类;
- 自画线 = dasharray/dashoffset 都设成 getTotalLength(),offset 过渡到 0;
- 沿路径移动 = SVG animateMotion + mpath,与线性缓动的过渡同步;
- 竞速柱状图是全章技术的大合奏,教程值得照做一遍。
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。
-
出处:「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)」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Transitions and Animation」第 147–150 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:148,搜「documented restart() method」)。 ↩↩ -
出处:「Transitions and Animation」第 165–198 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:172,搜「d3.transition()」;:248,搜「zero delay, a 250 ms duration」)。 ↩ -
出处:「Transitions and Animation」第 219–243 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:222,搜「FAILS!」;:227,搜「WORKS!」)。方法表照原书 Table 13.3 转写。 ↩↩ -
出处:「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)」)。 ↩ -
出处:「Transitions and Animation」第 322–401 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:333,搜「d3.interpolateNumber()」;:339,搜「text(value)」;:342,搜「removes each element」)。 ↩ -
出处:「Transitions and Animation」第 403–436 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:424,搜「classed() is not supported」;:429,搜「transition: all 2s」)。 ↩ -
出处:「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 段。 ↩ -
出处:「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」)。 ↩ -
出处:「Transitions and Animation」第 597–600 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:598,搜「transition-related events」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Transitions and Animation」第 818–836 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:823,搜「getTransition()」;:832,搜「transition(getTransition())」)。 ↩ -
出处:「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」)。 ↩↩↩ -
出处:「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」)。 ↩ -
出处:「Transitions and Animation」第 908–953 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:919,搜「stroke-dasharray」;:939,搜「stroke-dashoffset, 0」;:947,搜「getTotalLength()」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Transitions and Animation」第 1075–1140 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:1085,搜「keyframes spin」;:1139,搜「differences in easing functions」)。 ↩↩ -
出处:「Transitions and Animation」第 1142–1254 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:1190,搜「getCTM()」;:1218,搜「insane amount of objects」;:1237,搜「context.canvas」)。 ↩ -
出处:「Transitions and Animation」第 1271–1279 段(
text/20-ch13-chapter-13-transitions-and-animation.txt:1271,搜「Racing bar chart」)。 ↩