当前内容所在位置(可进入专栏查看其他译好的章节内容)
- 第一部分 D3.js 基础知识
- 第一章 D3.js 简介(已完结)
- 1.1 何为 D3.js?
- 1.2 D3 生态系统——入门须知
- 1.3 数据可视化最佳实践(上)
- 1.3 数据可视化最佳实践(下)
- 1.4 本章小结
- 第二章 DOM 的操作方法(已完结)
- 2.1 第一个 D3 可视化图表
- 2.2 环境准备
- 2.3 用 D3 选中页面元素
- 2.4 向选择集添加元素
- 2.5 用 D3 设置与修改元素属性
- 2.6 用 D3 设置与修改元素样式
- 2.7 本章小结
- 第三章 数据的处理(已完结)
- 3.1 理解数据
- 3.2 准备数据
- 3.3 将数据绑定到 DOM 元素
- 3.3.1 利用数据给 DOM 属性动态赋值
- 3.4 让数据适应屏幕
- 3.4.1 比例尺简介(上篇)
- 3.4.2 线性比例尺(中篇)
- 3.4.2.1 基于 Mocha 测试 D3 线性比例尺(DIY 实战)
- 3.4.3 分段比例尺(下篇)
- 3.4.3.1 使用 Observable 在线绘制 D3 条形图(DIY 实战)
- 3.5 加注图表标签(上篇)
- 3.5.1 人物专访:Krisztina Szűcs(下篇)
- 3.6 本章小结
- 第四章 直线、曲线与弧线的绘制 ✔️
- 4.1 坐标轴的创建(上篇)
- 4.1.0 DIY 实战:如何通过学习 d3.autoType 函数深度参与 D3 生态建设 ✔️
- 4.1.1 D3 中的边距约定(精译中 ⏳)
- 4.1.2 坐标轴的生成
- 4.2 D3 折线图的绘制
文章目录
- DIY 实战:如何通过学习 d3.autoType 函数深度参与 D3 生态建设
- 1 起因
- 2 经过
- 2.1 日期转换中的坑
- 2.2 关于“中规中矩”
- 2.3 直奔源码
- 2.4 issue 溯源
- 2.5 踏上单元测试的漫漫征途
- 2.6 提交 PR
- 3 结尾
DIY 实战:如何通过学习 d3.autoType 函数深度参与 D3 生态建设
1 起因
上一篇(即本专栏第 032 篇)谈到过一个快速转换数据类型的工具函数 d3.autoType
,当时作者推荐了一篇发表在 Observable
的文章,还说它是一篇写得很棒的文章(a great article)。今天就来研究研究这篇文章怎么个好法,顺便梳理一下身为码畜的本人是怎么通过这篇文章深度参与 D3.js 生态建设的(算是二度抛砖引玉吧)。
2 经过
文章链接在这里:https://observablehq.com/@d3/d3-autotype。打开一看,原来是 D3.js
的创始人 Mike Bostock 的大作!文章写于 2019 年 2 月 8 日,最后一次更新是在 2022 年 5 月 31 日(莫非是大佬送给自己的六一儿童节礼物?)。咳咳,说正题。
原来,d3.autoType
是 d3-dsv 模块下的一个工具函数,支持简单的类型推断(automatic type inference),前面先介绍它的诞生背景——让开发者从简单而繁琐的手动类型转换中解放出来,能转成数字、日期以及布尔值的就直接转了。
但问题也接踵而至:万一我要的是日期,自动转换却给我一个数字咋整?
还能咋整,特殊情况特殊处理呗,转完再手动检查一遍(这个环节不妨就叫 真·人工智能)。
2.1 日期转换中的坑
以上都是与《D3.js in Action》书中重复的内容。接下来,Mike 大佬提到了原生 JavaScript
一个奇葩的点:在解析成日期的时候,只有年月日的字符型日期(比如 "2024-10-11"
)会以子午本初线上的 0 时差为准;而带有时分秒这类后缀的(如 "2024-10-11T00:00"
),则会默认按当地时间来转换,即结果带时差。大佬都说没办法了,这是 ECMAScript
规范写好的,强大如 Observable
也只能照办。所以就会出现这样奇葩结果:
【图1 d3.autoType 在处理日期和时间戳时由于 ECMAScript 规范导致的转换不一致问题】
其实也不难理解,一般转换个时间戳,谁会希望拿到一个和所在地区隔了 16 小时时差的结果呢?只是日期按子午线的时差来算,确实有点强人所难了。
这个坑算是原生 JavaScript 挖的,今后注意便是了。
2.2 关于“中规中矩”
文章还说,d3.autoType
还支持美国各州县的五位邮政编码转换。六位就不行?赶紧测一下:
【图 2 d3.autoType 转换字符型编码没有预想中的五位数限制】
那么,不是那么中规中矩的呢?它会直接跳过——
【图 3 对于不那么中规中矩的数字字符串,d3.autoType 直接跳过】
不仅数字如此,日期和布尔值也一样——
【图 4 对于不那么中规中矩的日期和布尔值,d3.autoType 也直接跳过】
最后,Mike 总结了一下 d3.autoType
能转的类型:
- If empty, then
null
.(为空的,就转为null
) - If exactly “true”, then
true
.(严格写作"true"
的,才转为true
) - If exactly “false”, then
false
.(严格写作"false"
的,才转为false
) - If exactly “NaN”, then
NaN
.(严格写作"NaN"
的,才转为NaN
) - Otherwise, if coercible to a number, then a number.(以上都不行,但能强转为数字的,才转为数字)
- Otherwise, if a date-only or date-time string, then a Date.(以上再不行,但只有日期或者带时间的日期字符串,才转为
Date
型) - Otherwise, a string (the original untrimmed value).(以上都转不了的,还转个啥,摆烂吧)
2.3 直奔源码
既然来都来了,怎么能止步于上面列出的这几条八股文呢?果断扒出 d3.autoType
的源码(根据开头提到的 d3-dsv
模块按图索骥):
// Path: d3-dsv/src/autoType.js
export default function autoType(object) {
for (var key in object) {
var value = object[key].trim(), number, m;
if (!value) value = null;
else if (value === "true") value = true;
else if (value === "false") value = false;
else if (value === "NaN") value = NaN;
else if (!isNaN(number = +value)) value = number;
else if (m = value.match(/^([-+]\d{2})?\d{4}(-\d{2}(-\d{2})?)?(T\d{2}:\d{2}(:\d{2}(\.\d{3})?)?(Z|[-+]\d{2}:\d{2})?)?$/)) {
if (fixtz && !!m[4] && !m[7]) value = value.replace(/-/g, "/").replace(/T/, " ");
value = new Date(value);
}
else continue;
object[key] = value;
}
return object;
}
// https://github.com/d3/d3-dsv/issues/45
const fixtz = new Date("2019-01-01T00:00").getHours() || new Date("2019-07-01T00:00").getHours();
真相大白了:原来转为数字的时候,用的是 number = +value
(第 9 行),即便前面写再多的 0
也是徒劳。
最长的那行正则表达式,就是处理日期的,确实麻烦了点。扔给 AI 吧,我才懒得去匹配每一个捕获组呢:
【图 5 AI 对源码正则表达式的解读结果】
解释得还不赖。既然它这么懂源码,今后类似的活就交给它吧(有没有一种似曾相识的赶脚?)。
最后剩下那个 fixtz
常量,为啥要把 2019 年 1 月 1 号和 7 月 1 号写到一起呢?去看看上面的 issue
提案就知道了:https://github.com/d3/d3-dsv/issues/45。
这一看,便如同打开了潘多拉的魔盒……
2.4 issue 溯源
原来,这个 45 号提案已经关闭了,最后是由 53 号提案解决的。该 45 号提案还是 Mike 本人提出的:
【图 6 无意间看到的作者个人简介,迷之搞笑~】
他说 Safari
浏览器误将带时间的字符串默认按 UTC 子午本初线上的时差来考虑了,并且备注说这是浏览器自己的漏洞,改起来也简单(难道 Safari
是想以一己之力纠正 ECMAScript
这个历史遗留问题?)。之后有个叫 Fil 的开发者提了第 51 号提案,验证了这个说法并尝试进行修复。Fil 认为,通过分别检验冬时令和夏时令 某个日期的 00:00
零点时刻 是否真的为零,就可以复现 Safari
这个漏洞,但他不知道怎么在 node
环境下写测试用例,只在 Observable 上调通了 一个版本(可惜失效了,看不到最初的改动)。Mike 顺着这个思路对原函数进行了几处优化,经过多次沟通,最终给出了现在看到的版本。
2.5 踏上单元测试的漫漫征途
本以为事情圆满结束了,结果顺着 51 号提案的跟帖,又看到了该问题单元测试的一个修复过程。大致意思是,源码的 Bug 修复了,但是单元测试却失败了:
# (运行命令:yarn test)
# csvFormat(array) converts dates to ISO 8601
ok 131 should be equivalent
not ok 132 should be equivalent
---
operator: deepEqual
expected: |-
'date\n2018-01-01T08:00Z'
actual: |-
'date\n2017-12-31T23:00Z'
at: Test.<anonymous> (/Users/fil/Sites/d3/d3-dsv/test/csv-test.js:263:8)
stack: |-
Error: should be equivalent
...
根据回复中摘录的断言信息,我很快锁定了报错的测试用例(第 3 行报错):
it("csvFormat(array) converts dates to ISO 8601", () => {
assert.deepStrictEqual(csvFormat([{date: new Date(Date.UTC(2018, 0, 1))}]), "date\n2018-01-01");
assert.deepStrictEqual(csvFormat([{date: new Date(2018, 0, 1)}]), "date\n2018-01-01T08:00Z");
});
大佬就是大佬,Mike 一下子就看出了问题:命令行的默认时区可能和测试用例的不一致。他建议在测试脚本中硬编码一个时区,就像测试 d3-time
模块时那样:
【图 7 Mike Bostock 给出的单元测试修复意见】
要不怎么说姜还是老的辣呢,原来类似的问题早就处理过了,不用重复造轮子。在大神上帝视角般的关照下,幸运的 Fil 终于通过了测试,回复标题上都难掩激动之情:
【图 8 Fil 通过测试后的回复标题(地球上任何地方都能跑通测试了)】
有这么夸张吗?点开一看,原来是在 package.json
的 test
脚本的开头加了一个 TZ=America/Los_Angeles
,即 Mike 大神说的硬编码。
加了这玩意儿就真的能在我笔记本里运行?开什么玩笑?PowerShell
知道 TZ
是啥吗?带着一连串的问题,我把 d3-dsv
的代码拷到了本地:
$ git clone https://github.com/d3/d3-dsv.git
$ cd d3-dsv
$ npm install
$ npm run test
> d3-dsv@3.0.1 test
> TZ=America/Los_Angeles mocha 'test/**/*-test.js' && eslint src test
'TZ' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
怎么样?尽吹牛!还 run tests everywhere on the planet
呢,这不啪啪打脸吗?!?!
发现新大陆的惊喜是有的,但转瞬即逝:这个问题怎么解决?
既然问题是由 Safari
浏览器引发的,是否可以推断他俩的操作系统都是 Linux / Unix 这一路的?换成 Linux 就好使了?切换到 Windows
自带的 Linux
环境(即 Windows Subsystem for Linux
,简称 WSL
),又运行了一遍。没想到又双叒叕猜中了(之前的股票基金咋没那么神呢?):
【图 9 换到 WSL 环境下运行的单元测试结果图】
这么一折腾,问题反而简单了:只要让 PowerShell
能运行 Linux
下的这个命令就可以了。这都不用问 AI,之前就遇到过这样的问题,加个开发依赖就搞定了:
# (under PowerShell project root)
$ npm i -D cross-env
$ (Get-Content package.json) -replace '"test": "(.*?)"', '"test": "cross-env $1"' | Set-Content package.json
$ npm run test
成败在此一举:
【图 10 换回 Windows 环境下运行的单元测试结果图】
至此,才算真正破案了。
2.6 提交 PR
都测到这份上了,就提一个 pull request
吧。说干就干:
【图 11 正式提交到 d3-dsv 官方仓库下的拉取请求(pull-request)页截图】
3 结尾
以下是此次刨根问底的几点体会:
- 不要轻易放过《D3.js in Action》中推荐的资源链接(比如 Mike Bostock 那篇文章);
- 边学边动手实操;
- 尽量找到问题对应的源码;
- 从相关的
issue
提案、pull-request
议案中厘清事情的来龙去脉; - 积极参与,并尝试贡献自己的开源代码。
经此一役,d3.autoType
这个知识点里的坑,我相信这辈子都不会踩第二遍了。