d3 转场定时机制详解:transition.delay、duration、ease 与 easeVarying 的完整用法
在 D3(Data-Driven Documents)中,transition 负责把选区从当前状态平滑地过渡到目标状态,而 transition 的定时机制 决定了这场"过渡"何时开始、持续多久、以什么节奏推进。本文基于当前仓库(d3 v7.9.0)的官方文档 docs/d3-transition/timing.md,系统讲解 delay、duration、ease、easeVarying 四个 API 的取值规则、默认值与函数式配置方式,并结合仓库源码结构剖析其底层约定,帮助你写出从"逐个错开启动"到"按数据驱动缓动"的精细动画。
一、三个可配置维度与默认值
一次 D3 transition 的时间行为由三个维度共同决定:
| 维度 | API | 含义 | 默认值 |
|---|---|---|---|
| 延迟 | transition.delay(value) |
每个元素启动前的等待时间(毫秒) | 0 |
| 时长 | transition.duration(value) |
每个元素动画本身的持续时间(毫秒) | 250 |
| 缓动 | transition.ease(value) / transition.easeVarying(factory) |
将归一化时间 t 映射为缓动后时间 tʹ 的函数 | d3.easeCubic |
官方文档在 timing.md 开篇即点明了定时机制的实战价值:逐元素延迟(per-element delay)可以错开元素的过渡节奏,例如在排序柱状图中让每一根柱子按索引依次移动,显著改善视觉感知(文档引用了可视化领域的经典研究《Animated Transitions in Statistical Data Graphics》作为依据)。
一个直观的组合示例:
selection
.transition()
.delay(250) // 整体先等 250ms
.duration(750) // 每根柱子动画持续 750ms
.ease(d3.easeCubic) // 默认就是 easeCubic,这里显式写出
.attr("transform", (d, i) => `translate(0,${y(d.value)})`);
二、transition.delay(value)
基本用法
为选区中的每个元素设置过渡延迟,单位为毫秒:
transition.delay(250);
value 既可以是常量,也可以是函数:
// 函数形式:延迟随数据索引递增,形成"级联启动"效果
transition.delay((d, i) => i * 10);
函数形式求值约定
若 value 为函数,它会**立即(immediately)**按顺序对每个选中的元素求值,求值时的约定与 d3-selection 的访问器函数完全一致:
- 依次传入当前数据(
d)、当前索引(i)、当前分组(nodes); this指向当前 DOM 元素;- 函数的返回值即该元素的延迟。
这意味着函数里可以直接读 this 上的状态(例如 this.getAttribute('x'))或按数据字段计算,而不必依赖外部闭包变量。
取值(getter)与默认值
- 不传参数调用时,返回第一个非空元素的当前延迟值:
transition.delay() // 250
文档特别提醒:这一 getter 只在"你确定 transition 恰好只包含一个元素"时才真正有用——多元素选区读到的只是第一个元素值。
- 若不指定延迟,默认值为 0。
错开(staggering)的两种进阶写法
- 按索引错开:把延迟设为索引的倍数是最方便的写法,如上面的
i * 10,100 个元素会在 1000ms 内依次启动; - 按数据错开,或排序后再按索引错开:延迟可以写成数据的任意函数;官方文档还建议在计算索引型延迟之前,先对选区排序(参见 selection.sort 的文档),这样"视觉上从上到下依次移动"的效果才能与数据顺序一致。
// 先排序,再错开:让柱子从最长到最短依次就位
selection
.sort((a, b) => b.value - a.value)
.transition()
.delay((d, i) => i * 15)
.duration(400)
.attr("height", (d) => y(d.value));
三、transition.duration(value)
基本用法
为每个元素设置过渡时长,单位为毫秒:
transition.duration(750);
与 delay 一样,value 可以是常量或函数。函数形式同样是立即对每个选中的元素按顺序求值,传入 d、i、nodes,this 为当前 DOM 元素,返回值即该元素的时长:
// 数据越大的柱子过渡越久,形成"重量感"
selection.transition()
.duration((d) => 250 + d.value * 2);
默认值与 getter 行为:
- 不指定时长时,默认 250ms(这是 d3-transition 的内置默认时长);
- 不传参调用时,返回第一个非空元素的当前时长:
transition.duration() // 750
同样注意:getter 仅在 transition 恰好只含一个元素时才有普遍意义。
duration 与 delay 的协作
两个参数都是逐元素独立的,因此总动画窗口可以按元素估算为 delay(i) + duration(i)。实践中常把 delay 用作"启动编排"、duration 用作"单元素节奏",例如入场动画 delay(i * 30).duration(300),离场动画则常用 duration(200).remove()(remove() 的用法见 transition 修改类 API 文档)。
四、transition.ease(value)
缓动函数的契约
ease(value) 为所有选中元素指定同一个缓动函数:
transition.ease(d3.easeCubic);
value 必须是函数(不能是字符串)。缓动函数在动画的每一帧被调用一次,约定如下:
- 入参是归一化时间 t,取值范围 [0, 1];
- 返回缓动后的时间 tʹ,通常也在 [0, 1] 内;
- 一个好的缓动函数应满足 t = 0 时返回 0,t = 1 时返回 1。
若不指定,默认使用 d3.easeCubic(即 easeCubicInOut,对称三次缓动)。getter 行为:
transition.ease() // d3.easeCubic
同样,不传参返回的是第一个非空元素的当前缓动函数,仅在单元素 transition 中才有确定意义。
内置缓动函数家族
d3-ease 提供了一族以"指数"为参数的缓动函数,d3.easePolyIn 通过 .exponent(e) 可生成不同"陡峭度"的曲线(默认 e = 3,等价于三次缓动);easeCubic 是 easeCubicInOut 的别名。完整的函数列表、公式与曲线示例见 d3-ease 文档。本仓库的 docs/components/ExampleEase.vue 就是用 d3.ticks(0, 1, 500) 生成 500 个采样点、再对每个缓动函数求值绘制曲线,直观展示了 easePolyIn.exponent(e) 随指数变化的形态——这正是理解"缓动函数 = 把线性时间重排成非线性时间"的最好方式。
从源码结构看,缓动函数只负责"时间的重排",真正的属性插值(数值、颜色、路径等)由 transition 的 tween 机制配合 d3-interpolate 完成(参见 transition 修改类 API),两者组合起来才构成完整的逐帧动画。
五、transition.easeVarying(factory)
当"同一个缓动函数不适合所有元素"时,easeVarying 提供了按元素生成不同缓动函数的工厂机制:
transition.easeVarying((d) => d3.easePolyIn.exponent(d.exponent));
与 ease 的区别在于:
| 维度 | ease(value) |
easeVarying(factory) |
|---|---|---|
| 参数类型 | 必须是缓动函数 | 必须是返回缓动函数的工厂 |
| 作用范围 | 所有选中元素共用一个函数 | 每个节点调用一次工厂,各自得到一个函数 |
| 求值时机 | 设置时一次 | 对选区的每个节点求值 |
| 求值约定 | — | 传入 d、i、nodes,this 为当前 DOM 元素 |
上例中,每个数据对象自带 exponent 字段,于是每根柱子都拥有"陡峭度"不同的 easePolyIn 曲线——这是纯数据驱动的差异化动画。例如指数越大,加速越猛,适合表现"数据量越大的元素冲得越快"这类叙事。
由于工厂函数拥有与 d3 访问器完全一致的求值约定,你也可以根据 d 的类别、i 的奇偶或 this 上的几何位置来挑选缓动函数,实现成组的、方向感的或空间感知的节奏差异。
六、仓库源码层面的印证
- 本仓库是 d3 v7 的聚合入口包。src/index.js 第 29 行
export * from "d3-transition";说明 transition 的全部 API(含 delay/duration/ease/easeVarying)以 re-export 方式从独立的d3-transition包进入 d3 命名空间; - package.json 中
"d3-transition": "^3.0.1"与"d3-ease": "^3.0.1"声明了当前版本所依赖的两个子包版本。timing.md 文档中标注的 Source 文件(src/transition/delay.js、src/transition/duration.js、src/transition/ease.js、src/transition/easeVarying.js)位于 d3-transition 独立仓库中,本仓库的 docs/d3-transition/timing.md 即其官方文档镜像,可作为 API 语义的权威依据; - 文档构建流程由 package.json 的
docs:dev/docs:build脚本驱动(VitePress),因此 docs 下的 Markdown 与 d3 官方文档站保持同一套源文件,API 描述可直接当作当前仓库行为的规范来看待; - 测试入口 test/d3-test.js 与 test/docs-test.js 分别负责库行为与文档构建校验,
yarn test(即mocha 'test/**/*-test.js' && eslint src test)是验证本仓库改动的基础命令。
七、实用速查与易错点
- 单位:
delay与duration均为毫秒,不要与归一化的 0–1 时间混淆; - 默认值:delay 0ms、duration 250ms、ease
easeCubic——不写定时代码也能得到一个 250ms 的默认过渡; - 函数求值是立即的:delay/duration/easeVarying 的函数在设置时就被逐元素求值并固化,而不是每帧求值;每帧被调用的只有缓动函数本身;
- getter 的适用边界:
transition.delay()等 getter 只报告第一个非空元素的值,多元素选区不要依赖它做断言; - ease 必须是函数:传字符串会静默失效,需要
d3.easeCubicInOut这类函数引用; - 排序先于索引错开:希望"按视觉位置依次启动"时,先
sort再计算i * step,否则索引顺序与视觉顺序可能不一致; - 需要逐帧逻辑时用 tween:timing 只控制"时间轴",若需要逐帧自定义属性更新,应转到 tween/attrTween 等 API;需要等待动画结束再执行后续动作时,用 control-flow 中的 on/end。
八、相关文档索引
- docs/d3-transition/timing.md:本文对应的官方 API 文档(delay / duration / ease / easeVarying)
- docs/d3-transition/modifying.md:attr、style、text、tween、remove 等过渡中的属性修改
- docs/d3-transition/control-flow.md:on、each、end 等控制流 API
- docs/d3-transition/selecting.md:select、filter、merge、嵌套 transition
- docs/d3-ease.md:全部内置缓动函数及其公式、曲线
- docs/d3-selection/modifying.md:selection.sort 等与错开动画配合使用的选区操作
掌握 delay/duration/ease/easeVarying 四个 API 及其"常量或函数"的取值约定,就掌握了 D3 转场时间轴的全部控制面:从统一的 250ms 默认过渡,到按索引级联、按数据差异化缓动的精细编排,都是这四行 API 的直接组合。
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