【D3.js in Action 3 精译_033】4.1.0 DIY 实战:如何通过学习 d3.autoType 函数深度参与 D3 生态建设

#人工智能时代,程序员如何保持核心竞争力?#

当前内容所在位置(可进入专栏查看其他译好的章节内容)

  • 第一部分 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 起因

上一篇(即本专栏第 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.autoTyped3-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 规范导致的转换不一致问题

【图1 d3.autoType 在处理日期和时间戳时由于 ECMAScript 规范导致的转换不一致问题】

其实也不难理解,一般转换个时间戳,谁会希望拿到一个和所在地区隔了 16 小时时差的结果呢?只是日期按子午线的时差来算,确实有点强人所难了。

这个坑算是原生 JavaScript 挖的,今后注意便是了。

2.2 关于“中规中矩”

文章还说,d3.autoType 还支持美国各州县的五位邮政编码转换。六位就不行?赶紧测一下:

图 2 d3.autoType 转换字符型编码没有预想中的五位数限制

【图 2 d3.autoType 转换字符型编码没有预想中的五位数限制】

那么,不是那么中规中矩的呢?它会直接跳过——

图 3 对于不那么中规中矩的数字字符串,d3.autoType 直接跳过

【图 3 对于不那么中规中矩的数字字符串,d3.autoType 直接跳过】

不仅数字如此,日期和布尔值也一样——

图 4 对于不那么中规中矩的日期和布尔值,d3.autoType 也直接跳过

【图 4 对于不那么中规中矩的日期和布尔值,d3.autoType 也直接跳过】

最后,Mike 总结了一下 d3.autoType 能转的类型:

  1. If empty, then null.(为空的,就转为 null
  2. If exactly “true”, then true.(严格写作 "true" 的,才转为 true
  3. If exactly “false”, then false.(严格写作 "false" 的,才转为 false
  4. If exactly “NaN”, then NaN.(严格写作 "NaN"的,才转为 NaN
  5. Otherwise, if coercible to a number, then a number.(以上都不行,但能强转为数字的,才转为数字)
  6. Otherwise, if a date-only or date-time string, then a Date.(以上再不行,但只有日期或者带时间的日期字符串,才转为 Date 型)
  7. 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 对源码正则表达式的解读结果

【图 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 无意间看到的作者个人简介,迷之搞笑~

【图 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 给出的单元测试修复意见

【图 7 Mike Bostock 给出的单元测试修复意见】

要不怎么说姜还是老的辣呢,原来类似的问题早就处理过了,不用重复造轮子。在大神上帝视角般的关照下,幸运的 Fil 终于通过了测试,回复标题上都难掩激动之情:

图 8 Fil 通过测试后的回复标题(地球上任何地方都能跑通测试了)

【图 8 Fil 通过测试后的回复标题(地球上任何地方都能跑通测试了)】

有这么夸张吗?点开一看,原来是在 package.jsontest 脚本的开头加了一个 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 环境下运行的单元测试结果图

【图 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 环境下运行的单元测试结果图

【图 10 换回 Windows 环境下运行的单元测试结果图】

至此,才算真正破案了。

2.6 提交 PR

都测到这份上了,就提一个 pull request 吧。说干就干:

图 11 正式提交到 d3-dsv 官方仓库下的拉取请求(pull-request)页截图

【图 11 正式提交到 d3-dsv 官方仓库下的拉取请求(pull-request)页截图】

3 结尾

以下是此次刨根问底的几点体会:

  • 不要轻易放过《D3.js in Action》中推荐的资源链接(比如 Mike Bostock 那篇文章);
  • 边学边动手实操;
  • 尽量找到问题对应的源码;
  • 从相关的 issue 提案、pull-request 议案中厘清事情的来龙去脉;
  • 积极参与,并尝试贡献自己的开源代码。

经此一役,d3.autoType 这个知识点里的坑,我相信这辈子都不会踩第二遍了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值