Skip to content

skip intervals #1506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/features/projections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
2 changes: 1 addition & 1 deletion docs/features/scales.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion docs/marks/geo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
15 changes: 12 additions & 3 deletions src/interval.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
// For internal use.
export type LiteralTimeInterval =
| "3 months"
| "10 years"
| TimeIntervalName
| (`${TimeIntervalName}s` & Record<never, never>)
| (`${number} ${TimeIntervalName}` & Record<never, never>)
| (`${number} ${TimeIntervalName}s` & Record<never, never>);

/**
* 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
Expand All @@ -11,8 +20,8 @@ export type TimeIntervalName =
| "day"
| "week"
| "month"
| "quarter"
| "half"
| "quarter" // 3 months
| "half" // 6 months
| "year"
| "monday"
| "tuesday"
Expand Down Expand Up @@ -84,7 +93,7 @@ export interface NiceIntervalImplementation<T> extends RangeIntervalImplementati
}

/** A literal that can be automatically promoted to an interval. */
type LiteralInterval<T> = T extends Date ? TimeIntervalName : T extends number ? number : never;
type LiteralInterval<T> = T extends Date ? LiteralTimeInterval : T extends number ? number : never;

/**
* How to partition a continuous range into discrete intervals; one of:
Expand Down
44 changes: 31 additions & 13 deletions src/time.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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],
Expand All @@ -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],
Expand All @@ -41,14 +37,36 @@ const utcIntervals = new Map([
["sunday", utcSunday]
]);

function parseInterval(input, intervals) {
let name = `${input}`.toLowerCase();
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];
}
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}`);
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);
}
155 changes: 155 additions & 0 deletions test/marks/time-test.js
Original file line number Diff line number Diff line change
@@ -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));
});
6 changes: 3 additions & 3 deletions test/plots/aapl-bollinger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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})
Expand Down
2 changes: 1 addition & 1 deletion test/plots/aapl-close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function aaplCloseGridColor() {

export async function aaplCloseGridInterval() {
const AAPL = await d3.csv<any>("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() {
Expand Down