d3-transition 完全实战指南:d3 v7 中的过渡、插值器与动画控制流
d3-transition 是 d3 生态中负责“让数据动起来”的核心模块:它提供了一组与 selection 风格一致但面向动画的接口,让 DOM 属性、样式、文本从当前状态平滑插值到目标状态,而不是瞬间跳变。本文以本仓库文档 docs/d3-transition.md 及其四篇子文档(Selecting、Modifying、Timing、Control flow)为骨架,完整覆盖过渡的创建、时序配置、内容修改方法、插值器选择机制与生命周期控制流,并结合本仓库的聚合源码与测试给出可验证的工程事实,帮助你掌握 d3 v7 中编写、同步与调试动画的完整技术栈。
1. 什么是过渡(Transition)
一句话定义(来自 docs/d3-transition.md):过渡是一个 selection 风格的接口,用于对 DOM 变更进行动画。它不会瞬时应用变更,而是在给定持续时间内,把 DOM 从当前状态平滑插值(interpolate)到目标状态。
最简用法三步走:选择元素 → 调用 *selection*.transition → 声明目标状态:
d3.select("body")
.transition()
.style("background-color", "red");
过渡支持大多数 selection 方法(如 *transition*.attr、*transition*.style 分别替代 *selection*.attr、*selection*.style),但并非全部方法都支持。有一条关键顺序约束:必须在过渡开始之前完成元素 append 与 数据绑定;过渡启动后再创建新元素就无法纳入本次插值。与之配套,文档提供了 *transition*.remove 操作符,用于在过渡结束时便捷地移除元素(见 docs/d3-transition/modifying.md 中 *transition*.remove 一节)。
2. 计算中间状态:内建插值器的自动选择
过渡如何算出每一帧的中间状态?答案是 d3-interpolate 提供的内建插值器。根据 docs/d3-transition.md 的说明,以下类型会被自动检测:
- 颜色:走 interpolateRgb;
- 数字:走 interpolateNumber;
- 几何变换(translate / rotate / scale 等 transform):由 transform 插值器 处理;
- 内嵌数字的字符串:走 interpolateString。这一条对实际开发非常关键——许多 CSS 样式(如
padding、字体大小)和 SVG 路径d属性都是“文本里夹数字”的形态,d3-transition 能自动对其中的数值做插值。
当自动检测不够用(例如需要彩虹色渐变、路径形变、数据驱动插值)时,使用三类“自定义插值器入口”:
*transition*.attrTween(自定义属性插值);*transition*.styleTween(自定义样式插值);*transition*.tween(任意命名 tween,可执行副作用)。
三者的详细签名与示例见下文第 5 节。
3. 创建与选择过渡元素(Selecting)
3.1 *selection*.transition(*name*)
过渡从 selection 派生,入口是 *selection*.transition(*name*)(docs/d3-transition/selecting.md)。name 可选,缺省为 null;新过渡只与同名的其他过渡互斥——这是 d3-transition 并发模型的基础:同名过渡在同一元素上会相互打断/排队,不同名过渡可以并行。
name 还可以直接传入一个过渡实例,此时返回的过渡会沿用该实例的 id 与 name。若选中元素上已存在同 id 过渡,则对该元素直接返回既有过渡;否则,返回过渡的时序配置会从每个选中元素的最近祖先上同 id 的既有过渡继承。这个机制用于两类场景:
- 跨多个 selection 同步同一个过渡;
- 针对特定元素重新选中既有过渡并修改其配置。
文档给出的同步示例(两个 selection 共用同一节奏):
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");
注意:若指定过渡在节点及其祖先上都找不到(例如该过渡已经结束),当前版本使用默认时序参数;文档明确提示未来版本可能会改为抛出错误。
3.2 d3.transition(*name*)
在文档根元素 document.documentElement 上创建新过渡,等价于:
d3.selection()
.transition(name)
同时,d3.transition 这个构造函数可用于类型判断(instanceof d3.transition)与扩展 transition 原型。
3.3 子选择:select / selectAll / selectChild / selectChildren / filter / merge
*transition*.select、*transition*.selectAll、*transition*.selectChild、*transition*.selectChildren、*transition*.filter 的行为与对应 selection 方法一一对应,selector 均支持字符串或函数两种形式;函数按顺序对每个选中元素求值,收到当前数据 *d*、索引 *i*、当前组 *nodes*,this 为当前 DOM 元素。新过渡继承本过渡的 id、name 与时序。它们各自等价于“先取 *transition*.selection(),做子选择,再重新建过渡”三步,以 *transition*.select 为例:
transition
.selection()
.select(selector)
.transition(transition)
*transition*.merge(*other*)(docs/d3-transition/selecting.md 中 *transition*.merge 一节)返回两个过渡(必须同 id)合并后的新过渡:组数、父节点、name、id 与 this 相同,this 中缺失(null)的元素用 other 中对应(非 null)元素补位,等价于:
transition
.selection()
.merge(other.selection())
.transition(transition)
*transition*.selection() 则直接返回与该过渡对应的 selection。
3.4 *transition*.transition():链式排队的核心
这是串行动画的关键方法:返回一个作用在相同选中元素上的新过渡,其启动时间排定为本过渡结束时;新过渡的参考时间 = 本过渡时间 + delay + duration,并继承本过渡的 name、duration 与 easing。文档的“苹果三段变色”示例完整展示了链式排队(注意:每一段的 delay 相对于其前一段):
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();
效果:苹果先渐变为绿,再变红,保持红色一秒后进入最后一段,变棕并移除。
3.5 active(node, name):获取元素上的活跃过渡
active(node, name) 返回指定节点上指定名称的活跃过渡(若无 name 则按 null 处理),没有则返回 null。它是构建循环/自重复动画的工具——文档给出的 “disco mode” 完整示例:
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);
});
思路:start 事件回调里通过 d3.active(this) 拿到当前正在运行的过渡,在其上继续链式 .transition(),最后一段的 start 回调再次注册自身,形成无限循环,同时用 delay((d, i) => i * 50) 让各圆错峰启动。
4. 时序配置(Timing)
过渡的 easing、delay、duration 三者全部可配置。官方文档特别指出:利用逐元素 delay 错峰(stagger)元素的重排,可显著改善人眼对动画的感知质量。
4.1 *transition*.delay(*value*)
单位为毫秒,支持常量或函数两种形式;函数立即对每个选中元素求值,参数为 *d*、*i*、*nodes*,this 为当前 DOM 元素。未指定时默认为 0。不传参调用返回第一个(非 null)元素当前的 delay 值(仅在过渡恰好含一个元素时才有实用意义):
transition.delay(250);
transition.delay() // 250
最典型的错峰写法是把 delay 设为索引的倍数:
transition.delay((d, i) => i * 10);
也可以把 delay 计算为数据的函数,或在计算索引 delay 前先对 selection 排序(selection.sort)。
4.2 *transition*.duration(*value*)
单位毫秒,同样支持常量或函数。未指定时默认 250ms。不传参调用返回第一个(非 null)元素当前的 duration:
transition.duration(750);
transition.duration() // 750
4.3 *transition*.ease(*value*)
指定 easing 函数。value 必须是函数:动画每一帧都会调用它,传入归一化时间 *t*(范围 [0, 1]),返回缓动后的时间 *tʹ*(通常也在 [0, 1])。良好的缓动函数应满足 t=0 时返回 0、t=1 时返回 1。未指定时默认为 easeCubic。不传参调用返回第一个(非 null)元素当前的缓动函数:
transition.ease(d3.easeCubic);
transition.ease() // d3.easeCubic
4.4 *transition*.easeVarying(*factory*)
与 ease 的区别在于:它接受一个工厂函数,对每个选中节点求值(参数同样是 *d*、*i*、*nodes*,this 为当前 DOM 元素),要求其返回一个缓动函数——即不同元素可以使用不同的缓动曲线:
transition.easeVarying((d) => d3.easePolyIn.exponent(d.exponent));
5. 修改元素(Modifying)
创建过渡之后(见第 3 节),使用过渡的转换方法影响文档内容。
5.1 *transition*.attr(*name*, *value*)
为指定属性分配一个属性 tween:tween 的起始值是过渡开始时刻该属性的值(这保证了从任意当前状态出发的插值)。目标 value 支持常量或函数(函数参数与前述一致)。目标值为 null 时,属性在过渡开始时被移除;否则按以下三步算法自动选择插值器(docs/d3-transition/modifying.md 原文算法):
- 若 value 是数字,使用 interpolateNumber;
- 若 value 是颜色或可强制转换为颜色的字符串,使用 interpolateRgb;
- 否则使用 interpolateString。
需要其他插值器时用 *transition*.attrTween。
5.2 *transition*.attrTween(*name*, *factory*)
factory 是“插值器工厂”:返回一个 interpolator。过渡开始时,factory 按序对每个选中元素求值(参数 *d*、*i*、*nodes*,this 为当前 DOM 元素);返回的插值器在过渡的每一帧被调用,传入缓动后的时间 *t*(通常 [0, 1]),其返回值被设为属性值——插值器必须返回字符串。传 null 移除已赋值的属性 tween;不传参返回当前的插值器工厂(不存在则为 undefined)。文档给出的三个层次递进的示例:
// 固定从 red 插值到 blue
transition.attrTween("fill", () => d3.interpolateRgb("red", "blue"));
// 从当前 fill 插值到 blue(等价于 *transition*.attr 的默认行为)
transition.attrTween("fill", function() {
return d3.interpolateRgb(this.getAttribute("fill"), "blue");
});
// 自定义“彩虹”插值器:把归一化时间映射到色相环
transition.attrTween("fill", () => (t) => `hsl(${t * 360},100%,50%)`);
该方法的另一个高价值用途是数据插值(data interpolation):用 interpolateObject 对两个数据对象插值,再把中间数据代入 shape 等生成器计算新的属性值——这是实现平滑形变(morphing)的常用模式。关于属性移除的时机约定:过渡开始时移除用 *transition*.attr(传 null),结束时移除则用 *transition*.on(docs/d3-transition/control-flow.md 中 *transition*.on)监听 end 事件。
5.3 *transition*.style 与 *transition*.styleTween
*transition*.style(*name*, *value*, *priority*) 与 attr 的规则几乎一致,两点差异:tween 的起始值优先取该样式的内联值,无内联值时取计算值(computed value);且可指定 CSS *priority*。目标值为 null 时在过渡开始移除该样式;插值器选择算法同样是三步(数字 → interpolateNumber;颜色/颜色字符串 → interpolateRgb;其余 → interpolateString),换用其他插值器用 *styleTween*。
*transition*.styleTween(*name*, *factory*, *priority*) 的工厂契约与 attrTween 相同,返回值以指定 priority 写入样式值。示例与 attrTween 对称:
transition.styleTween("fill", () => d3.interpolateRgb("red", "blue"));
transition.styleTween("fill", function() {
return d3.interpolateRgb(this.style.fill, "blue");
});
transition.styleTween("fill", () => (t) => `hsl(${t * 360},100%,50%)`);
5.4 *transition*.text 与 *transition*.textTween
*transition*.text(*value*):在过渡开始时把文本内容整体设为目标值(支持常量或函数;null 清空内容)。文本默认不做逐帧插值,因为逐帧改文本通常并不理想;若确实需要“数字滚动”效果,用 *transition*.textTween,或者追加一个替换元素做透明度交叉淡入淡出(cross-fade)。
*transition*.textTween(*factory*):工厂返回的插值器每帧被调用并传入缓动时间 *t*,返回值用于设置文本(必须返回字符串)。传 null 移除既有 text tween;不传参返回当前工厂。文档示例(整数从 0 滚到 100,注意 interpolateRound 保证每帧都是整数):
transition.textTween(() => d3.interpolateRound(0, 100));
5.5 *transition*.remove()
为每个选中元素登记:过渡结束时移除该元素——但前提是此刻该元素没有其他活跃或待启动的过渡;若存在其他过渡,则不做任何事。这条保护规则避免了“元素正被另一个过渡使用就被删掉”的竞态。
5.6 `transition.tween(name, value)
最底层的通用 tween 注册:value 必须是“返回函数的函数”。过渡开始时对每个选中元素求值(参数 *d*、*i*、*nodes*,this 为当前 DOM 元素),得到的内层函数在每一帧被调用并传入缓动时间 *t*。传 null 移除同名 tween。相比 attrTween/styleTween,tween 可以直接写任意副作用(不限于设置某个属性/样式)。文档的示例——用 tween 复现 attr 把 fill 插值到 blue 的效果:
transition.tween("attr.fill", function() {
const i = d3.interpolateRgb(this.getAttribute("fill"), "blue");
return function(t) {
this.setAttribute("fill", i(t));
};
});
6. 控制流与过渡的生命周期(Control flow)
docs/d3-transition/control-flow.md 是理解 d3-transition 并发与错误信息的权威章节。
6.1 过渡的一生(The life of a transition)
文档按时间线精确描述了五个阶段,逐段理解有助于定位“为什么我改不动过渡”这类问题:
- 创建后(配置期):通过
*selection*.transition或*transition*.transition创建过渡后,可立即用delay、duration、attr、style等方法配置。注意两类方法的处理时点差异:指定目标值的方法(如*transition*.attr)是同步求值的;而需要起始值参与插值的方法(如*attrTween*、*styleTween*)必须延迟到过渡开始时求值。 - 调度(scheduled):创建后的不久(当前帧末或下一帧期间),过渡被调度。从此 delay 与
start事件监听器不可再修改,尝试修改会抛出错误信息 “too late: already scheduled”(若过渡已结束,则为 “transition not found”)。 - 开始(start):过渡启动时,它打断同一元素上同名的活跃过渡(如有),并向监听器派发
interrupt事件。文档特别强调两点:打断发生在 start 而非创建时——即使是零 delay 过渡也不会立刻打断活跃过渡(旧过渡还能拿到最后一帧);新过渡同时取消(cancel)同元素上同名且更早创建的待启动过渡。随后派发start事件。start 是最后一次可修改过渡的时刻:运行中的过渡其时序、tween、监听器都不可再改,尝试修改抛出 “too late: already running”(已结束时为 “transition not found”)。过渡在 start 之后立即初始化其 tweens。 - 逐帧运行(running):在本帧所有开始中的过渡都启动完之后,各过渡首次调用自己的 tweens——这种“批量初始化”避免了 DOM 读取与写入交错,是性能上的关键设计。此后每个活跃帧,过渡以缓动后的
*t*(0 到 1)调用 tweens;同一帧内按 tween 注册顺序调用。 - 结束(end):结束时过渡最后再调用一次 tweens,传入的是未缓动的
*t*= 1(保证精确落在目标值上),随后派发end事件。这是最后一次可以检查该过渡的时刻:end 之后过渡从元素上删除、配置被销毁(interrupt 或 cancel 同样会销毁配置)。此时再尝试检查会抛出 “transition not found”。
6.2 *selection*.interrupt(*name*) 与 interrupt(*node*, *name*)
两者分别作用于 selection 与单个节点:打断指定名称的活跃过渡并取消同名待启动过渡(未指定 name 时按 null 处理)。一个容易踩坑的点(文档原文强调):打断某个元素上的过渡,不会影响其子元素上的过渡。例如 d3-axis 的轴过渡实际由轴 G 元素下多个相互独立但同步的子过渡组成(刻度线、刻度标签、domain 路径等)。要打断整条轴的过渡,必须打断它的后代:
selection.selectAll("*").interrupt();
* 是通用选择器,选中所有后代元素;若还想同时打断 G 元素本身:
selection.interrupt().selectAll("*").interrupt();
6.3 *transition*.end():可等待的动画
*transition*.end() 返回一个 promise:当所有选中元素都完成过渡时 resolve;若任何元素的过渡被取消或打断,promise 则 reject。这使得“动画跑完再执行下一步”可以用 await 表达,而不再只能依赖事件回调。
6.4 *transition*.on(*typenames*, *listener*):过渡事件
为每个选中元素添加/移除监听器,事件 typenames 只能是四种字符串之一:
start— 过渡开始时;end— 过渡结束时;interrupt— 过渡被打断时;cancel— 过渡被取消时。
再次对照过渡的一生理解触发时机。务必注意:这些是过渡事件,不是 *selection*.on / *selection*.dispatch 实现的原生 DOM 事件。
类型名后可选地跟一个英文句点加名称(如 start.foo、start.bar),用于同一事件类型注册多个独立回调;多个 typenames 用空格分隔(如 interrupt end、start.foo start.bar)。事件派发时,监听器以 *d*、*i*、*nodes* 为参数、this 为当前 DOM 元素被调用;监听器总是看到元素的最新数据,但索引是选择(selection)的属性,在监听器分配时固定——要更新索引需重新分配监听器。
监听器的替换/移除规则:同一 typename 上已有监听器时,先移除旧监听器再添加新的;传 null 作为 listener 移除指定 typename 的监听器;传 null 且 typename 为 .foo 移除该名称下的所有监听器;typename 为 . 则移除所有无名称监听器。不指定 listener 时,返回第一个(非 null)选中元素上该 typename 的当前监听器(多个 typename 时返回第一个匹配的)。
6.5 实用工具方法:each / call / empty / nodes / node / size
以下方法与 selection 的同名方法行为等价,作用在过渡上:
*transition*.each(*function*):对每个选中元素调用函数(参数*d*、*i*、*nodes*,this为当前 DOM 元素),可用于同时访问父子数据的上下文逻辑;*transition*.call(*function*, ...*arguments*):把过渡与可选参数传给函数调用一次,并返回该过渡以支持链式调用。文档示例展示了“可复用函数 + call”的组合模式:
function color(transition, fill, stroke) {
transition
.style("fill", fill)
.style("stroke", stroke);
}
d3.selectAll("div").transition().call(color, "red", "blue");
// 等价于
color(d3.selectAll("div").transition(), "red", "blue");
*transition*.empty():是否不含(非 null)元素;*transition*.nodes():返回所有(非 null)元素数组;*transition*.node():返回第一个(非 null)元素,空过渡返回 null;*transition*.size():元素总数。
7. 本仓库中的工程事实与验证路径
d3 主仓库(umbrella package)并不包含 d3-transition 的实现源码,而是将其作为依赖聚合。以下事实可直接在本仓库中查证:
- 版本与依赖:package.json 声明
"version": "7.9.0"(即 d3 v7.9.0),依赖项包含"d3-transition": "^3.0.1",另有"d3-selection": "^3.0.0"、"d3-ease": "^3.0.1"、"d3-interpolate": "^3.0.1"等过渡的配套模块;运行时要求node >= 12。 - 统一导出:src/index.js 第 29 行
export * from "d3-transition";表明 d3-transition 的全部 API(d3.transition、active、interrupt等)被整体重导出到 d3 主命名空间,import * as d3 from "d3"后即可直接使用本文全部接口。 - 导出完整性测试:test/d3-test.js 遍历 package.json 的每个 dependency 并断言其导出项(除
version外)都出现在 d3 命名空间中,从工程上保证d3.transition、d3.active等符号不会丢失。 - 文档链接完整性测试:test/docs-test.js 会递归扫描
docs/下所有 Markdown,校验每个内部链接的目标文件与锚点({#anchor})真实存在。这意味着本文引用的 docs/d3-transition/ 目录下的四个子文档及其锚点(如selection_transition、transition_delay、the-life-of-a-transition)都是仓库中经过测试保证有效的一手 API 参考。 - 适用前提:本文所述 API 与默认值(delay 0ms、duration 250ms、ease 为 easeCubic 等)以本仓库 v7.9.0 文档为准;d3-transition 子包独立演进(^3.0.1),若单独安装该子包,请以对应版本的 API 为准。
8. 快速参考:方法-用途-文档位置
| 方法 | 作用 | 参考文档 |
|---|---|---|
*selection*.transition(*name*) |
从 selection 派生过渡;name 控制互斥域 | docs/d3-transition/selecting.md |
d3.transition(*name*) |
在文档根元素上建过渡 / 类型判断 | docs/d3-transition/selecting.md |
*transition*.select / selectAll / selectChild / selectChildren / filter |
子选择,继承 id、name、时序 | docs/d3-transition/selecting.md |
*transition*.merge(*other*) |
合并同 id 的两个过渡 | docs/d3-transition/selecting.md |
*transition*.transition() |
排定链式后续过渡 | docs/d3-transition/selecting.md |
active(*node*, *name*) |
取节点上活跃过渡,构建循环动画 | docs/d3-transition/selecting.md |
*transition*.delay(*value*) |
延迟(ms),默认 0 | docs/d3-transition/timing.md |
*transition*.duration(*value*) |
持续时长(ms),默认 250 | docs/d3-transition/timing.md |
*transition*.ease(*value*) |
缓动函数,默认 easeCubic | docs/d3-transition/timing.md |
*transition*.easeVarying(*factory*) |
逐元素自定义缓动 | docs/d3-transition/timing.md |
*transition*.attr / style |
目标值声明,自动选插值器 | docs/d3-transition/modifying.md |
*transition*.attrTween / styleTween |
自定义属性/样式插值器工厂 | docs/d3-transition/modifying.md |
*transition*.text / textTween |
文本整体替换 / 逐帧插值 | docs/d3-transition/modifying.md |
*transition*.remove() |
过渡结束时移除元素 | docs/d3-transition/modifying.md |
*transition*.tween(*name*, *value*) |
通用命名 tween | docs/d3-transition/modifying.md |
*selection*.interrupt(*name*) / interrupt(*node*, *name*) |
打断活跃过渡、取消待启动过渡 | docs/d3-transition/control-flow.md |
*transition*.end() |
返回 Promise,可 await | docs/d3-transition/control-flow.md |
*transition*.on(*typenames*, *listener*) |
start / end / interrupt / cancel 事件 | docs/d3-transition/control-flow.md |
each / call / empty / nodes / node / size |
与 selection 等价的工具方法 | docs/d3-transition/control-flow.md |
掌握以上内容后,你可以独立回答 d3 动画开发中最常见的问题:动画从哪里出发(起始值在过渡开始时捕获)、如何让多个元素同步或错峰(transition 实例 + delay/easeVarying)、如何让动画串行或循环(*transition*.transition + active)、如何自定义中间状态(三类 Tween + 自动插值器算法),以及如何安全地打断、等待与监听(interrupt / end / on 与生命周期五阶段)。
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