From cea038e42241cfec95a30cf1b8c302680a14e9f9 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 28 Apr 2023 10:56:51 -0700 Subject: [PATCH 1/6] skip intervals --- docs/features/projections.md | 2 +- docs/marks/geo.md | 2 +- src/interval.d.ts | 16 ++++++++++++--- src/time.js | 40 ++++++++++++++++++++++++------------ test/plots/aapl-bollinger.ts | 6 +++--- test/plots/aapl-close.ts | 2 +- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/docs/features/projections.md b/docs/features/projections.md index b689824acd..65ef60610c 100644 --- a/docs/features/projections.md +++ b/docs/features/projections.md @@ -206,7 +206,7 @@ Plot.plot({ marginRight: 0, projection: "albers", fx: { - interval: d3.utcYear.every(10), + interval: "10 years", tickFormat: (d) => `${d.getUTCFullYear()}’s`, label: null }, diff --git a/docs/marks/geo.md b/docs/marks/geo.md index 690e5ee267..94fe77c758 100644 --- a/docs/marks/geo.md +++ b/docs/marks/geo.md @@ -137,7 +137,7 @@ Plot.plot({ margin: 0, padding: 0, projection: "albers", - fy: {interval: d3.utcYear.every(10)}, + fy: {interval: "10 years"}, marks: [ Plot.geo(statemesh, {strokeOpacity: 0.2}), Plot.geo(nation), diff --git a/src/interval.d.ts b/src/interval.d.ts index 6e86bcd6f1..92d1ba8182 100644 --- a/src/interval.d.ts +++ b/src/interval.d.ts @@ -1,3 +1,9 @@ +// For internal use. +export type PeriodicTimeIntervalName = + | (`${number} ${TimeIntervalName}s` & Record) // see https://github.com/microsoft/TypeScript/issues/29729 + | "3 months" + | "10 years"; + /** * The built-in time intervals; UTC or local time, depending on context. The * *week* interval is an alias for *sunday*. The *quarter* interval is every @@ -11,8 +17,8 @@ export type TimeIntervalName = | "day" | "week" | "month" - | "quarter" - | "half" + | "quarter" // alias for 3 months + | "half" // alias for 6 months | "year" | "monday" | "tuesday" @@ -84,7 +90,11 @@ export interface NiceIntervalImplementation extends RangeIntervalImplementati } /** A literal that can be automatically promoted to an interval. */ -type LiteralInterval = T extends Date ? TimeIntervalName : T extends number ? number : never; +type LiteralInterval = T extends Date + ? TimeIntervalName | PeriodicTimeIntervalName + : T extends number + ? number + : never; /** * How to partition a continuous range into discrete intervals; one of: diff --git a/src/time.js b/src/time.js index 71892547d5..3e438b8e47 100644 --- a/src/time.js +++ b/src/time.js @@ -1,4 +1,4 @@ -import {utcSecond, utcMinute, utcHour, utcDay, utcWeek, utcMonth, utcYear} from "d3"; +import {utcSecond, utcMinute, utcHour, unixDay, utcWeek, utcMonth, utcYear} from "d3"; import {utcMonday, utcTuesday, utcWednesday, utcThursday, utcFriday, utcSaturday, utcSunday} from "d3"; import {timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear} from "d3"; import {timeMonday, timeTuesday, timeWednesday, timeThursday, timeFriday, timeSaturday, timeSunday} from "d3"; @@ -7,11 +7,9 @@ const timeIntervals = new Map([ ["second", timeSecond], ["minute", timeMinute], ["hour", timeHour], - ["day", timeDay], + ["day", timeDay], // TODO local time equivalent of unixDay? ["week", timeWeek], ["month", timeMonth], - ["quarter", timeMonth.every(3)], - ["half", timeMonth.every(6)], ["year", timeYear], ["monday", timeMonday], ["tuesday", timeTuesday], @@ -26,11 +24,9 @@ const utcIntervals = new Map([ ["second", utcSecond], ["minute", utcMinute], ["hour", utcHour], - ["day", utcDay], + ["day", unixDay], ["week", utcWeek], ["month", utcMonth], - ["quarter", utcMonth.every(3)], - ["half", utcMonth.every(6)], ["year", utcYear], ["monday", utcMonday], ["tuesday", utcTuesday], @@ -41,14 +37,32 @@ const utcIntervals = new Map([ ["sunday", utcSunday] ]); +function parseInterval(input, intervals) { + let name = `${input}`.toLowerCase(); + switch (name) { + case "quarter": + return intervals.get("month").every(3); + case "half": + return intervals.get("month").every(6); + } + let interval, period; + if (/s$/.test(name)) name = name.slice(0, -1); + const match = /^(?:(\d+)\s+)/.exec(name); + if (match) { + name = name.slice(match[0].length); + period = +match[1]; + } + interval = intervals.get(name); + if (!interval) throw new Error(`unknown interval: ${input}`); + if (!(period > 1)) return interval; + if (!interval.every) throw new Error(`non-periodic interval: ${name}`); + return interval.every(period); +} + export function maybeTimeInterval(interval) { - const i = timeIntervals.get(`${interval}`.toLowerCase()); - if (!i) throw new Error(`unknown interval: ${interval}`); - return i; + return parseInterval(interval, timeIntervals); } export function maybeUtcInterval(interval) { - const i = utcIntervals.get(`${interval}`.toLowerCase()); - if (!i) throw new Error(`unknown interval: ${interval}`); - return i; + return parseInterval(interval, utcIntervals); } diff --git a/test/plots/aapl-bollinger.ts b/test/plots/aapl-bollinger.ts index 768f20cec4..c83fe05cb6 100644 --- a/test/plots/aapl-bollinger.ts +++ b/test/plots/aapl-bollinger.ts @@ -41,9 +41,9 @@ export async function aaplBollingerGridSpacing() { Plot.gridY({interval: 10, stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}), Plot.gridY({interval: 20, stroke: "#fff", strokeOpacity: 1}), Plot.axisY({interval: 20}), - Plot.gridX({interval: d3.utcMonth.every(3), stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}), - Plot.gridX({interval: d3.utcYear, stroke: "#fff", strokeOpacity: 1}), - Plot.axisX({interval: d3.utcYear}), + Plot.gridX({interval: "3 months", stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}), + Plot.gridX({interval: "1 years", stroke: "#fff", strokeOpacity: 1}), + Plot.axisX({interval: "1 years"}), Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})), Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})), Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1}) diff --git a/test/plots/aapl-close.ts b/test/plots/aapl-close.ts index 6d4a8053ef..0c0b32105e 100644 --- a/test/plots/aapl-close.ts +++ b/test/plots/aapl-close.ts @@ -37,7 +37,7 @@ export async function aaplCloseGridColor() { export async function aaplCloseGridInterval() { const AAPL = await d3.csv("data/aapl.csv", d3.autoType); - return Plot.lineY(AAPL, {x: "Date", y: "Close"}).plot({x: {grid: d3.utcMonth.every(3)}}); + return Plot.lineY(AAPL, {x: "Date", y: "Close"}).plot({x: {grid: "3 months"}}); } export async function aaplCloseGridIntervalName() { From fccd1a74b33d963e69a35608f622164744596afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 28 Apr 2023 13:34:21 -0700 Subject: [PATCH 2/6] tiny bit of documentation --- docs/features/scales.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/scales.md b/docs/features/scales.md index ad0a070878..18aae3bbc0 100644 --- a/docs/features/scales.md +++ b/docs/features/scales.md @@ -705,7 +705,7 @@ The default range depends on the scale: for position scales (*x*, *y*, *fx*, and The behavior of the **unknown** scale option depends on the scale type. For quantitative and temporal scales, the unknown value is used whenever the input value is undefined, null, or NaN. For ordinal or categorical scales, the unknown value is returned for any input value outside the domain. For band or point scales, the unknown option has no effect; it is effectively always equal to undefined. If the unknown option is set to undefined (the default), or null or NaN, then the affected input values will be considered undefined and filtered from the output. -For data at regular intervals, such as integer values or daily samples, the [**interval** option](#scale-transforms) can be used to enforce uniformity. The specified *interval*—such as d3.utcMonth—must expose an *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) functions. The option can also be specified as a number, in which case it will be promoted to a numeric interval with the given step. The option can alternatively be specified as a string (*second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, *sunday*) naming the corresponding time interval. This option sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, the default *scale*.domain is an array of uniformly-spaced values spanning the extent of the values associated with the scale. +For data at regular intervals, such as integer values or daily samples, the [**interval** option](#scale-transforms) can be used to enforce uniformity. The specified *interval*—such as d3.utcMonth—must expose an *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) functions. The option can also be specified as a number, in which case it will be promoted to a numeric interval with the given step. The option can alternatively be specified as a string (*second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, *sunday*) naming the corresponding time interval, or a skip interval consisting of a number followed by the interval name (possibly pluralized), such as *3 months* or *10 years*. This option sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, the default *scale*.domain is an array of uniformly-spaced values spanning the extent of the values associated with the scale. Quantitative scales can be further customized with additional options: From a6ac4f5939969efc89e1af860bc8325bbe4fe94b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 28 Apr 2023 16:45:18 -0400 Subject: [PATCH 3/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philippe Rivière --- src/interval.d.ts | 9 +++++---- test/plots/aapl-bollinger.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/interval.d.ts b/src/interval.d.ts index 92d1ba8182..75157dc2eb 100644 --- a/src/interval.d.ts +++ b/src/interval.d.ts @@ -1,8 +1,11 @@ // For internal use. export type PeriodicTimeIntervalName = - | (`${number} ${TimeIntervalName}s` & Record) // see https://github.com/microsoft/TypeScript/issues/29729 + | ((`1 ${TimeIntervalName}` | `${number} ${TimeIntervalName}s`) & Record) // see https://github.com/microsoft/TypeScript/issues/29729 | "3 months" - | "10 years"; + | "10 years" + | "quarter" // alias for 3 months, no plural + | "half" // alias for 6 months, no plural +; /** * The built-in time intervals; UTC or local time, depending on context. The @@ -17,8 +20,6 @@ export type TimeIntervalName = | "day" | "week" | "month" - | "quarter" // alias for 3 months - | "half" // alias for 6 months | "year" | "monday" | "tuesday" diff --git a/test/plots/aapl-bollinger.ts b/test/plots/aapl-bollinger.ts index c83fe05cb6..7ff5f6d2ce 100644 --- a/test/plots/aapl-bollinger.ts +++ b/test/plots/aapl-bollinger.ts @@ -42,8 +42,8 @@ export async function aaplBollingerGridSpacing() { Plot.gridY({interval: 20, stroke: "#fff", strokeOpacity: 1}), Plot.axisY({interval: 20}), Plot.gridX({interval: "3 months", stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}), - Plot.gridX({interval: "1 years", stroke: "#fff", strokeOpacity: 1}), - Plot.axisX({interval: "1 years"}), + Plot.gridX({interval: "1 year", stroke: "#fff", strokeOpacity: 1}), + Plot.axisX({interval: "1 year"}), Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})), Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})), Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1}) From 66630b71b6432b31a859e8da2d6226e5d71443d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 28 Apr 2023 14:34:51 -0700 Subject: [PATCH 4/6] prettier --- src/interval.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/interval.d.ts b/src/interval.d.ts index 75157dc2eb..ee6981365b 100644 --- a/src/interval.d.ts +++ b/src/interval.d.ts @@ -4,8 +4,7 @@ export type PeriodicTimeIntervalName = | "3 months" | "10 years" | "quarter" // alias for 3 months, no plural - | "half" // alias for 6 months, no plural -; + | "half"; // alias for 6 months, no plural /** * The built-in time intervals; UTC or local time, depending on context. The From 0041ba07e1ea0f9fcb4d14e234346c591605eff5 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 28 Apr 2023 14:35:59 -0700 Subject: [PATCH 5/6] LiteralTimeInterval; 3 quarters --- src/interval.d.ts | 17 ++++++++--------- src/time.js | 22 +++++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/interval.d.ts b/src/interval.d.ts index ee6981365b..29882a04ff 100644 --- a/src/interval.d.ts +++ b/src/interval.d.ts @@ -1,10 +1,11 @@ // For internal use. -export type PeriodicTimeIntervalName = - | ((`1 ${TimeIntervalName}` | `${number} ${TimeIntervalName}s`) & Record) // see https://github.com/microsoft/TypeScript/issues/29729 +export type LiteralTimeInterval = | "3 months" | "10 years" - | "quarter" // alias for 3 months, no plural - | "half"; // alias for 6 months, no plural + | TimeIntervalName + | (`${TimeIntervalName}s` & Record) + | (`${number} ${TimeIntervalName}` & Record) + | (`${number} ${TimeIntervalName}s` & Record); /** * The built-in time intervals; UTC or local time, depending on context. The @@ -19,6 +20,8 @@ export type TimeIntervalName = | "day" | "week" | "month" + | "quarter" // 3 months + | "half" // 6 months | "year" | "monday" | "tuesday" @@ -90,11 +93,7 @@ export interface NiceIntervalImplementation extends RangeIntervalImplementati } /** A literal that can be automatically promoted to an interval. */ -type LiteralInterval = T extends Date - ? TimeIntervalName | PeriodicTimeIntervalName - : T extends number - ? number - : never; +type LiteralInterval = T extends Date ? LiteralTimeInterval : T extends number ? number : never; /** * How to partition a continuous range into discrete intervals; one of: diff --git a/src/time.js b/src/time.js index 3e438b8e47..ac15b8131b 100644 --- a/src/time.js +++ b/src/time.js @@ -39,20 +39,24 @@ const utcIntervals = new Map([ function parseInterval(input, intervals) { let name = `${input}`.toLowerCase(); - switch (name) { - case "quarter": - return intervals.get("month").every(3); - case "half": - return intervals.get("month").every(6); - } - let interval, period; - if (/s$/.test(name)) name = name.slice(0, -1); + if (name.endsWith("s")) name = name.slice(0, -1); // drop plural + let period = 1; const match = /^(?:(\d+)\s+)/.exec(name); if (match) { name = name.slice(match[0].length); period = +match[1]; } - interval = intervals.get(name); + switch (name) { + case "quarter": + name = "month"; + period *= 3; + break; + case "half": + name = "month"; + period *= 6; + break; + } + let interval = intervals.get(name); if (!interval) throw new Error(`unknown interval: ${input}`); if (!(period > 1)) return interval; if (!interval.every) throw new Error(`non-periodic interval: ${name}`); From bfd9c29c1aebb7df4268b366dded4fd2d59e42e4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 28 Apr 2023 14:49:13 -0700 Subject: [PATCH 6/6] test interval parsing --- test/marks/time-test.js | 155 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 test/marks/time-test.js diff --git a/test/marks/time-test.js b/test/marks/time-test.js new file mode 100644 index 0000000000..8445bcd820 --- /dev/null +++ b/test/marks/time-test.js @@ -0,0 +1,155 @@ +import assert from "assert"; +import * as d3 from "d3"; +import {maybeTimeInterval, maybeUtcInterval} from "../../src/time.js"; + +it("maybeTimeInterval('period') returns the expected time interval", () => { + assert.strictEqual(maybeTimeInterval("second"), d3.timeSecond); + assert.strictEqual(maybeTimeInterval("minute"), d3.timeMinute); + assert.strictEqual(maybeTimeInterval("hour"), d3.timeHour); + assert.strictEqual(maybeTimeInterval("day"), d3.timeDay); + assert.strictEqual(maybeTimeInterval("week"), d3.timeWeek); + assert.strictEqual(maybeTimeInterval("month"), d3.timeMonth); + assert.strictEqual(maybeTimeInterval("year"), d3.timeYear); + assert.strictEqual(maybeTimeInterval("monday"), d3.timeMonday); + assert.strictEqual(maybeTimeInterval("tuesday"), d3.timeTuesday); + assert.strictEqual(maybeTimeInterval("wednesday"), d3.timeWednesday); + assert.strictEqual(maybeTimeInterval("thursday"), d3.timeThursday); + assert.strictEqual(maybeTimeInterval("friday"), d3.timeFriday); + assert.strictEqual(maybeTimeInterval("saturday"), d3.timeSaturday); + assert.strictEqual(maybeTimeInterval("sunday"), d3.timeSunday); +}); + +it("maybeTimeInterval('periods') returns the expected time interval", () => { + assert.strictEqual(maybeTimeInterval("seconds"), d3.timeSecond); + assert.strictEqual(maybeTimeInterval("minutes"), d3.timeMinute); + assert.strictEqual(maybeTimeInterval("hours"), d3.timeHour); + assert.strictEqual(maybeTimeInterval("days"), d3.timeDay); + assert.strictEqual(maybeTimeInterval("weeks"), d3.timeWeek); + assert.strictEqual(maybeTimeInterval("months"), d3.timeMonth); + assert.strictEqual(maybeTimeInterval("years"), d3.timeYear); + assert.strictEqual(maybeTimeInterval("mondays"), d3.timeMonday); + assert.strictEqual(maybeTimeInterval("tuesdays"), d3.timeTuesday); + assert.strictEqual(maybeTimeInterval("wednesdays"), d3.timeWednesday); + assert.strictEqual(maybeTimeInterval("thursdays"), d3.timeThursday); + assert.strictEqual(maybeTimeInterval("fridays"), d3.timeFriday); + assert.strictEqual(maybeTimeInterval("saturdays"), d3.timeSaturday); + assert.strictEqual(maybeTimeInterval("sundays"), d3.timeSunday); +}); + +it("maybeTimeInterval('1 periods) returns the expected time interval", () => { + assert.strictEqual(maybeTimeInterval("1 second"), d3.timeSecond); + assert.strictEqual(maybeTimeInterval("1 minute"), d3.timeMinute); + assert.strictEqual(maybeTimeInterval("1 hour"), d3.timeHour); + assert.strictEqual(maybeTimeInterval("1 day"), d3.timeDay); + assert.strictEqual(maybeTimeInterval("1 week"), d3.timeWeek); + assert.strictEqual(maybeTimeInterval("1 month"), d3.timeMonth); + assert.strictEqual(maybeTimeInterval("1 year"), d3.timeYear); + assert.strictEqual(maybeTimeInterval("1 monday"), d3.timeMonday); + assert.strictEqual(maybeTimeInterval("1 tuesday"), d3.timeTuesday); + assert.strictEqual(maybeTimeInterval("1 wednesday"), d3.timeWednesday); + assert.strictEqual(maybeTimeInterval("1 thursday"), d3.timeThursday); + assert.strictEqual(maybeTimeInterval("1 friday"), d3.timeFriday); + assert.strictEqual(maybeTimeInterval("1 saturday"), d3.timeSaturday); + assert.strictEqual(maybeTimeInterval("1 sunday"), d3.timeSunday); +}); + +it("maybeTimeInterval('1 periods') returns the expected time interval", () => { + assert.strictEqual(maybeTimeInterval("1 seconds"), d3.timeSecond); + assert.strictEqual(maybeTimeInterval("1 minutes"), d3.timeMinute); + assert.strictEqual(maybeTimeInterval("1 hours"), d3.timeHour); + assert.strictEqual(maybeTimeInterval("1 days"), d3.timeDay); + assert.strictEqual(maybeTimeInterval("1 weeks"), d3.timeWeek); + assert.strictEqual(maybeTimeInterval("1 months"), d3.timeMonth); + assert.strictEqual(maybeTimeInterval("1 years"), d3.timeYear); + assert.strictEqual(maybeTimeInterval("1 mondays"), d3.timeMonday); + assert.strictEqual(maybeTimeInterval("1 tuesdays"), d3.timeTuesday); + assert.strictEqual(maybeTimeInterval("1 wednesdays"), d3.timeWednesday); + assert.strictEqual(maybeTimeInterval("1 thursdays"), d3.timeThursday); + assert.strictEqual(maybeTimeInterval("1 fridays"), d3.timeFriday); + assert.strictEqual(maybeTimeInterval("1 saturdays"), d3.timeSaturday); + assert.strictEqual(maybeTimeInterval("1 sundays"), d3.timeSunday); +}); + +it("maybeTimeInterval('n seconds') returns the expected time interval", () => { + const start = new Date("2012-01-01T12:01:02"); + const end = new Date("2012-01-01T12:14:08"); + assert.deepStrictEqual(maybeTimeInterval("5 seconds").range(start, end), d3.timeSecond.every(5).range(start, end)); + assert.deepStrictEqual(maybeTimeInterval("15 seconds").range(start, end), d3.timeSecond.every(15).range(start, end)); + assert.deepStrictEqual(maybeTimeInterval("45 seconds").range(start, end), d3.timeSecond.every(45).range(start, end)); +}); + +it("maybeUtcInterval('period') returns the expected UTC interval", () => { + assert.strictEqual(maybeUtcInterval("second"), d3.utcSecond); + assert.strictEqual(maybeUtcInterval("minute"), d3.utcMinute); + assert.strictEqual(maybeUtcInterval("hour"), d3.utcHour); + assert.strictEqual(maybeUtcInterval("day"), d3.unixDay); + assert.strictEqual(maybeUtcInterval("week"), d3.utcWeek); + assert.strictEqual(maybeUtcInterval("month"), d3.utcMonth); + assert.strictEqual(maybeUtcInterval("year"), d3.utcYear); + assert.strictEqual(maybeUtcInterval("monday"), d3.utcMonday); + assert.strictEqual(maybeUtcInterval("tuesday"), d3.utcTuesday); + assert.strictEqual(maybeUtcInterval("wednesday"), d3.utcWednesday); + assert.strictEqual(maybeUtcInterval("thursday"), d3.utcThursday); + assert.strictEqual(maybeUtcInterval("friday"), d3.utcFriday); + assert.strictEqual(maybeUtcInterval("saturday"), d3.utcSaturday); + assert.strictEqual(maybeUtcInterval("sunday"), d3.utcSunday); +}); + +it("maybeUtcInterval('periods') returns the expected UTC interval", () => { + assert.strictEqual(maybeUtcInterval("seconds"), d3.utcSecond); + assert.strictEqual(maybeUtcInterval("minutes"), d3.utcMinute); + assert.strictEqual(maybeUtcInterval("hours"), d3.utcHour); + assert.strictEqual(maybeUtcInterval("days"), d3.unixDay); + assert.strictEqual(maybeUtcInterval("weeks"), d3.utcWeek); + assert.strictEqual(maybeUtcInterval("months"), d3.utcMonth); + assert.strictEqual(maybeUtcInterval("years"), d3.utcYear); + assert.strictEqual(maybeUtcInterval("mondays"), d3.utcMonday); + assert.strictEqual(maybeUtcInterval("tuesdays"), d3.utcTuesday); + assert.strictEqual(maybeUtcInterval("wednesdays"), d3.utcWednesday); + assert.strictEqual(maybeUtcInterval("thursdays"), d3.utcThursday); + assert.strictEqual(maybeUtcInterval("fridays"), d3.utcFriday); + assert.strictEqual(maybeUtcInterval("saturdays"), d3.utcSaturday); + assert.strictEqual(maybeUtcInterval("sundays"), d3.utcSunday); +}); + +it("maybeUtcInterval('1 periods) returns the expected UTC interval", () => { + assert.strictEqual(maybeUtcInterval("1 second"), d3.utcSecond); + assert.strictEqual(maybeUtcInterval("1 minute"), d3.utcMinute); + assert.strictEqual(maybeUtcInterval("1 hour"), d3.utcHour); + assert.strictEqual(maybeUtcInterval("1 day"), d3.unixDay); + assert.strictEqual(maybeUtcInterval("1 week"), d3.utcWeek); + assert.strictEqual(maybeUtcInterval("1 month"), d3.utcMonth); + assert.strictEqual(maybeUtcInterval("1 year"), d3.utcYear); + assert.strictEqual(maybeUtcInterval("1 monday"), d3.utcMonday); + assert.strictEqual(maybeUtcInterval("1 tuesday"), d3.utcTuesday); + assert.strictEqual(maybeUtcInterval("1 wednesday"), d3.utcWednesday); + assert.strictEqual(maybeUtcInterval("1 thursday"), d3.utcThursday); + assert.strictEqual(maybeUtcInterval("1 friday"), d3.utcFriday); + assert.strictEqual(maybeUtcInterval("1 saturday"), d3.utcSaturday); + assert.strictEqual(maybeUtcInterval("1 sunday"), d3.utcSunday); +}); + +it("maybeUtcInterval('1 periods') returns the expected UTC interval", () => { + assert.strictEqual(maybeUtcInterval("1 seconds"), d3.utcSecond); + assert.strictEqual(maybeUtcInterval("1 minutes"), d3.utcMinute); + assert.strictEqual(maybeUtcInterval("1 hours"), d3.utcHour); + assert.strictEqual(maybeUtcInterval("1 days"), d3.unixDay); + assert.strictEqual(maybeUtcInterval("1 weeks"), d3.utcWeek); + assert.strictEqual(maybeUtcInterval("1 months"), d3.utcMonth); + assert.strictEqual(maybeUtcInterval("1 years"), d3.utcYear); + assert.strictEqual(maybeUtcInterval("1 mondays"), d3.utcMonday); + assert.strictEqual(maybeUtcInterval("1 tuesdays"), d3.utcTuesday); + assert.strictEqual(maybeUtcInterval("1 wednesdays"), d3.utcWednesday); + assert.strictEqual(maybeUtcInterval("1 thursdays"), d3.utcThursday); + assert.strictEqual(maybeUtcInterval("1 fridays"), d3.utcFriday); + assert.strictEqual(maybeUtcInterval("1 saturdays"), d3.utcSaturday); + assert.strictEqual(maybeUtcInterval("1 sundays"), d3.utcSunday); +}); + +it("maybeUtcInterval('n seconds') returns the expected UTC interval", () => { + const start = new Date("2012-01-01T12:01:02"); + const end = new Date("2012-01-01T12:14:08"); + assert.deepStrictEqual(maybeUtcInterval("5 seconds").range(start, end), d3.utcSecond.every(5).range(start, end)); + assert.deepStrictEqual(maybeUtcInterval("15 seconds").range(start, end), d3.utcSecond.every(15).range(start, end)); + assert.deepStrictEqual(maybeUtcInterval("45 seconds").range(start, end), d3.utcSecond.every(45).range(start, end)); +});