d3 过渡选择与链式动画指南:深入 d3-transition 的选择、同步与接力 API
本篇技术指南围绕 d3 v7(当前仓库版本 7.9.0)官方文档 docs/d3-transition/selecting.md 展开,系统讲解如何在 d3-selection 之上创建、子选择、合并与接力过渡(transition)。读完后你可以掌握:如何用 *selection*.transition(name) 在不同元素集合间同步同一个过渡,如何用 transition.select/selectAll/filter/merge 在过渡上做子选择,以及如何用 transition.transition() 与 d3.active() 编排链式、循环动画。
1. 创建过渡:selection.transition(name)
过渡是"selection-like"(类似选择集)的接口:不立即修改 DOM,而是在给定时长内将 DOM 从当前状态平滑插值到目标状态(参见 d3-transition 总览)。所有过渡都派生自选择集,入口就是 *selection*.transition(name):
- 参数
name为过渡名,不传时默认使用null; - 返回一个新过渡,作用于当前选择集上的每个元素;
- 排他性规则:新过渡只与"同名"的其他过渡互斥。即同一元素上可以同时存在
null名过渡和"update"名过渡而互不干扰。
原文文档给出的典型场景是跨选择集同步同一个过渡:当 name 传入的是一个过渡实例(而非字符串)时,返回的过渡与该实例具有相同的 id 和 name;若某个被选元素上已存在同 id 的过渡,则对该元素直接返回已存在的过渡;否则,返回过渡的定时(timing)会从"每个被选元素最近的祖先上同 id 的既有过渡"继承而来。这样你就可以先定义一个过渡模板,再把不同的选择集"挂"到同一时间轴上:
const t = d3.transition()
.duration(750)
.ease(d3.easeLinear);
d3.selectAll(".apple").transition(t)
.style("fill", "red");
d3.selectAll(".orange").transition(t)
.style("fill", "orange");
.apple 与 .orange 两组元素由此共用同一过渡 id,起始时间保持一致,实现同步动画。
边界情况(原文档明确警告):如果指定的过渡实例既不在被选节点上、也不在其任何祖先上(例如该过渡已经结束),当前会回退到默认定时参数;但原文档指出,未来版本可能会改为抛出错误(对应 d3-transition 上游 issue #59 的行为讨论)。因此在生产代码中不要依赖"找不到模板过渡时静默回退"这一行为。
默认定时参数
结合 Timing 文档 可以确认,创建过渡后可用以下方法覆盖默认值:
| 方法 | 默认值 | 说明 |
|---|---|---|
*transition*.delay(value) |
0 ms | 延迟开始的时间(毫秒),支持逐元素函数 |
*transition*.duration(value) |
250 ms | 持续时间(毫秒),支持逐元素函数 |
*transition*.ease(value) |
d3.easeCubic |
缓动函数,每帧传入归一化时间 t∈[0,1] |
这三个方法都接受"常量或函数":函数形式会被立即对每个被选元素求值,依次传入当前数据 d、当前索引 i、当前组 nodes,this 为当前 DOM 元素。
2. 过渡级选择函数:d3.transition(name)
d3.transition(name) 直接在文档根元素 document.documentElement 上创建一个新过渡,name 的语义与 *selection*.transition 完全一致(不传为 null,只与同名过渡互斥;也可传入过渡实例,见上一节)。它等价于:
d3.selection()
.transition(name)
此外,d3.transition 还承担两个工程用途:
- 类型判断:可用
t instanceof d3.transition测试一个值是否为过渡实例,这在前文selection.transition(t)接收过渡实例时非常有用; - 扩展原型:由于过渡对象基于原型链构造,你可以通过修改
d3.transition.prototype为所有过渡添加自定义方法。
3. 过渡的子选择:select / selectAll / selectChild / selectChildren / filter
这组方法是 d3-transition 独有的"过渡版子选择",它们在过渡上而不是在选择集上工作,因此返回的是过渡而非选择集,可以直接继续链式设置 style、attr 等。
transition.select(selector)
对当前过渡的每个被选元素,选出第一个匹配 selector 的后代元素(如有),并在新选择上返回过渡。等价于"先取选择、再做子选择、最后按同一过渡 id 派生新过渡"这三步的组合:
transition
.selection()
.select(selector)
.transition(transition)
transition.selectAll(selector)
对每个被选元素,选出所有匹配的后代元素。同样等价于:
transition
.selection()
.selectAll(selector)
.transition(transition)
transition.selectChild(selector) / transition.selectChildren(selector)
这两个是 d3 v7 新增的"仅子元素"变体:selectChild 对每个被选元素选出第一个匹配的直接子元素,selectChildren 选出所有匹配的直接子元素。与 select/selectAll 的区别在于不再向下穿透更深层后代。分别等价于:
transition
.selection()
.selectChild(selector)
.transition(transition)
transition
.selection()
.selectChildren(selector)
.transition(transition)
对应底层语义可参考 d3-selection 的子选择文档:例如 selectChild 会把父元素的数据传播(propagate)到选中的子元素上,这对"外层 g 绑定数据、内层子元素做动画"的 SVG 结构尤其重要。
参数与排他性(四个子选择方法通用):
selector可以是选择器字符串,也可以是函数;函数按序对每个被选元素求值,传入d、i、nodes,this为当前 DOM 元素;- 新过渡与本过渡具有相同的 id、name 和 timing,因此父过渡跑到一半时,对其子元素调用
transition.select(".tick").style(...),子过渡会在同一 id 下被继承并同步; - 若目标元素上已存在同 id 的过渡,则对该元素返回既有过渡——这一点保证了"重复子选择不会重启动画",是构建增量动画的关键幂等性。
transition.filter(filter)
只保留匹配 filter 的元素,返回子集上的过渡。filter 为选择器字符串或函数(函数参数同样是 d、i、nodes,this 为当前 DOM 元素)。等价于:
transition
.selection()
.filter(filter)
.transition(transition)
实际用途典型如:数据更新时先对整组元素启动一个过渡,再 filter(d => d.value > 0) 只对满足条件的子集追加不同的 attr 目标,其余元素保持原过渡不变。
4. 过渡合并:transition.merge(other)
返回本过渡与 other 合并后的新过渡,要求 other 与本过渡具有相同的 id。结果过渡保持与本体相同的组数、parent、name 和 id;本过渡中缺失(null)的位置,会用 other 中对应位置的非 null 元素补上。
等价于在 selection 层面合并后再派生过渡:
transition
.selection()
.merge(other.selection())
.transition(transition)
这与 selection.merge 的语义一致:两个"组结构相同、部分元素互补"的过渡(例如 enter 组与 update 组分开创建的过渡)可以先合并,再统一调用 transition.attr("cx", ...),避免对两个过渡重复写同一目标值。
5. 链式过渡:transition.transition()
在本过渡结束时,对相同的被选元素调度一个新的过渡。新过渡继承:
官方文档示例展示了"绿色 → 红色 → 停顿 1 秒 → 棕色并移除"的多段接力:
d3.selectAll(".apple")
.transition() // First fade to green.
.style("fill", "green")
.transition() // Then red.
.style("fill", "red")
.transition() // Wait one second. Then brown, and remove.
.delay(1000)
.style("fill", "brown")
.remove();
关键语义:每一段的 delay 都是相对于上一段过渡结束时刻的偏移。所以上例中苹果保持红色 1 秒后,才会开始向棕色过渡——这是"相对时间轴"而非绝对时间戳,编写多阶段动画时务必按此理解。
注意与第 3 节的配合:链上每一段的 transition.transition() 都继承同一 id,而 *transition*.remove()(见 modifying 文档)会在过渡结束时移除元素,因此链式过渡是"先动画、后清理"的标准写法。
6. 取出选择集与查询活跃过渡
transition.selection()
返回该过渡对应的 selection。这是"过渡世界"回到"选择世界"的桥:当你需要在过渡之外操作元素(例如 append 新元素——过渡不支持创建元素,必须先 append 或绑定数据再启动过渡,见 d3-transition 总览),用 transition.selection().append("circle") 即可。
active(node, name)
返回指定 node 上名为 name 的活跃过渡(如存在);不传 name 时按 null 查;没有则返回 null。文档标注其典型用途是"创建链式过渡"。官方"迪斯科模式"示例展示了如何借事件回调自举出无限循环动画:
d3.selectAll("circle").transition()
.delay((d, i) => i * 50)
.on("start", function repeat() {
d3.active(this)
.style("fill", "red")
.transition()
.style("fill", "green")
.transition()
.style("fill", "blue")
.transition()
.on("start", repeat);
});
拆解其机制:d3.active(this) 取回当前正在运行的过渡,在其上再 .transition() 依次挂接红、绿、蓝三段接力过渡,最后一段的 on("start", repeat) 又注册同一个回调,从而每轮开始都递归重建下一轮——由于 transition.transition() 的相对 delay 语义,轮次之间无缝衔接。这里的 .on("start", ...) 是过渡事件(start / end / interrupt / cancel),与原生 DOM 事件不同,详见 控制流文档。
7. 源码与仓库结构印证
从当前仓库的包结构看,d3 元包并不内联实现过渡逻辑,而是聚合各独立子包:
- src/index.js 第 29 行
export * from "d3-transition";将 d3-transition 的全部 API 并入d3命名空间,因此d3.transition、d3.active与各*selection*.transition行为都由 d3-transition 子包提供; - package.json 声明依赖
"d3-transition": "^3.0.1"(同时含d3-selection": "^3.0.0"、d3-timer": "^3.0.1"),与文档所述 v3 系列 API(如selectChild/selectChildren为 v7 新增)一致; - 原文档为每个 API 标注了 d3-transition 子包内的源码文件,如
selection.transition对应子包src/selection/transition.js、*transition*.select对应src/transition/select.js、active对应src/active.js等(当前仓库未随附 node_modules,无法在本仓库内直接查看这些实现文件,此处仅转述文档标注的位置)。
由文档描述与 control-flow 中"过渡的一生" 可以推断出若干工程约束:
- 中断发生在 start 而非创建:新过渡启动时才会中断同元素同名活跃过渡并派发
interrupt事件;零延迟过渡也不会立即中断旧过渡(旧过渡还有一帧),需要立即中断时应调用*selection*.interrupt(name); - 可修改窗口有限:过渡一旦调度(当前帧末或下一帧)便不能再改
delay与start监听器(报错 "too late: already scheduled"),运行后连 timing、tween、监听器都不可改(报错 "too late: already running"),结束后配置即被销毁("transition not found")。因此第 5 节链式过渡中"延迟到运行时才能确定的起始值"类逻辑必须写进on("start")或attrTween/styleTween; - 同 id 幂等:第 1、3、4 节反复出现的"若元素上已有同 id 过渡则返回既有过渡",与上述"中断/取消同名过渡"的机制共同构成了 d3 过渡的冲突消解模型——这解释了为什么模板过渡同步(第 1 节)与幂等子选择(第 3 节)能够可靠工作。
8. API 速查
| API | 作用 | 关键约束 |
|---|---|---|
selection.transition(name) |
在选择集上创建过渡;name 可为字符串或过渡实例 |
默认 name 为 null;仅与同名过渡互斥;找不到模板过渡时回退默认定时 |
d3.transition(name) |
在 document.documentElement 上创建过渡;instanceof 判断与原型扩展 |
等价于 d3.selection().transition(name) |
transition.select(selector) |
子选择:第一个匹配的后代 | 继承 id/name/timing;同 id 已存在则复用 |
transition.selectAll(selector) |
子选择:所有匹配的后代 | 同上 |
transition.selectChild(selector) |
子选择:第一个匹配的直接子元素 | v7 新增 |
transition.selectChildren(selector) |
子选择:所有匹配的直接子元素 | v7 新增 |
transition.filter(filter) |
过滤:仅保留匹配元素 | 选择器字符串或 (d, i, nodes) => bool |
transition.merge(other) |
与同 id 过渡合并,补全 null 位置 | other 必须同 id |
transition.transition() |
在本过渡结束时接力新过渡 | 继承 name/duration/ease;delay 相对上段结束时刻 |
transition.selection() |
返回过渡对应的 selection | 过渡 → 选择世界的桥 |
active(node, name) |
查询节点上活跃过渡 | 用于 on("start") 中重建循环动画 |
掌握以上 API 后,你就能覆盖 d3 动画实践中最常用的三种结构:多选择集同步动画(模板过渡 + 继承 timing)、增量局部动画(幂等子选择 + filter)、多阶段编排(transition.transition() 相对接力 + d3.active() 循环自举),并能在 docs/d3-transition/timing.md 与 docs/d3-transition/control-flow.md 的基础上进一步定制时序与控制流。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00