首页
/ d3-format 数字格式化完全指南:格式说明符微型语言、Locale 与 SI 前缀在 d3 中的实战应用

d3-format 数字格式化完全指南:格式说明符微型语言、Locale 与 SI 前缀在 d3 中的实战应用

2026-09-04 20:28:44作者:齐添朝

本文基于 d3 官方文档 d3-format 展开。d3-format 解决的是“把数字变成适合人类阅读的字符串”这一问题:消除二进制浮点误差带来的 0.30000000000000004、统一表格列宽、千分位分组、货币精度、科学计数与 SI 前缀、以及按地区(locale)适配分隔符。读完后,你将掌握完整的格式说明符(specifier)语法、d3.formatLocale / d3.formatDefaultLocale 的地区定义方式、formatPrefix 的 SI 前缀机制,以及 precisionFixed / precisionRound / precisionPrefix 三个精度推断工具,并能理解它们在 d3 坐标轴刻度格式化等模块中的真实用途。

一、问题背景:为什么不能直接打印 JavaScript 数字

d3-format 文档开篇用一个经典例子说明了问题所在:

for (let i = 0; i < 10; ++i) {
  console.log(0.1 * i);
}

输出是:

0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001
0.7000000000000001
0.8
0.9

这是 IEEE 754 双精度浮点表示的必然结果。d3 仓库中 d3-array/ticks.md 同样明确指出这一行为源自 IEEE 754 双精度浮点(0.2 * 3 = 0.6000000000000001),并建议“使用 d3-format 对数字做适合人类阅读的格式化(Use d3-format to format numbers for human consumption with appropriate rounding)”。

除了舍入误差,数字格式化还有更多动机:

  • 表格中的数字应统一格式以便比较(例如 0.0 优于 0);
  • 大数应做千分位分组(42,000)或使用科学计数 / SI 记法(4.2e+442k);
  • 货币应固定精度($3.50);
  • 统计结果应按有效数字舍入(4021 变为 4000);
  • 格式应适配读者地区(42.000,0042,000.00)。

d3-format 的设计借鉴了 Python 3 的 format specification mini-language(PEP 3101)。重新审视上面的例子:

const f = d3.format(".1f");
for (let i = 0; i < 10; ++i) {
  console.log(f(0.1 * i));
}

输出变为干净的 0.00.9

二、d3-format 在 d3 仓库中的位置

当前仓库(d3 7.9.0)是一个 monorepo 风格的总包:d3-format 作为独立依赖 ^3.1.0 声明在 package.json 中,并在 src/index.js 中通过 export * from "d3-format"; 全量再导出,因此安装 d3 后即可直接使用 d3.format 等全部 API,无需单独引入 d3-format。docs/api.md 中对 d3-format 的条目索引也列出了 d3.formatd3.formatPrefixd3.formatSpecifierd3.precisionFixedd3.precisionPrefixd3.precisionRoundd3.formatLocaled3.formatDefaultLocale 这些方法。

需要说明的是:d3-format 的源码(如 src/locale.jssrc/formatSpecifier.js)位于独立的 d3-format 依赖中,当前仓库不包含其实现文件;本文所有 API 语义均以 docs/d3-format.md 的官方描述为准。

三、d3.format:基础格式化器

d3.format(specifier) 是默认地区上 locale.format 的别名。返回一个格式化函数,接受一个数字,返回格式化后的字符串:

const f = d3.format(".2f");

文档给出的示例集覆盖了常见场景(以下注释即文档标注的期望输出):

d3.format(".0%")(0.123)   // 舍入的百分比,"12%"
d3.format("($.2f")(-3.5)  // 本地化定点货币,"(£3.50)"
d3.format("+20")(42)      // 空格填充且带符号,"                 +42"
d3.format(".^20")(42)     // 点填充且居中,".........42........."
d3.format(".2s")(42e6)    // 两位有效数字的 SI 前缀,"42M"
d3.format("#x")(48879)    // 带前缀的小写十六进制,"0xbeef"
d3.format(",.2r")(4223)  // 千分位分组加两位有效数字,"4,200"

文档同时提醒:可以用 d3.formatSpecifier 对上述任意说明符做解码,查看每个字段被解析成什么(见第六节)。

四、格式说明符的完整语法

locale.format(specifier)(及 d3.format 别名)接受一个字符串说明符,其通用形式为:

[[fill]align][sign][symbol][0][width][,][.precision][~][type]

4.1 fill 与 align

fill 可以是任意字符,其存在由紧随其后的 align 字符来标识。align 必须是以下之一:

  • > — 右对齐(默认行为);
  • < — 左对齐;
  • ^ — 居中;
  • = — 与 > 类似,但符号和货币符号位于填充之前。

4.2 sign

  • - — 零或正数无符号,负数用减号(默认行为);
  • + — 零或正数用加号,负数用减号;
  • ( — 零或正数无符号,负数用圆括号;
  • (空格)— 零或正数用空格,负数用减号。

一个值得注意的仓库事实:CHANGES.md 记载,在 d3 v7 中 d3.format 对负值的默认负号从连字符(hyphen-minus)改为了真正的减号(minus sign),这是 v7 的行为变更之一。

4.3 symbol

  • $ — 按地区定义应用货币符号;
  • # — 用于二进制、八进制、十六进制,分别加 0b0o0x 前缀。

4.4 zero、width 与逗号

0 选项启用零填充;它会隐式地把 fill 设为 0align 设为 =width 定义最小字段宽度,不指定时由内容决定宽度。, 选项启用分组分隔符(如千位逗号)。

4.5 precision 的语义(重要)

precision 的含义取决于 type

  • 对类型 f%,precision 表示小数点后的位数
  • 对类型 (none)、egrsp,precision 表示有效数字的位数

未指定 precision 时,除 none 类型默认为 12 外,其他类型均默认为 6。整数格式(bodxX)和字符类型 c 会忽略 precision。

4.6 ~ 修剪选项

~ 选项在所有格式类型中修剪无意义的末尾零,最常与 res% 配合使用:

d3.format("s")(1500)  // "1.50000k"
d3.format("~s")(1500) // "1.5k"

4.7 type 类型总表

type 含义
e 指数记法
f 定点记法
g 小数或指数记法,按有效数字舍入
r 十进制记法,按有效数字舍入
s 带 SI 前缀的十进制记法,按有效数字舍入
% 乘以 100,十进制记法加百分号
p 乘以 100,按有效数字舍入,十进制记法加百分号
b 二进制记法,舍入到整数
o 八进制记法,舍入到整数
d 十进制记法,舍入到整数
x 十六进制(小写字母),舍入到整数
X 十六进制(大写字母),舍入到整数
c 字符数据,用于文本字符串

此外:

  • 类型 (none)是 ~g 的简写,默认 precision 为 12(而非 6);
  • 类型 n,g 的简写;
  • gn 和 none 类型,若结果字符串的位数不超过 precision 则用小数记法,否则用指数记法:
d3.format(".2")(42)  // "42"
d3.format(".2")(4.2) // "4.2"
d3.format(".1")(42)  // "4e+1"
d3.format(".1")(4.2) // "4"

五、formatPrefix:一致的 SI 前缀格式化

d3.formatPrefix(specifier, value) 是默认地区上 locale.formatPrefix 的别名:

const f = d3.formatPrefix(",.0", 1e-6);

它返回一个格式化器,在按定点记法格式化之前,先将数值换算到与参考数值 value 相匹配的 SI 前缀单位。支持的完整前缀列表为:

前缀 名称 量级
y yocto 10⁻²⁴
z zepto 10⁻²¹
a atto 10⁻¹⁸
f femto 10⁻¹⁵
p pico 10⁻¹²
n nano 10⁻⁹
µ micro 10⁻⁶
m milli 10⁻³
(none) 10⁰
k kilo 10³
M mega 10⁶
G giga 10⁹
T tera 10¹²
P peta 10¹⁵
E exa 10¹⁸
Z zetta 10²¹
Y yotta 10²⁴

s 类型的关键区别在于:

  1. formatPrefix 返回的是一致的 SI 前缀——所有数值共用同一前缀,而不是像 s 类型那样为每个数字动态计算前缀;
  2. specifier 中的 precision 含义变为小数点后的位数(类似 f),而非有效数字位数。
const f = d3.formatPrefix(",.0", 1e-6);
f(0.00042); // "420µ"
f(0.0042);  // "4,200µ"

这使得同一单位下的多个数字可以直接比较。CHANGES.md 中还记录了这一 API 的历史演进:d3 v4 起 d3.formatPrefix 由“返回一个 SI 前缀字符串”改为“返回给定 specifier 和参考 value 的 SI 前缀格式化函数”,例如 d3.formatPrefix(",.0", 1e3) 用来格式化千级数字——强调与 s 指令不同,它始终采用同一个 SI 前缀,产生一致的结果。

仓库中的真实用例

文档站示例 ExampleChord.vue 展示了 formatPrefix 的典型用法:先由 d3.tickStep(0, sum, 100) 计算刻度步长,再用它同时作为 SI 前缀的参考值和刻度间距:

const tickStepMinor = d3.tickStep(0, sum, 100);
const formatValue = d3.formatPrefix(",.0", tickStepMinor);

格式化后的 formatValue(d.value) 直接渲染到弦图的刻度文本上(ExampleChord.vue)。这是一个“刻度步长决定单位,单位决定格式化器”的完整闭环。

六、formatLocale 与 formatDefaultLocale:地区定义

6.1 formatLocale(definition)

const enUs = d3.formatLocale({
  thousands: ",",
  grouping: [3],
  currency: ["$", ""]
});

返回一个 locale 对象,带有 locale.formatlocale.formatPrefix 两个方法。definition 必须包含:

  • decimal — 小数点(如 ".");
  • thousands — 分组分隔符(如 ",");
  • grouping — 分组长度数组(如 [3]),按需循环使用;
  • currency — 货币前缀和后缀(如 ["$", ""]);
  • numerals — 可选;十个字符串,用于替换数字 0–9;
  • percent — 可选;百分号(默认 "%");
  • minus — 可选;减号(默认 "−");
  • nan — 可选;非数值(默认 "NaN")。

注意文档的提醒:thousands 属性其实是个“误称”,因为 grouping 定义允许非千位分组的分组方式。

6.2 formatDefaultLocale(definition)

const enUs = d3.formatDefaultLocale({
  thousands: ",",
  grouping: [3],
  currency: ["$", ""]
});

d3.formatLocale 等价,额外会把 d3.formatd3.formatPrefix 重新指向新地区的 locale.format / locale.formatPrefix。若从未设置默认地区,则默认使用美式英语(U.S. English)定义。

七、formatSpecifier 与 new FormatSpecifier:解码与派生说明符

7.1 formatSpecifier(specifier)

d3.formatSpecifier(".1f") 解析说明符字符串,返回一个字段对应格式微型语言各组成部分、并带 toString() 方法可重建说明符的对象。例如 formatSpecifier("s") 返回:

FormatSpecifier {
  "fill": " ",
  "align": ">",
  "sign": "-",
  "symbol": "",
  "zero": false,
  "width": undefined,
  "comma": false,
  "precision": undefined,
  "trim": false,
  "type": "s"
}

这个方法的两大用途:理解说明符是如何被解析的;以及派生新的说明符。例如用 precisionFixed 计算出合适精度后动态构建新格式:

const s = d3.formatSpecifier("f");
s.precision = d3.precisionFixed(0.01);
const f = d3.format(s);
f(42); // "42.00";

注意最后一步直接把 specifier 对象传给了 d3.format——toString() 重建说明符字符串的机制在此生效。

7.2 new d3.FormatSpecifier(specifier)

new d3.FormatSpecifier({type: "f", precision: 1})

接受一个说明符对象(而非字符串),返回同样带 toString() 的 FormatSpecifier。new FormatSpecifier({type: "s"}) 的返回结构与上面 formatSpecifier("s") 相同。这是从对象字段直接构造说明符的入口。

八、精度推断三件套:precisionFixed / precisionPrefix / precisionRound

这三个方法都用于“根据你的数据自动选择合适的 precision”,前提是待格式化的值本身是 step 的整数倍

8.1 precisionFixed(step)

给定定点记法的步长 step(即待格式化值之间的最小绝对差),返回建议的小数 precision:

d3.precisionFixed(0.01) // 2

对数字 1、1.5、2,step 应为 0.5,建议 precision 为 1:

const p = d3.precisionFixed(0.5);
const f = d3.format("." + p + "f");
f(1);   // "1.0"
f(1.5); // "1.5"
f(2);   // "2.0"

而对 1、2、3,step 为 1,建议 precision 为 0:

const p = d3.precisionFixed(1);
const f = d3.format("." + p + "f");
f(1); // "1"
f(2); // "2"
f(3); // "3"

注意:对 % 类型需要减 2:

const p = Math.max(0, d3.precisionFixed(0.05) - 2);
const f = d3.format("." + p + "%");
f(0.45); // "45%"
f(0.50); // "50%"
f(0.55); // "55%"

8.2 precisionPrefix(step, value)

d3.precisionPrefix(1e5, 1.3e6) // 1

给定 step 和参考 value(决定使用哪个 SI 前缀),返回配合 formatPrefix 的建议小数 precision。对数字 1.1e6、1.2e6、1.3e6,step 取 1e5,value 可取 1.3e6,建议 precision 为 1:

const p = d3.precisionPrefix(1e5, 1.3e6);
const f = d3.formatPrefix("." + p, 1.3e6);
f(1.1e6); // "1.1M"
f(1.2e6); // "1.2M"
f(1.3e6); // "1.3M"

8.3 precisionRound(step, max)

d3.precisionRound(0.01, 1.01) // 3

给定 stepmax(待格式化值的最大绝对值),返回按有效数字舍入类型(如 r)的建议 precision。对 0.99、1.0、1.01,step 为 0.01、max 为 1.01,建议 precision 为 3:

const p = d3.precisionRound(0.01, 1.01);
const f = d3.format("." + p + "r");
f(0.99); // "0.990"
f(1.0);  // "1.00"
f(1.01); // "1.01"

而对 0.9、1.0、1.1,step 为 0.1、max 为 1.1,建议 precision 为 2:

const p = d3.precisionRound(0.1, 1.1);
const f = d3.format("." + p + "r");
f(0.9); // "0.90"
f(1.0); // "1.0"
f(1.1); // "1.1"

注意:对 e 类型需要减 1:

const p = Math.max(0, d3.precisionRound(0.01, 1.01) - 1);
const f = d3.format("." + p + "e");
f(0.01); // "1.00e-2"
f(1.01); // "1.01e+0"

九、d3-format 在 d3 生态中的实际协作方式

d3-format 并不是孤立使用的,仓库文档中有两处体现它与坐标轴 / 比例尺模块的协作:

  1. 坐标轴的刻度格式化docs/d3-axis.md):axis.tickFormat(format) 可直接传入 d3-format 的格式化器:
axis.tickFormat(d3.format(",.0f"));

文档进一步指出更常见的做法是把格式说明符传给 axis.ticks(count, format),这样精度会基于刻度间距自动设置:

axis.ticks(10, ",f");

若不设置显式 tickFormat,轴会回落到比例尺的默认 tickFormat

  1. 比例尺的刻度格式docs/d3-scale/linear.md):*linear*.tickFormat(count, specifier) 内部正是结合 precisionFixed 类方法与 d3.formatSpecifier 派生说明符——precisionFixed / precisionRound 的文档也明确说明这些方法“被 d3-scale 用于刻度格式化”。
  2. 刻度值的舍入docs/d3-array/ticks.md):d3-array 的 tick 生成因 IEEE 754 可能产生浮点噪声,文档明确指向 d3-format 作为“对人类友好”的展示层解决方案。

由此可以推断出一条清晰的分层:d3-array 负责生成“数学上正确”的刻度值,d3-scale 负责生成“默认合理”的刻度格式器,d3-format 则提供底层的全部格式化原语,d3-axis 在渲染时消费这些格式器。

十、快速参考与适用前提

API 作用 关键参数
d3.format(specifier) 默认地区格式化器 说明符字符串
d3.formatPrefix(specifier, value) 一致 SI 前缀格式化器 说明符 + 参考值(precision 为小数位数)
d3.formatLocale(definition) 创建地区对象(format + formatPrefix) decimal/thousands/grouping/currency 等
d3.formatDefaultLocale(definition) 创建地区并重定义 d3.format 别名 同上
d3.formatSpecifier(specifier) 解析说明符为对象 字符串
new d3.FormatSpecifier(obj) 由对象构造说明符 {type, precision, ...}
d3.precisionFixed(step) 定点记法建议精度 值的最小绝对差
d3.precisionPrefix(step, value) formatPrefix 建议精度 步长 + 参考值
d3.precisionRound(step, max) 有效数字记法建议精度 步长 + 最大绝对值

适用前提与限制:

  • 当前仓库为 d3 7.9.0,d3-format 版本为 ^3.1.0(见 package.json);d3.format 对负值默认使用减号(minus sign)而非连字符,这是 v7 的行为;
  • 精度推断三件套均假设“待格式化的值本身是 step 的整数倍”,否则建议值可能不适用;
  • formatPrefixs 类型的 precision 语义不同(小数位数 vs 有效数字位数),混用 specifier 时容易出错,可用 d3.formatSpecifier 解码核对;
  • 自定义数字字形(如全角数字)通过 definition 的 numerals 数组替换 0–9 实现。

深入阅读建议从 docs/d3-format.md 原文、docs/api.md 的总索引,以及 docs/d3-axis.mddocs/components/ExampleChord.vue 中的两处真实格式化用法入手。

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