Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
系列文章可以查看《数据可视化》专栏
定制时距器
有时候需要基于 D3 所提供的各种类型的 interval 进行简单的定制,可以使用 interval.filter(test)
方法,该方法返回(定制过的)interval
方法 interval.filter(test)
的入参是一个测试函数,该函数会接收 Date 对象作为参数,最后只有该测试函数的返回值为 truthy 时,该 Date 对象才可以作为定制后的 interval 的「潜在的可采集的时间点」,类似于数组的 arr.filter()
方法,对元素进行筛选。
例如使用 d3.timeDay.range(start, end)
在时间范围中每隔 1 天进行时间点采集,最后生成包含一系列 Date 对象的数组。如果先对 d3.timeDay
这个 interval 进行定制,使其只能采集以 1 为结尾的日期
// 只能采集以 1 结尾的日期,如每个月的 1st、11th、21th、31th 才能进行采集
d3.timeDay.filter(d => (d.getDate() - 1) % 10 === 0)
复制代码
⚠️ interval.filter()
返回的定制过的 interval 没有 interval.count()
方法
? 对于需要按照特定间隔/步长来采集时间的需求,D3 提供了一个更简单的方法 interval.every(step)
,它相当于 interval.filter()
的语法糖(但不需要设置复杂的 test
测试函数,直接指定步长 step
即可),最后返回的是一个(定制过的)interval
d3.timeDay.every(2).range(new Date(2015, 0, 1), new Date(2015, 0, 7));
// [2015-01-01T00:00, 2015-01-03T00:00, 2015-01-05T00:00]
复制代码
虽然 interval.every()
和 interval.range()
类似,但是两者的作用和适用场景是不同的。
-
interval.every()
返回的是一个定制过的 interval,需要进一步调用 interval 的其他方法(对时间进行修约的方法)才会返回 Date 对象;而interval.range()
返回的是一个包含一系列 Date 对象的数组 -
interval.every(step)
其步长是针对 interval 所在时间尺度的父级而言的,例如对于d3.timeMinute.every(15)
由于步长是15
,即每 15 分钟采集一次时间点,所以最后返回的 interval 其「潜在的可采集的时间点」是:00
、:15
、:30
依次类推,这个采集的起始点是固定的,由于时间尺度是分钟,则其父级为小时,所以从父级时间尺度的下界,即 0 分 0 秒开始;而interval.range(start, end, step)
其步长是针对 interval 所在的时间尺度本身而言的,即采集的起始点有start
控制,从大于(或等于)start
时间点,且是 interval 所在时间尺度的下界开始,每隔特定的步长step
进行采集。console.log("--- within month ---"); const startDateWithinMonth = new Date("2022-05-02T10:12:12Z"); const endDateWithinMonth = new Date("2022-05-10T14:12:12Z"); // 使用未经修改的 interval // 以天为间隔时间尺度 // 采集时间点的步长为 2,即每隔一天采集一次 const rangeArrWithinMonth = d3.utcDay.range( startDateWithinMonth, endDateWithinMonth, 2 ); // 先对 interval 进行定制 // 以天为间隔时间尺度,则父级时间尺度为月 // 设置步长为 2 // 也是将采集时间点步长设置为 2,即每隔一天采集一次 const filterInterval = d3.utcDay.every(2); // 然后使用定制过的 interval 进行时间采集 const filterArrWithinMonth = filterInterval.range( startDateWithinMonth, endDateWithinMonth ); // 当时间范围的起始点和结束点在一个月内,两者的结果有时候是一样的(可以看以下关于 interval.every() 的工作原理的解释) console.log("within month range: ", rangeArrWithinMonth); console.log("within month filter range: ", filterArrWithinMonth); /* * --- outside month --- */ console.log("--- outside month ---"); // 对于时间范围跨越月份,情况就不一样 // 由于 interval.every(step) 设置的步长是先对父级的时间尺度而言的 // 可以将 d3.timeDay.every(2) 理解为从每个月(父级时间尺度)的 1 号开始,每隔一天进行时间采集,即每个月份的 1号、3号、5号……27号、29号、31号(如果该月份有 31号)这些采样的日子都已经是固定的 // 而 d3.timeDay.range(start, end, 2) 采样是由 start 参数决定的从哪一天开始采集 const startDateOutsideMonth = new Date("2022-05-28T10:12:12Z"); const endDateOutsideMonth = new Date("2022-06-06T14:12:12Z"); const rangeArrOutsideMonth = d3.utcDay.range( startDateOutsideMonth, endDateOutsideMonth, 2 ); const filterArrOutsideMonth = filterInterval.range( startDateOutsideMonth, endDateOutsideMonth ); console.log("outside month range: ", rangeArrOutsideMonth); console.log("outside month filter range: ", filterArrOutsideMonth); 复制代码
以下是控制台输出的结果,具体的代码演示可以查看这个 Codepen
所以通过
interval.every(step)
设置采集的间距,再使用定制过的时距器interval.range()
获得的一系列 Date 对象,它们之间时间间距可能会不均匀? 可以使用
interval.every(step)
从父级时间尺度设置采集时间的间距,同时也可以在调用interval.range(start, end, step)
时从 interval 所属的时间尺度,再设置采集时间的步长。可以理解为先在父级时间尺度约束固定的采集时间点,这样 interval 就成为一个具有离散的时间点的数组,然后在interval.range(start, end, step)
里设置的步长就是在这个数组里再挑选元素const start = new Date("2022-05-02T10:12:12Z"); const end = new Date("2022-05-10T14:12:12Z"); const filterInterval = d3.utcDay.every(2); const rangeArr = filterInterval.range(start, end); const rangeArrWithStep = filterInterval.range(start, end, 2); console.log('range array: ', rangeArr); console.log('range array with step: ', rangeArrWithStep) 复制代码
控制台输出的结果如下
⚠️ 和
interval.filter()
方法一样,interval.every()
返回的定制过的 interval 没有interval.count()
方法
如果希望深度定制 interval 可以使用方法 d3.timeInterval(floor, offset[, count[, field]])
可以对修约行为、日期偏移行为以及采集时间点的行为,最后该方法返回经过深度定制的 interval:
第一个参数 floor
是一个函数,它会接收一个 Date 对象,其作用是对时间进行向下修约,返回在该时间尺度下的下边界值(一个 Date 对象)(设置了 floor
函数后,相应的方法 interval([date])
、interval.ceil(date)
、interval.round(date)
的行为也确定了)
第二个参数 offset
是一个函数,它会接收一个 Date 对象和偏移步长 step(应该是整数),其作用是对时间偏移,偏移量是 step
,单位是当前的时间尺度,返回偏移后的 Date 对象
第三个(可选)参数 count
是一个函数,它会接收两个参数(它们已经修约到相应时间尺度的下边界),分别表示时间范围的起始点 start
(不包含)和结束点 end
(包括),其作用是计算在 (start, end]
时间范围内以相应时间尺度来计算,有多少个间隔。⚠️ 如果该参数未设置,则最后返回的深度定制的 interval 没有 interval.count()
和 interval.every()
方法
第四个(可选)参数 field
是一个函数,它会接收一个 Date 对象(已经修约到相应时间尺度的下边界),然后返回 Date 对象特定字段的值,例如 D3 默认提供的 d3.timeDay
时距器,其 field
函数就是 date => date.getDate() - 1
返回 Date 对象在月份中的天数。该方法定义了 interval.every()
的行为,因此在对 Date 修约并返回 Date 对象的特定字段值的时候,应该考虑其父级时间尺度的限制。⚠️ 如果该参数未设置,则默认返回在当前时间尺度下(以 UTC 方式计算)自 1970年1月1日以来的间隔数
// 创建一个自定义时距器
// 它的向下修约行为是直接去掉秒,即将 Date 对象的秒字段设置为 0
// 它的偏移行为的时间尺度是分钟级别的
const customInterval = d3.timeInterval(
(date) => {
date.setSeconds(0, 0);
},
(date, step) => {
date.setMinutes(date.getMinutes() + step);
}
);
const date = new Date("2022-02-06T13:12:12.123Z");
console.log("original date: ", date); // original date: 2022-02-06T13:12:12.123Z
const floorTime = customInterval(date);
const offsetTime = customInterval.offset(date, 2);
console.log("floor date: ", floorTime); // floor date: 2022-02-06T13:12:00.000Z
console.log("offset date: ", offsetTime); // offset date: 2022-02-06T13:14:12.123Z
复制代码
? 具体代码可以在这个 Codepen 查看
时间轴刻度
D3 为时间轴刻度生成提供了简便的方法
方法 d3.timeTicks(start, stop, count)
或 d3.utcTicks(start, stop, count)
可以在给定的时间范围内(开始点和结束点均包含),基于所需生成的刻度数量 count
进行调整,生成一系列保证可读性的时间对象 ? 和 d3.ticks
方法类似
基于 start
和 end
的距离,会自动从以下的多种时距器中,挑选出时间尺度适合的 interval(
- 1 second
- 5 seconds
- 15 seconds
- 30 seconds
- 1 minute
- 5 minutes
- 15 minutes
- 30 minutes
- 1 hour
- 3 hours
- 6 hours
- 12 hours
- 1 day
- 2 days
- 1 week
- 1 month
- 3 months
- 1 year
? 以上方法内部使用相应的时距器的方法 interval.range
生成一系列 Date 对象作为时间轴刻度,如果希望知道内部使用的是哪一种 interval,可以通过相应的方法 d3.timeTickInterval(start, stop, count)
和 d3.utcTickInterval(start, stop, count)
获取得到
? 对于 start
和 end
的间隔较小(毫秒)和较大(多年)该方法也支持,刻度值生成遵循 d3.ticks
方法的规则
start = new Date(Date.UTC(1970, 2, 1))
stop = new Date(Date.UTC(1996, 2, 19))
count = 4
d3.utcTicks(start, stop, count) // [1975-01-01, 1980-01-01, 1985-01-01, 1990-01-01, 1995-01-01]
复制代码
? 如果参数 count
不是一个数值,而直接就是一个 interval,则直接使用该时距器的 interval.range()
方法采集 Date 对象。