d3-format 数字格式化完全指南:格式说明符微型语言、Locale 与 SI 前缀在 d3 中的实战应用
本文基于 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+4、42k); - 货币应固定精度(
$3.50); - 统计结果应按有效数字舍入(
4021变为4000); - 格式应适配读者地区(
42.000,00或42,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.0 到 0.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.format、d3.formatPrefix、d3.formatSpecifier、d3.precisionFixed、d3.precisionPrefix、d3.precisionRound、d3.formatLocale、d3.formatDefaultLocale 这些方法。
需要说明的是:d3-format 的源码(如 src/locale.js、src/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
$— 按地区定义应用货币符号;#— 用于二进制、八进制、十六进制,分别加0b、0o、0x前缀。
4.4 zero、width 与逗号
0 选项启用零填充;它会隐式地把 fill 设为 0、align 设为 =。width 定义最小字段宽度,不指定时由内容决定宽度。, 选项启用分组分隔符(如千位逗号)。
4.5 precision 的语义(重要)
precision 的含义取决于 type:
- 对类型
f和%,precision 表示小数点后的位数; - 对类型
(none)、e、g、r、s、p,precision 表示有效数字的位数。
未指定 precision 时,除 none 类型默认为 12 外,其他类型均默认为 6。整数格式(b、o、d、x、X)和字符类型 c 会忽略 precision。
4.6 ~ 修剪选项
~ 选项在所有格式类型中修剪无意义的末尾零,最常与 r、e、s、% 配合使用:
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的简写; - 对
g、n和 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 类型的关键区别在于:
formatPrefix返回的是一致的 SI 前缀——所有数值共用同一前缀,而不是像s类型那样为每个数字动态计算前缀;- 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.format 和 locale.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.format 和 d3.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
给定 step 和 max(待格式化值的最大绝对值),返回按有效数字舍入类型(如 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 并不是孤立使用的,仓库文档中有两处体现它与坐标轴 / 比例尺模块的协作:
- 坐标轴的刻度格式化(docs/d3-axis.md):
axis.tickFormat(format)可直接传入 d3-format 的格式化器:
axis.tickFormat(d3.format(",.0f"));
文档进一步指出更常见的做法是把格式说明符传给 axis.ticks(count, format),这样精度会基于刻度间距自动设置:
axis.ticks(10, ",f");
若不设置显式 tickFormat,轴会回落到比例尺的默认 tickFormat。
- 比例尺的刻度格式(docs/d3-scale/linear.md):
*linear*.tickFormat(count, specifier)内部正是结合precisionFixed类方法与d3.formatSpecifier派生说明符——precisionFixed/precisionRound的文档也明确说明这些方法“被 d3-scale 用于刻度格式化”。 - 刻度值的舍入(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 的整数倍”,否则建议值可能不适用;
formatPrefix与s类型的 precision 语义不同(小数位数 vs 有效数字位数),混用 specifier 时容易出错,可用d3.formatSpecifier解码核对;- 自定义数字字形(如全角数字)通过 definition 的
numerals数组替换 0–9 实现。
深入阅读建议从 docs/d3-format.md 原文、docs/api.md 的总索引,以及 docs/d3-axis.md 和 docs/components/ExampleChord.vue 中的两处真实格式化用法入手。
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