d3 时间格式化完全指南:d3-time-format 的 strftime/strptime 指令、解析器与 Locale 定制
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
本篇基于 D3 官方文档 d3-time-format 模块说明 展开,系统讲解该模块提供的 strftime/strptime 近似实现:如何用d3.timeFormat/d3.timeParse/d3.utcFormat/d3.utcParse在日期对象与字符串之间双向转换,如何完整掌握 30 余种格式指令与填充修饰符,以及如何通过d3.timeFormatLocale/d3.timeFormatDefaultLocale定制国际化时间表示。读完后你既能写出多尺度自适应时间格式(D3 时间轴刻度标签的底层原理),也能为图表配置自定义语言环境下的日期解析。
模块定位:JavaScript 中的 strftime 与 strptime
d3-time-format 是 D3 生态中负责"时间 ↔ 字符串"转换的独立模块,它的目标是提供 C 标准库中久经考验的 strptime 和 strftime 函数的 JavaScript 近似实现,支持将 Date 对象格式化为各种本地化(locale-specific)的字符串表示,以及反向从字符串解析出日期。
在当前 D3 仓库(d3@7.9.0,见 package.json)中,d3-time-format以^4.1.0版本作为独立依赖声明,并由聚合入口 src/index.js 通过export * from "d3-time-format"统一导出,因此d3.timeFormat、d3.utcParse、d3.isoFormat等 API 均可直接在全局d3命名空间下使用。
使用模式分为两条基本链路:
- 格式化(format):从格式指令串创建格式化器,传入 Date 得到字符串;
- 解析(parse):从同一指令串创建解析器,传入字符串得到 Date 或
null。
// 格式化:Date -> 字符串 const formatTime = d3.utcFormat("%B %d, %Y"); formatTime(new Date()); // "May 31, 2023" // 解析:字符串 -> Date const parseTime = d3.utcParse("%B %d, %Y"); parseTime("June 30, 2015"); // 2023-05-31值得注意的是,模块内区分timeFormat(本地时区)与utcFormat(UTC 时区)两套对称 API。由于浏览器本地时区不可控,官方在 d3-scale 时间刻度文档中建议"尽可能优先使用 UTC 变体"——UTC 下每一天恒为 24 小时,且行为不依赖浏览器时区,结果更可预测。这与 d3-time 模块文档中"本模块仅工作在本地时区与 UTC 之下"的设计边界是一致的。
四大顶层 API:timeFormat、timeParse、utcFormat、utcParse
文档将四个顶层函数定义为对"默认 locale"上对应方法的别名(alias):
| API | 等价于 | 时区语义 |
|---|---|---|
d3.timeFormat(specifier) | locale.format | 本地时间 |
d3.timeParse(specifier) | locale.parse | 本地时间 |
d3.utcFormat(specifier) | locale.utcFormat | UTC 时间 |
d3.utcParse(specifier) | locale.utcParse | UTC 时间 |
d3.timeFormat("%b %d") // 本地时间格式化器 d3.timeParse("%b %d") // 本地时间解析器 d3.utcFormat("%b %d") // UTC 格式化器 d3.utcParse("%b %d") // UTC 解析器四个函数共享同一套格式指令(下文详述),差别仅在于各指令解释 Date 时使用的时区:本地时间版本调用date.getMonth()、date.getHours()等本地 getter;UTC 版本则调用getUTCMonth()、getUTCHours()等。因此对同一 Date 对象,%d、%H、%m等指令在本地版本与 UTC 版本下可能产出不同字符串(尤其是跨越时区边界时)。
*locale*.utcFormat与*locale*.utcParse的语义在文档中明确为:"与本地版本等价,只是所有指令按协调世界时(UTC)而非本地时间解释"。这意味着指令串本身在本地/UTC 两套 API 间完全通用,切换 API 即可切换时区语义,无需改动 specifier。
完整格式指令参考表(locale.format)
locale.format(specifier)返回一个新的格式化函数,specifier 是包含以%开头的"指令"(directive)的字符串。以下是文档给出的完整指令表,其中带*的指令会受 locale 定义影响:
| 指令 | 含义 |
|---|---|
%a* | 星期缩写(abbreviated weekday name) |
%A* | 星期全称(full weekday name) |
%b* | 月份缩写(abbreviated month name) |
%B* | 月份全称(full month name) |
%c* | 该 locale 的日期与时间组合格式,如%x, %X |
%d | 月份中的日,零填充十进制[01,31] |
%e | 月份中的日,空格填充[ 1,31];等价于%_d |
%f | 微秒,十进制[000000, 999999] |
%g | ISO 8601 周纪年(不带世纪),十进制[00,99] |
%G | ISO 8601 周纪年(带世纪),十进制 |
%H | 小时(24 小时制),十进制[00,23] |
%I | 小时(12 小时制),十进制[01,12] |
%j | 年中的第几天,十进制[001,366] |
%m | 月份,十进制[01,12] |
%M | 分钟,十进制[00,59] |
%L | 毫秒,十进制[000, 999] |
%p* | AM 或 PM |
%q | 年第几季度,十进制[1,4] |
%Q | 自 UNIX 纪元起经过的毫秒数 |
%s | 自 UNIX 纪元起经过的秒数 |
%S | 秒,十进制[00,61](含闰秒上限) |
%u | 以周一为首(ISO 8601)的星期,十进制[1,7] |
%U | 以周日为首的周序号,十进制[00,53] |
%V | ISO 8601 周序号,十进制[01, 53] |
%w | 以周日为首的星期,十进制[0,6] |
%W | 以周一为首的周序号,十进制[00,53] |
%x* | 该 locale 的日期格式,如%-m/%-d/%Y |
%X* | 该 locale 的时间格式,如%-I:%M:%S %p |
%y | 年份(不带世纪),十进制[00,99] |
%Y | 年份(带世纪),如1999 |
%Z | 时区偏移,如-0700、-07:00、-07或Z |
%% | 字面量百分号% |
周序号的三种口径:%U / %W / %V
文档对周序号的边界规则做了专门说明:
- %U:以周日为周首。新年中第一个周日之前的所有天都算第 0 周;
- %W:以周一为周首。新年中第一个周一之前的所有天都算第 0 周;
- 两者的周号计算均基于 d3-time 的interval.count。举例:2015-52 与 2016-00 同时指向 2015 年 12 月 28 日(周一),而 2015-53 与 2016-01 指向 2016 年 1 月 4 日(周一);
- %V / %g / %G则遵循 strftime man page 的 ISO 周定义:
在该系统中,周从周一开始,编号从第一周的 01 直到最后一周的 52 或 53。第 1 周是新年中首个满足"至少 4 天落在新年内"的周(同义表述:第 1 周是包含星期四的第一个周;或等价于包含 1 月 4 日的那一周)。若 ISO 周号归属于上一年或下一年,则使用那一年作为年份。
也就是说%V的周号与%g/%G的"周纪年"是配套的:%V给出周号时,%G/%g给出该 ISO 周所属的年份(可能与%Y不同)。仓库的 CHANGES.md 记录了这一特性的引入历史:%G与%g(ISO 8601 "week year")是在 5.0 版本中为 d3-time-format 新增的指令。
填充修饰符:0、_、-
%指示符后可以紧跟一个填充修饰符:
| 修饰符 | 含义 |
|---|---|
0 | 零填充(zero-padding) |
_ | 空格填充(space-padding) |
- | 禁用填充(disable padding) |
若不指定修饰符,默认是:所有指令默认为0(零填充),唯一例外是%e默认_(空格填充)。文档同时指出:部分 strftime/strptime 实现支持字段宽度或精度参数,但 d3-time-format 尚未实现该特性。
locale.format的返回值是"格式化函数",接收一个 Date 并返回对应字符串:
const formatMonth = d3.timeFormat("%B"), formatDay = d3.timeFormat("%A"), date = new Date(2014, 4, 1); // Thu May 01 2014 00:00:00 GMT-0700 (PDT) formatMonth(date); // "May" formatDay(date); // "Thursday"locale.parse:严格解析与 null 语义
locale.parse(specifier)返回一个解析器,接受字符串,返回对应的 Date;若字符串无法按该 specifier 精确匹配,则返回null。几个关键规则:
- 解析时可使用与
locale.format相同的指令集; %d与%e在解析时被视为等价(一个接受零填充、一个接受空格填充,解析端不做区分);- 解析是严格的:字符串必须与 specifier 精确匹配。以
%Y-%m-%dT%H:%M:%SZ为例:"2011-07-01T19:15:28Z"→ 正常解析(注意这里的Z是指令串中的字面字符,与%Z指令不同);"2011-07-01T19:15:28"、"2011-07-01 19:15:28"、"2011-07-01"→ 均返回null。
如果需要更宽松的解析策略,官方建议的顺序是:依次尝试多个格式,直到某个返回非 null 值(try multiple formats sequentially)。这是典型的"多格式回退"解析模式,适用于来源格式不统一的 CSV/JSON 数据。
ISO 8601 快捷通道:isoFormat 与 isoParse
除了通用指令系统,模块还提供了一对完整的 ISO 8601 UTC 快捷方法:
d3.isoFormat(new Date()); // "2023-05-31T18:17:36.788Z" d3.isoParse("2023-05-31T18:17:36.788Z"); // Date 对象d3.isoFormat是完整 ISO 8601 UTC 格式化器,可用时会走Date.toISOString快路径;d3.isoParse是完整 ISO 8601 UTC 解析器,可用时会直接交给 Date 构造函数。
需要特别注意的是d3.isoParse不保证严格的 ISO 8601 校验——它可能接受一些不符合规范的输入。文档明确指出:如果你依赖严格校验,应自行构造 UTC 解析器:
const strictIsoParse = d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ");实战:多尺度自适应时间格式(multi-scale time format)
文档给出的进阶用法是条件时间格式:根据日期落在哪个时间粒度边界上,选择不同的指令串。这是 D3 时间刻度默认刻度标签生成逻辑的同款模式(时间刻度文档 中*time*.tickFormat的默认行为正是按 %Y / %B / %b %d / %a %d / %I %p / %I:%M / :%S / .%L 逐级选择)。
实现上,多尺度格式组合了 d3-time 的时间区间(interval)API(d3-time 文档 中的 d3.utcSecond、d3.utcMinute 等):interval(date) < date表示"该区间边界早于 date",即 date 携带了比该粒度更细的时间信息。
const formatMillisecond = d3.utcFormat(".%L"), formatSecond = d3.utcFormat(":%S"), formatMinute = d3.utcFormat("%I:%M"), formatHour = d3.utcFormat("%I %p"), formatDay = d3.utcFormat("%a %d"), formatWeek = d3.utcFormat("%b %d"), formatMonth = d3.utcFormat("%B"), formatYear = d3.utcFormat("%Y"); function multiFormat(date) { return (d3.utcSecond(date) < date ? formatMillisecond : d3.utcMinute(date) < date ? formatSecond : d3.utcHour(date) < date ? formatMinute : d3.utcDay(date) < date ? formatHour : d3.utcMonth(date) < date ? (d3.utcWeek(date) < date ? formatDay : formatWeek) : d3.utcYear(date) < date ? formatMonth : formatYear)(date); }这段代码的判定链自下而上逐级检查:
d3.utcSecond(date) < date→ 秒内有毫秒成分 → 用.%L(毫秒);d3.utcMinute(date) < date→ 分钟内有秒成分 → 用:%S;d3.utcHour(date) < date→ 小时内有分钟成分 → 用%I:%M;d3.utcDay(date) < date→ 日内有时分成分 → 用%I %p;d3.utcMonth(date) < date且d3.utcWeek(date) >= date→ 落在周边界上 → 用%b %d(周粒度);否则落在日边界上 → 用%a %d;d3.utcYear(date) < date→ 落在月边界 → 用%B;- 其余 → 落在年边界 → 用
%Y。
这种"局部 + 全局上下文兼得"的表示方式正是 D3 时间轴的招牌风格:连续刻度被格式化为[11 PM, Mon 07, 01 AM]时,读者能同时看到小时、星期和日期信息,而不是三个孤立的小时[11 PM, 12 AM, 01 AM]。d3-scale 时间刻度文档明确推荐:若想自建这种条件格式,请参考 d3-time-format——即本文主题模块。
在 D3 生态中的角色:时间刻度的刻度标签来源
d3-time-format 并不是孤立模块,它在 D3 渲染管线中承担"刻度标签文本生成"的职责:
- 时间刻度(
d3.scaleTime/d3.scaleUtc)生成ticks后,由*time*.tickFormat产出一个"合适的格式化器"用于轴标签。当显式传入 specifier 时,tickFormat等价于本文的locale.format;不传时则返回上述默认多尺度格式(时间刻度文档); - 坐标轴(
d3.axisBottom等)在tickFormat缺省时会调用*scale*.tickFormat,文档明确引导:"如需创建格式化器,参见 d3-format 与 d3-time-format"(见 d3-axis 文档)。
从源码组织看,D3 主仓库(本仓库)本身不直接包含 d3-time-format 的实现文件,它通过 package.json 中的依赖d3-time-format: ^4.1.0引入,并由 src/index.js 统一 re-export;文档则由 VitePress 站点维护(docs:dev/docs:build脚本,见 package.json),且 test/docs-test.js 中的测试会爬取docs/下所有 Markdown,校验文档内部链接指向存在的锚点——这解释了本文档中大量{#anchor}显式锚点的存在意义,也是文档内锚点链接(如本文引用的各小节)能够稳定跳转的原因。
Locale 定制:timeFormatLocale 与 timeFormatDefaultLocale
带*的指令(%a、%A、%b、%B、%c、%p、%x、%X)依赖 locale 定义。d3.timeFormatLocale(definition)接收一个定义对象,返回一个 locale 对象,该对象暴露format、parse、utcFormat、utcParse四个方法。definition必须包含以下 8 个属性:
| 属性 | 含义 |
|---|---|
dateTime | 日期与时间(%c)的格式 specifier,如"%a %b %e %X %Y" |
date | 日期(%x)的格式 specifier,如"%m/%d/%Y" |
time | 时间(%X)的格式 specifier,如"%H:%M:%S" |
periods | 上午/下午标记,如["AM", "PM"] |
days | 星期全称数组,从周日开始 |
shortDays | 星期缩写数组,从周日开始 |
months | 月份全称数组,从 January 开始 |
shortMonths | 月份缩写数组,从 January 开始 |
完整示例(美式英语 locale):
const enUs = d3.timeFormatLocale({ dateTime: "%x, %X", date: "%-m/%-d/%Y", time: "%-I:%M:%S %p", periods: ["AM", "PM"], days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] });之后调用enUs.format("%a %b %e")、enUs.utcParse("%B %d, %Y")等,即可得到基于该语言环境的格式化器/解析器。关键区别:timeFormatLocale只返回一个独立的 locale 对象,不影响全局;而d3.timeFormatDefaultLocale(definition)与之等价,但会额外重定义全局的d3.timeFormat、d3.timeParse、d3.utcFormat、d3.utcParse为新 locale 的对应方法:
const enUs = d3.timeFormatDefaultLocale({ dateTime: "%x, %X", date: "%-m/%-d/%Y", time: "%-I:%M:%S %p", periods: ["AM", "PM"], days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] });若不显式设置默认 locale,d3.timeFormat等顶层 API 默认使用美式英语locale(文档注明其来源为 d3-time-format 仓库中的locale/en-US.json)。因此对于非英文界面,推荐做法是:数据解析端用timeFormatLocale定义目标 locale 的parse方法(避免污染全局),仅当整站统一语言时再用timeFormatDefaultLocale。
速查与常见陷阱小结
| 场景 | 推荐用法 |
|---|---|
| 展示本地时区日期 | d3.timeFormat("%Y-%m-%d") |
| 展示/解析 UTC 日期 | d3.utcFormat/d3.utcParse(行为更可预测,不依赖浏览器时区) |
| 序列化/交换 ISO 8601 | d3.isoFormat/d3.isoParse |
| 严格 ISO 校验解析 | d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ") |
| 解析格式不统一的来源数据 | 依次尝试多个parse,取首个非null结果 |
| 时间轴刻度标签 | 参考多尺度格式,或直接使用scaleUtc().tickFormat()默认行为 |
| 多语言界面 | d3.timeFormatLocale定义独立 locale;全站统一时再用d3.timeFormatDefaultLocale |
常见陷阱与文档明示的规则对照:
%d/%e解析等价:格式化端二者输出不同(零填充 vs 空格填充),但解析端视为同一指令;- 严格解析:
parse要求字符串与 specifier 精确匹配,差一个字符即返回null,不存在"尽力而为"模式; - 字面量
Z≠%Z:%Y-%m-%dT%H:%M:%SZ末尾的Z是指令串中的普通字符; - 周号三口径不可混用:
%U(周日起始,零周规则)、%W(周一起始,零周规则)、%V(ISO 周,配%G/%g周纪年)三者对同一日期可能给出不同结果; - 字段宽度/精度参数未实现:部分 strftime 实现支持
%05d之类的宽度语法,d3-time-format 目前不支持。
参考文档索引
- 模块完整 API 文档:docs/d3-time-format.md
- 时间区间 API(多尺度格式依赖):docs/d3-time.md
- 时间刻度与默认刻度格式:docs/d3-scale/time.md
- 坐标轴刻度格式化入口:docs/d3-axis.md
- 聚合导出入口:src/index.js
- 依赖版本与文档站点脚本:package.json
- 版本变更历史(
%G/%g新增等):CHANGES.md - 文档链接完整性测试:test/docs-test.js
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考