首页
/ d3-transition 完全实战指南:d3 v7 中的过渡、插值器与动画控制流

d3-transition 完全实战指南:d3 v7 中的过渡、插值器与动画控制流

2026-09-06 13:46:49作者:龚格成

d3-transition 是 d3 生态中负责“让数据动起来”的核心模块:它提供了一组与 selection 风格一致但面向动画的接口,让 DOM 属性、样式、文本从当前状态平滑插值到目标状态,而不是瞬间跳变。本文以本仓库文档 docs/d3-transition.md 及其四篇子文档(SelectingModifyingTimingControl 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 的既有过渡继承。这个机制用于两类场景:

  1. 跨多个 selection 同步同一个过渡
  2. 针对特定元素重新选中既有过渡并修改其配置。

文档给出的同步示例(两个 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)

过渡的 easingdelayduration 三者全部可配置。官方文档特别指出:利用逐元素 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 原文算法):

  1. value 是数字,使用 interpolateNumber
  2. value颜色或可强制转换为颜色的字符串,使用 interpolateRgb
  3. 否则使用 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*.ondocs/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/styleTweentween 可以直接写任意副作用(不限于设置某个属性/样式)。文档的示例——用 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)

文档按时间线精确描述了五个阶段,逐段理解有助于定位“为什么我改不动过渡”这类问题:

  1. 创建后(配置期):通过 *selection*.transition*transition*.transition 创建过渡后,可立即用 delaydurationattrstyle 等方法配置。注意两类方法的处理时点差异:指定目标值的方法(如 *transition*.attr)是同步求值的;而需要起始值参与插值的方法(如 *attrTween**styleTween*必须延迟到过渡开始时求值。
  2. 调度(scheduled):创建后的不久(当前帧末或下一帧期间),过渡被调度。从此 delay 与 start 事件监听器不可再修改,尝试修改会抛出错误信息 “too late: already scheduled”(若过渡已结束,则为 “transition not found”)。
  3. 开始(start):过渡启动时,它打断同一元素上同名的活跃过渡(如有),并向监听器派发 interrupt 事件。文档特别强调两点:打断发生在 start 而非创建时——即使是零 delay 过渡也不会立刻打断活跃过渡(旧过渡还能拿到最后一帧);新过渡同时取消(cancel)同元素上同名且更早创建的待启动过渡。随后派发 start 事件。start 是最后一次可修改过渡的时刻:运行中的过渡其时序、tween、监听器都不可再改,尝试修改抛出 “too late: already running”(已结束时为 “transition not found”)。过渡在 start 之后立即初始化其 tweens。
  4. 逐帧运行(running):在本帧所有开始中的过渡都启动完之后,各过渡首次调用自己的 tweens——这种“批量初始化”避免了 DOM 读取与写入交错,是性能上的关键设计。此后每个活跃帧,过渡以缓动后的 *t*(0 到 1)调用 tweens;同一帧内按 tween 注册顺序调用。
  5. 结束(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.foostart.bar),用于同一事件类型注册多个独立回调;多个 typenames 用空格分隔(如 interrupt endstart.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.transitionactiveinterrupt 等)被整体重导出到 d3 主命名空间,import * as d3 from "d3" 后即可直接使用本文全部接口。
  • 导出完整性测试test/d3-test.js 遍历 package.json 的每个 dependency 并断言其导出项(除 version 外)都出现在 d3 命名空间中,从工程上保证 d3.transitiond3.active 等符号不会丢失。
  • 文档链接完整性测试test/docs-test.js 会递归扫描 docs/ 下所有 Markdown,校验每个内部链接的目标文件与锚点({#anchor})真实存在。这意味着本文引用的 docs/d3-transition/ 目录下的四个子文档及其锚点(如 selection_transitiontransition_delaythe-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 与生命周期五阶段)。

登录后查看全文
热门项目推荐
相关项目推荐