D3 时间尺度详解:scaleTime 与 scaleUtc 的构造、刻度生成与域名规整
时间尺度(Time Scales)是 D3 将时间维度映射到视觉编码的核心组件。本文以官方文档 时间尺度 为主线,系统讲解 scaleTime / scaleUtc 的构造与默认值、ticks 的日历间隔自动选择机制、tickFormat 的多尺度格式化策略,以及 nice 的域名规整方法,并结合本仓库 d3 包(v7.9.0,依赖 d3-scale@^4.0.2、d3-time@^3.1.0、d3-time-format@^4.1.0)与示例组件 ExampleBlankChart.vue 补充实战上下文。读完本文,你将能够独立搭建一条带合理刻度和标签的时间轴,并理解 UTC 与本地时间两种尺度在行为上的差异。
一、什么是时间尺度:面向时间域的线性尺度变体
时间尺度是线性尺度的一个变体,区别在于它拥有时间域(temporal domain):
- 域名中的值会被强制转换为 ECMAScript
Date对象,而不是数字; invert的返回值同样是日期对象;- 刻度(ticks)基于 d3-time 的日历间隔生成,省去了为时间域手动生成轴刻度的繁琐工作。
从 线性尺度文档 可以看出,时间尺度继承其完整的连续尺度契约:domain、range、invert、clamp、interpolate 等方法的行为语义与线性尺度一致,只是输入输出换成了时间语义。在 d3 主包中,入口文件 依次再导出 d3-scale、d3-time 与 d3-time-format 三个模块,这正是时间尺度“连续变换 + 日历间隔 + 日期格式化”三层能力的组合来源。
二、d3.scaleTime(domain, range):本地时间尺度
d3.scaleTime 使用指定的 domain 和 range 构造一个新的时间尺度,默认采用默认插值器,且钳制(clamping)处于关闭状态。
const x = d3.scaleTime([new Date(2000, 0, 1), new Date(2000, 0, 2)], [0, 960]);
x(new Date(2000, 0, 1, 5)); // 200
x(new Date(2000, 0, 1, 16)); // 640
x.invert(200); // Sat Jan 01 2000 05:00:00 GMT-0800 (PST)
x.invert(640); // Sat Jan 01 2000 16:00:00 GMT-0800 (PST)
上例中域跨度为 24 小时、值域跨度为 960 像素,因此每 5 小时对应 200 像素。注意 invert 的返回值受运行环境的时区影响(示例输出为 PST),这正是本地时间尺度的特性所在。
默认值约定:
- 若未指定 domain,默认为本地时间的 [2000-01-01, 2000-01-02];
- 若未指定 range,默认为 [0, 1]。
三、d3.scaleUtc(domain, range):可预测性更强的 UTC 尺度
d3.scaleUtc 与 scaleTime 等价,但返回的时间尺度运行在 Coordinated Universal Time(协调世界时) 下,而非本地时间:
const x = d3.scaleUtc([new Date("2000-01-01"), new Date("2000-01-02")], [0, 960]);
x(new Date("2000-01-01T05:00Z")); // 200
x(new Date("2000-01-01T16:00Z")); // 640
x.invert(200); // 2000-01-01T05:00Z
x.invert(640); // 2000-01-01T16:00Z
同样默认 domain 为 [2000-01-01, 2000-01-02](UTC),range 为 [0, 1]。
官方建议:尽可能优先使用 UTC 尺度,因为它行为更可预测——每天始终是 24 小时,且结果不依赖于浏览器所在时区。
这一建议在 d3-time 文档 中有充分的原理支撑:本地时间下,受夏令时影响,一天可能是 23 到 25 小时,一周可能是 167 到 169 小时;直接做毫秒减法计算天数甚至会得到 30.958333333333332 这样的结果。而 d3-time 明确说明它只支持本地时区和 UTC 两种时间基准,UTC 路径下每个间隔长度恒定,因此 scaleUtc 的 ticks、nice 输出在任意机器上都一致,便于复现和调试。
四、time.ticks(count):基于日历间隔的自动刻度
*time*.ticks(*count*) 返回尺度域中具有代表性的日期数组:
const x = d3.scaleTime();
x.ticks(10);
// [Sat Jan 01 2000 00:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 03:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 06:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 09:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 12:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 15:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 18:00:00 GMT-0800 (PST),
// Sat Jan 01 2000 21:00:00 GMT-0800 (PST),
// Sun Jan 02 2000 00:00:00 GMT-0800 (PST)]
关键特性:
- 返回的刻度值(大体)等间距、取值合理(例如每天的午夜时刻);
- 刻度保证落在域的取值范围(extent)之内;
- count 未指定时默认为 10,且只是一个提示值——根据域的不同,尺度可能返回更多或更少的刻度。
自动刻度会从以下日历间隔中选取:
| 粒度 | 候选间隔 |
|---|---|
| 秒 | 1 秒、5 秒、15 秒、30 秒 |
| 分 | 1 分、5 分、15 分、30 分 |
| 时 | 1 时、3 时、6 时、12 时 |
| 日 | 1 天、2 天 |
| 周 | 1 周 |
| 月 | 1 月、3 月 |
| 年 | 1 年 |
这套间隔清单与 d3.utcTicks 的文档完全一致:产生与 count 最接近个数的间隔即被采用,此外还会按 d3.ticks 的规则考虑毫秒级(小范围)与多年级(大范围)的倍数。这也解释了上面示例为何在 24 小时域上选择了 3 小时间隔。
若希望显式控制间隔,可以传入一个时间间隔代替 count;配合 interval.every 可以抽取子集。例如以 15 分钟为间隔生成刻度:
const x = d3.scaleUtc().domain([new Date("2000-01-01T00:00Z"), new Date("2000-01-01T02:00Z")]);
x.ticks(d3.utcMinute.every(15));
// [2000-01-01T00:00Z,
// 2000-01-01T00:15Z,
// 2000-01-01T00:30Z,
// 2000-01-01T00:45Z,
// 2000-01-01T01:00Z,
// 2000-01-01T01:15Z,
// 2000-01-01T01:30Z,
// 2000-01-01T01:45Z,
// 2000-01-01T02:00Z]
注意:在某些情况下(例如按天取间隔时),指定 step 可能导致刻度间距不规则,因为时间间隔本身的长度是变化的(d3.timeDay 的父间隔是 d3.timeMonth,interval 编号会在每月重置,这一点在 interval.every 的文档 中有明确说明)。
五、time.tickFormat(count, specifier):多尺度时间格式化
*time*.tickFormat(*count*, *specifier*) 返回一个适合显示刻度值的时间格式化函数:
const x = d3.scaleUtc().domain([new Date("2000-01-01T00:00Z"), new Date("2000-01-01T02:00Z")]);
const T = x.ticks(); // [2000-01-01T00:00Z, 2000-01-01T00:15Z, 2000-01-01T00:30Z, …]
const f = x.tickFormat();
T.map(f); // ["2000", "12:15", "12:30", "12:45", "01 AM", "01:15", "01:30", "01:45", "02 AM"]
参数行为:
- count 目前会被忽略,仅为与 linear.tickFormat 等其他尺度保持接口一致而接受;
- 若指定了格式 specifier,则等价于 d3-time-format 的 format(如
d3.utcFormat("%b %d")); - 若未指定 specifier,返回默认的多尺度时间格式,它根据具体日期自动选择人类可读的表示:
| 场景 | 默认格式 | 示例输出 |
|---|---|---|
| 年边界 | %Y |
2011 |
| 月边界 | %B |
February |
| 周边界 | %b %d |
Feb 06 |
| 日边界 | %a %d |
Mon 07 |
| 时边界 | %I %p |
01 AM |
| 分边界 | %I:%M |
01:23 |
| 秒边界 | :%S |
:45 |
| 其他时刻(毫秒) | .%L |
.012 |
这种默认策略虽然看起来不寻常,却有一个明显好处:同时提供局部与全局上下文。例如把一组刻度格式化为 [11 PM, Mon 07, 01 AM],能同时传达小时、日期与星期信息;而如果只输出小时 [11 PM, 12 AM, 01 AM],读者就无法判断日期是否发生了跨越。d3-time-format 文档 中给出的 multiFormat 示例函数正是这一默认策略的手工等价实现——它利用 d3.utcSecond(date) < date 这类“向下取整后与原值比较”的判定逐级选择格式,想自定义条件化时间格式时可参照该实现。
六、time.nice(count):把域名规整到“整点”边界
*time*.nice(*count*) 将域名向外扩展,使其起止于整齐的时间值:
const x = d3.scaleUtc().domain([new Date("2000-01-01T12:34Z"), new Date("2000-01-01T12:59Z")]).nice();
x.domain(); // [2000-01-01T12:30Z, 2000-01-01T13:00Z]
行为要点(与 linear.nice 的语义对齐):
- 该方法通常会修改尺度的域名,且一般只向外扩展到最近的整值;
- 可选的刻度 count 参数可控制用于扩展边界的步长,保证返回的ticks恰好覆盖整个域名;也可以直接指定一个时间间隔来显式设定刻度,若指定了 interval,还可以再给一个 step 来跳过部分刻度。例如
time.nice(d3.utcSecond.every(10))会把域名扩展到整十秒(0、10、20 秒……); - 当域名由数据计算得出(例如用 extent)而边界不规则时,nicing 尤其有用。例如域名
[2009-07-13T00:02, 2009-07-13T23:48]规整后为[2009-07-13, 2009-07-14]; - 如果域名超过两个值,nicing 只影响第一个和最后一个值。
需要记住的限制:nice 只修改当前的域名,之后用 domain 重新设置域名不会自动重新规整,需要再次调用 nice。
七、实战:用 scaleUtc 搭建完整的时间轴骨架
仓库文档站中的示例组件 ExampleBlankChart.vue 展示了时间尺度与 d3-axis 的标准组合,也是官方推荐的坐标系搭建骨架:
// 声明 x(水平位置)尺度:UTC 时间尺度
const x = d3.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// 声明 y(垂直位置)尺度:线性尺度
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// 创建 SVG 容器
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// 添加 x 轴:axisBottom 会自动调用 x.ticks() 与 x.tickFormat()
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
从这个示例可以归纳出时间尺度的典型工作流:
- 用
scaleUtc(或scaleTime)声明域和值域,domain直接接收Date对象; - 交给
d3.axisBottom(x)/d3.axisLeft(y)等轴生成器,轴的刻度位置来自*time*.ticks,刻度标签来自*time*.tickFormat——前文介绍的自动间隔选择与多尺度格式化在此“零配置”生效; - 若数据域边界不规则,先调用
x.nice()规整,再绑定轴; - 需要交互反查(例如根据鼠标位置还原时间)时使用
x.invert,返回值是Date对象。
八、常见陷阱与最佳实践
- 优先 UTC:除非业务必须展示本地时区(如用户所在城市的本地营业时段),否则用
scaleUtc规避夏令时导致的 23/25 小时日、刻度不均与跨环境结果不一致问题; new Date构造函数重载陷阱:new Date(2000, 0, 1)是本地时间,而new Date("2000-01-01")按 ISO 字符串解析为 UTC 零点。混用两种写法是时间尺度 bug 的高发来源,建议全项目统一一种约定;- count 只是提示:
ticks(10)不保证恰好返回 10 个刻度,tickFormat(count)中的 count 当前也被忽略,不要据此断言刻度数量; - step 间隔的不规则性:对
timeDay这类父间隔为月的日历间隔,every(step)的锚点会随月重置,跨月拼接多个 range 时可能出现间距不一致,官方建议用 interval.every 而非 range 的 step 参数来保证区间一致性; - nicing 是手动操作:每次重新
domain之后都需要重新nice; - 格式化扩展:默认多尺度格式覆盖了常见需求,需要固定格式(如纯
%H:%M)时用 specifier 直接落到 d3-time-format;需要复杂条件化格式时参照其multiFormat手工实现。
九、小结
| API | 作用 | 关键参数语义 |
|---|---|---|
d3.scaleTime(domain, range) |
构造本地时间尺度 | 默认域 [2000-01-01, 2000-01-02] 本地时间,默认值域 [0, 1] |
d3.scaleUtc(domain, range) |
构造 UTC 时间尺度 | 默认域 [2000-01-01, 2000-01-02] UTC,默认值域 [0, 1] |
*time*.ticks(count) |
生成日历间隔刻度 | count 默认 10,仅是提示;可传时间间隔 + interval.every(step) |
*time*.tickFormat(count, specifier) |
返回刻度格式化函数 | count 当前被忽略;specifier 缺省时启用多尺度默认格式 |
*time*.nice(count) |
规整域名边界 | 可传 count 或时间间隔(可再配 step);只改首尾值 |
时间尺度的价值在于把“时间”这件不规则的事(变长的月、跳变的夏令时、跨时区的读者)封装进与线性尺度一致的连续映射契约中,并内置了与日历对齐的刻度与格式系统。结合本文的 API 语义、d3-time 的间隔机制、d3-time-format 的格式说明 以及 d3-scale 总览 与 API 索引,你可以在此基础上构建出任意粒度的时间轴、时间刷选与时间交互功能。
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