首页
/ d3-interpolate 值插值完全指南:从 d3.interpolate 类型分派到 quantize 与 piecewise 采样

d3-interpolate 值插值完全指南:从 d3.interpolate 类型分派到 quantize 与 piecewise 采样

2026-09-04 09:48:12作者:滑思眉Philip

本文以 d3 官方文档中的“Value interpolation(值插值)”页面(docs/d3-interpolate/value.md)为核心,系统讲解 d3-interpolate 模块中最通用的这一族插值器:d3.interpolate 的类型分派算法、数值/字符串/日期/数组/对象插值的模板机制、B 样条与离散插值,以及 quantizepiecewise 两个把连续插值器转化为固定采样序列的工具函数。读完本文,你将能够正确地在 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 命名空间中。换言之,interpolateinterpolateNumberquantizepiecewise 等函数是否完整暴露给 d3,是被测试用例持续把关的事实。文档本身则由 VitePress 构建(见 package.jsondocs:dev / docs:build 脚本,构建前先执行 prebuild.sh 预生成示例数据)。

下面按原文档的脉络,逐一展开每个插值器。

d3.interpolate(a, b):基于终点类型的通用分派

d3.interpolate("red", "blue")(0.5) // "rgb(128, 0, 128)"

interpolate(a, b) 返回一个介于任意两个值 ab 之间的插值器。它是“最通用”的插值器,适合绝大多数值。其选择实现的关键在于终点值 b 的类型,算法如下(这是原文档给出的完整分派顺序,顺序即优先级):

  1. bnullundefined 或布尔值,使用常量 b(即插值器恒等于 b)。
  2. b 是数字,使用 interpolateNumber
  3. bcolor 或可强制转换为颜色的字符串,使用 interpolateRgb
  4. b 是 JavaScript Date 对象,使用 interpolateDate
  5. b 是字符串,使用 interpolateString
  6. b 是数字型 typed array(如 Float64Array),使用 interpolateNumberArray
  7. b 是普通数组(Array.isArray 判定),使用 interpolateArray
  8. b 可强制转换为数字,使用 interpolateNumber
  9. 其余情况使用 interpolateObject

选定插值器之后,a 会被强制转换为与之匹配的类型——这也是为什么上面示例里两个 CSS 颜色字符串会被自动走 RGB 通道插值,而无需调用方显式指定 d3.interpolateRgb。从分派顺序可以看出设计取向:颜色、日期这类“有语义”的类型优先于“可当字符串处理”的兜底逻辑。

interpolateNumber(a, b) 与 interpolateRound(a, b)

d3.interpolateNumber(20, 620)(0.8) // 500

interpolateNumber 返回两个数字 ab 之间的插值器,其数学形式等价于:

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"

字符串插值器会在 ab 中查找嵌入的数字——每个数字的形态须能被 JavaScript 识别。原文档给出的可被识别的数字形式示例:-1423.141596.0221413e+23(含负数、小数与科学计数法)。

工作机制分两步:

  1. b 中的每个嵌入数字,插值器尝试在 a 中找到对应的数字。找到则用 interpolateNumber 为该数字对创建数值插值器;
  2. 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 片段过渡(如 fontstroke-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 条的原因。同样警告:模板数组及参数 ab 本身都不做防御性拷贝,修改这些数组可能影响后续求值。

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}za 中缺失,静态取 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² 连续性(二阶导数连续)。它对应 curveBasisClosedinterpolateRgbBasisClosed——后者正是构建周期色标(如色相环循环插值)的基础。

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) 的四个采样值逐一展示。

原文档给出的警告必须留意:

对不返回防御性拷贝的插值器无效,例如 interpolateArrayinterpolateDateinterpolateObject。对这些插值器,你必须自行包装插值器,为每个返回值创建副本,否则 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 自动分派。

工程小结:约定、陷阱与组合方式

把原文档散布各节的警告与示例收拢起来,可以得到一组可操作的原则:

  1. 优先用 d3.interpolate 入口:分派算法会替你处理颜色、日期、typed array 等类型;只有当默认行为不符合预期(如想要 HSL/Lab 空间的颜色过渡)时,才显式改用 docs/d3-interpolate/color.md 中的专用插值器。
  2. 字符串生成路径避开 0:opacity 等会转字符串的数值插值从 1e-6 起步,规避科学计数法。
  3. 共享模板意识interpolateDate / interpolateArray / interpolateObject / interpolateNumberArray 返回的是共享模板(无防御性拷贝),帧内读值可以,帧间留存必须自行拷贝;使用 quantize 采样这类插值器时尤其要包装副本。
  4. 离散化两条路:类目化用 interpolateDiscrete(等价固定域 quantize scale);连续取样用 quantize;多段平滑过渡用 piecewise(等价轻量 linear scale),并与 d3-scale-chromatic 的色标组合使用。
  5. 数据空间插值:复杂图形(饼图扇区、层级布局节点等)插值其描述对象再交给形状生成器(如 arc),而不是插值最终的路径字符串。

以上 API 均可在当前仓库中直接验证:d3-interpolate 作为依赖声明于 package.json,全量导出经 src/index.js 透传、并由 test/d3-test.js 的导出一致性测试覆盖;文档页面(本指南所依据的 docs/d3-interpolate/value.md)随 VitePress 文档站一起构建,可与 docs/d3-interpolate/color.mddocs/d3-interpolate/transform.mddocs/d3-transition.md 对照阅读,形成“插值器 → 过渡 → 比例尺”的完整使用链。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384