d3-interpolate 值插值完全指南:从 d3.interpolate 类型分派到 quantize 与 piecewise 采样
本文以 d3 官方文档中的“Value interpolation(值插值)”页面(docs/d3-interpolate/value.md)为核心,系统讲解 d3-interpolate 模块中最通用的这一族插值器:d3.interpolate 的类型分派算法、数值/字符串/日期/数组/对象插值的模板机制、B 样条与离散插值,以及 quantize、piecewise 两个把连续插值器转化为固定采样序列的工具函数。读完本文,你将能够正确地在 d3 过渡动画(transition)与比例尺(scale)中选用、组合这些插值器,并理解其中“无防御性拷贝”等性能约定背后的原因。
这组 API 在当前仓库中的位置
在 d3 聚合包中,d3-interpolate 的整套导出通过 src/index.js 中的 export * from "d3-interpolate" 一行原样透传给 d3 命名空间,因此文档中所有 d3.interpolate* 写法都直接可用。package.json 中声明的依赖版本为 "d3-interpolate": "^3.0.1"(d3 聚合包当前版本 7.9.0,要求 Node >= 12),这意味着本文描述的行为对应 d3-interpolate 3.x 系列。
仓库自带的 test/d3-test.js 提供了一个可执行的验证机制:它遍历 package.json 中每个依赖模块,import 该模块后逐一断言其每个导出属性都存在于 d3 命名空间中。换言之,interpolate、interpolateNumber、quantize、piecewise 等函数是否完整暴露给 d3,是被测试用例持续把关的事实。文档本身则由 VitePress 构建(见 package.json 中 docs:dev / docs:build 脚本,构建前先执行 prebuild.sh 预生成示例数据)。
下面按原文档的脉络,逐一展开每个插值器。
d3.interpolate(a, b):基于终点类型的通用分派
d3.interpolate("red", "blue")(0.5) // "rgb(128, 0, 128)"
interpolate(a, b) 返回一个介于任意两个值 a 与 b 之间的插值器。它是“最通用”的插值器,适合绝大多数值。其选择实现的关键在于终点值 b 的类型,算法如下(这是原文档给出的完整分派顺序,顺序即优先级):
- 若 b 是
null、undefined或布尔值,使用常量 b(即插值器恒等于 b)。 - 若 b 是数字,使用 interpolateNumber。
- 若 b 是 color 或可强制转换为颜色的字符串,使用 interpolateRgb。
- 若 b 是 JavaScript Date 对象,使用 interpolateDate。
- 若 b 是字符串,使用 interpolateString。
- 若 b 是数字型 typed array(如
Float64Array),使用 interpolateNumberArray。 - 若 b 是普通数组(
Array.isArray判定),使用 interpolateArray。 - 若 b 可强制转换为数字,使用 interpolateNumber。
- 其余情况使用 interpolateObject。
选定插值器之后,a 会被强制转换为与之匹配的类型——这也是为什么上面示例里两个 CSS 颜色字符串会被自动走 RGB 通道插值,而无需调用方显式指定 d3.interpolateRgb。从分派顺序可以看出设计取向:颜色、日期这类“有语义”的类型优先于“可当字符串处理”的兜底逻辑。
interpolateNumber(a, b) 与 interpolateRound(a, b)
d3.interpolateNumber(20, 620)(0.8) // 500
interpolateNumber 返回两个数字 a、b 之间的插值器,其数学形式等价于:
function interpolator(t) {
return a * (1 - t) + b * t;
}
即标准线性混合(lerp)。这是所有其他数值插值的基础原语。
原文档特别强调一条实践约束(CAUTION):
避免在与 0 之间的数值插值用于生成字符串时经过 0。 当极小数值被字符串化时,会转成科学计数法,而科学计数法在部分旧版浏览器中不是合法的 attribute 或 style 属性值。例如数字
0.0000001会被转成字符串"1e-7"。这在插值 opacity 时尤为明显。要避免科学计数法,应从/到1e-6开始或结束过渡——这是不会被字符串化为科学计数法的最小值。
这条经验规则在 d3 的 attr / style 过渡中非常实用:把透明度过渡写成 style("opacity", 1e-6) → style("opacity", 1) 而不是从 0 开始,是社区通行的写法。
interpolateRound(a, b) 行为与 interpolateNumber 相同,但会把结果四舍五入到最接近的整数:
d3.interpolateRound(20, 620)(0.821) // 513
适用于元素数量、像素步长等必须为整数的场景,避免动画过程中出现小数的计数值或坐标。
interpolateString(a, b):嵌入数字的模板插值
d3.interpolateString("20px", "32px")(0.5) // "26px"
字符串插值器会在 a 与 b 中查找嵌入的数字——每个数字的形态须能被 JavaScript 识别。原文档给出的可被识别的数字形式示例:-1、42、3.14159、6.0221413e+23(含负数、小数与科学计数法)。
工作机制分两步:
- 对 b 中的每个嵌入数字,插值器尝试在 a 中找到对应的数字。找到则用
interpolateNumber为该数字对创建数值插值器; - b 中剩余的静态部分作为模板:静态片段在插值过程中保持 b 的形态不变,仅其中的数值被内嵌插值器替换。
原文档的经典例子:a 为 "300 12px sans-serif",b 为 "500 36px Comic-Sans",则检测到两个嵌入数字(300/500 与 12/36),静态部分为两数字之间的空格 " " 以及后缀 "px Comic-Sans"。插值器在 t = 0.5 时的结果是 "400 24px Comic-Sans"。注意字体名等非数字部分直接取自 b——这意味着字符串插值适合“骨架一致、数值变化”的 CSS 片段过渡(如 font、stroke-width 混合了数字与单位的情形),而不是任意文本的形变。
interpolateDate(a, b):时间插值与“无防御性拷贝”
d3.interpolateDate(new Date("2014-01-01"), new Date("2024-01-01"))(0.5) // 2019-01-01
日期插值器内部按时间戳做线性插值,再把结果装回 Date 实例。原文档给出了一个重要的性能约定:
返回的 Date 不会做防御性拷贝——每次求值返回的是同一个 Date 实例,不拷贝是出于性能考虑,因为插值器常常位于动画过渡的内循环中。
这一约定是理解后续几节警告的钥匙:插值器被设计为高频调用(每帧每元素一次),因此凡是涉及对象/数组/日期返回值的插值器,默认共享内部模板,而不是每次 t 求值都返回新副本。
interpolateArray(a, b) 与 interpolateNumberArray(a, b)
d3.interpolateArray([0, 0, 0], [1, 2, 3])(0.5) // [0.5, 1, 1.5]
interpolateArray 的模板机制:
- 内部创建与 b 等长的数组模板;
- 对 b 的每个元素,若 a 中存在对应位置的元素,就用通用的 interpolate 为该元素对创建插值器;若 a 中不存在(a 更短),则模板中直接使用 b 的静态值;
- 对给定参数 t,依次求值模板内嵌的插值器并返回更新后的数组模板。
原文档的例子:a = [0, 1],b = [1, 10, 100],t = 0.5 时结果为 [0.5, 5.5, 100]——第三个位置 a 中无对应元素,因此静态取 100。
同样地,这里有“无防御性拷贝”警告:模板数组本身被共享,修改返回的数组会反过来污染插值器后续求值的结果(性能原因同上,插值器处于过渡内循环)。
interpolateNumberArray(a, b) 是 typed array 的专用路径:
d3.interpolateNumberArray([0, 1], Float64Array.of(1, 3))(0.5) // [0.5, 2]
内部创建与 b 同类型、同长度的数组模板;对 b 每个元素,若 a 有对应值则直接做数值插值写入模板,否则静态拷贝 b 的值,最后返回更新后的模板。与 interpolateArray 不同,它不逐个元素走通用 interpolate,因此对数值型 typed array 更快,这也是 d3.interpolate 分派算法中第 6 条优先于第 7 条的原因。同样警告:模板数组及参数 a、b 本身都不做防御性拷贝,修改这些数组可能影响后续求值。
interpolateObject(a, b):对象插值与数据空间插值
d3.interpolateObject({x: 0, y: 1}, {x: 1, y: 10, z: 100})(0.5) // {x: 0.5, y: 5.5, z: 100}
对象插值的模板机制与数组版本同构,只是维度从“下标”换成“属性名”:
- 创建具有 b 全部属性的对象模板;
- 对 b 的每个属性,若 a 中存在同名属性,用通用 interpolate 创建插值器;否则模板中静态取 b 的值;
- 对参数 t 求值内嵌插值器后返回更新后的对象模板。
原文档给出的例子:a = {x: 0, y: 1},b = {x: 1, y: 10, z: 100},t = 0.5 时为 {x: 0.5, y: 5.5, z: 100}(z 在 a 中缺失,静态取 100)。
数据空间插值(dataspace interpolation) 是对象插值最具实战价值的用法:你插值的是描述图形的数据对象,而不是最终的 SVG 属性。例如对一个描述饼图扇区的对象 {startAngle, endAngle, innerRadius, ...} 做插值,每一帧再把插值结果交给 arc 形状生成器重算 d 属性。相比直接对路径字符串做插值,数据空间插值保证了每一帧的几何形状都是合法的,这也是 d3 过渡动画中饼图、树图平滑变形的标准做法。
同样地,无防御性拷贝警告适用:返回的对象模板被共享,修改它会影响后续求值。
interpolateBasis 与 interpolateBasisClosed:一维 B 样条
d3.interpolateBasis([0, 0.1, 0.4, 1])(0.5) // 0.2604166666666667
interpolateBasis(values) 返回穿过给定数值数组的均匀非有理 B 样条插值器(values 必须是数字)。它会隐式生成控制点,使得插值器在 t = 0 时返回 values[0]、在 t = 1 时返回 values[values.length - 1],即两端精确贴合端点值。它与 curveBasis(形状生成中的 B 样条曲线)和 interpolateRgbBasis(颜色版 B 样条)属于同一数学工具的三个变体。
d3.interpolateBasisClosed([0, 0.1, 0.4, 1])(0.5) // 0.45
interpolateBasisClosed(values) 同样是均匀非有理 B 样条,但控制点被隐式循环重复,使结果的一维样条在 t ∈ [0,1] 循环衔接时具有周期性的 C² 连续性(二阶导数连续)。它对应 curveBasisClosed 与 interpolateRgbBasisClosed——后者正是构建周期色标(如色相环循环插值)的基础。
interpolateDiscrete(values):轻量级 quantize 比例尺
d3.interpolateDiscrete(["red", "blue", "green"])(0.5) // "blue"
离散插值器把 [0, 1) 均分为 n 段(n = values.length):t ∈ [0, 1/n) 映射到 values[0],t ∈ [1/n, 2/n) 映射到 values[1],依此类推。原文档的概括很到位:这本质上是一个固定域为 [0, 1] 的轻量级 quantize scale。当你的数据本身已归一化到 [0, 1](例如比例尺的 range 已经是有序类目数组)时,可以直接用它替代一次完整的 quantize scale 构造。
quantize(interpolator, n):从连续插值器取 n 个均匀采样
d3.quantize(d3.interpolate("red", "blue"), 4)
// ["rgb(255, 0, 0)", "rgb(170, 0, 85)", "rgb(85, 0, 170)", "rgb(0, 0, 255)"]
quantize 对任意插值器取 n(大于 1 的整数)个等间距采样:第一个采样恒在 t = 0,最后一个恒在 t = 1。典型用途是从连续插值器派生固定数量的样本,例如把一个连续插值器(如 interpolateWarm 这类色标)离散成 quantize scale 的 range。
在本文档页面中,这个函数的可视化效果正是由 docs/components/ColorSwatches.vue 组件渲染出来的色块条——对 d3.quantize(d3.interpolate('red', 'blue'), 4) 的四个采样值逐一展示。
原文档给出的警告必须留意:
对不返回防御性拷贝的插值器无效,例如
interpolateArray、interpolateDate、interpolateObject。对这些插值器,你必须自行包装插值器,为每个返回值创建副本,否则quantize返回的 n 个“不同”样本实际上全部指向同一个内部模板。
这与前文的性能约定一脉相承:插值器为过渡内循环省掉了拷贝,而采样类工具又要求每个样本彼此独立,二者的矛盾需要使用者显式调和。
piecewise(interpolate, values):轻量级 linear 比例尺
d3.piecewise(d3.interpolateRgb.gamma(2.2), ["red", "green", "blue"])
d3.piecewise(["red", "green", "blue"])
piecewise 为每对相邻值 values[i]、values[i+1] 组合一个插值器,返回分段插值器:t ∈ [0, 1/(n−1)] 走 interpolate(values[0], values[1]),t ∈ [1/(n−1), 2/(n−1)] 走 interpolate(values[1], values[2]),依此类推(n = values.length)。若未指定 interpolate,默认为通用的 interpolate。
原文档把它概括为:本质上是一个轻量级 linear scale——多段线性域映射,每段内部按给定插值器平滑过渡。文档页面中的色带可视化由 docs/components/ColorRamp.vue 完成:它在一个 1×256 的 canvas 上逐像素调用 color(i / (n - 1)),把插值器在 [0, 1] 上的完整输出渲染成一条色带,这也是官方文档中所有 piecewise / quantize / 色标示例的通用演示手段。
自定义段间插值器是 piecewise 的关键能力:上面的 d3.interpolateRgb.gamma(2.2) 让红绿蓝三段过渡都先做 gamma 校正再混合,避免中段出现视觉上的灰暗断层;而默认调用则对每段使用通用 interpolate 自动分派。
工程小结:约定、陷阱与组合方式
把原文档散布各节的警告与示例收拢起来,可以得到一组可操作的原则:
- 优先用
d3.interpolate入口:分派算法会替你处理颜色、日期、typed array 等类型;只有当默认行为不符合预期(如想要 HSL/Lab 空间的颜色过渡)时,才显式改用 docs/d3-interpolate/color.md 中的专用插值器。 - 字符串生成路径避开 0:opacity 等会转字符串的数值插值从
1e-6起步,规避科学计数法。 - 共享模板意识:
interpolateDate/interpolateArray/interpolateObject/interpolateNumberArray返回的是共享模板(无防御性拷贝),帧内读值可以,帧间留存必须自行拷贝;使用quantize采样这类插值器时尤其要包装副本。 - 离散化两条路:类目化用
interpolateDiscrete(等价固定域 quantize scale);连续取样用quantize;多段平滑过渡用piecewise(等价轻量 linear scale),并与d3-scale-chromatic的色标组合使用。 - 数据空间插值:复杂图形(饼图扇区、层级布局节点等)插值其描述对象再交给形状生成器(如 arc),而不是插值最终的路径字符串。
以上 API 均可在当前仓库中直接验证:d3-interpolate 作为依赖声明于 package.json,全量导出经 src/index.js 透传、并由 test/d3-test.js 的导出一致性测试覆盖;文档页面(本指南所依据的 docs/d3-interpolate/value.md)随 VitePress 文档站一起构建,可与 docs/d3-interpolate/color.md、docs/d3-interpolate/transform.md、docs/d3-transition.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 StartedRust0623
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