Skip to content

Commit 5474ea5

Browse files
mbostockFil
andcommitted
default multi-line time format (#1718)
* remove maybeAutoTickFormat * multi-line time format * Update test/plots/covid-ihme-projected-deaths.ts Co-authored-by: Philippe Rivière <[email protected]> * link to d3-time * fix test size --------- Co-authored-by: Philippe Rivière <[email protected]>
1 parent 44c6495 commit 5474ea5

34 files changed

+4185
-241
lines changed

src/axes.js

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
1-
import {format, utcFormat} from "d3";
2-
import {formatIsoDate} from "./format.js";
3-
import {constant, isTemporal, string} from "./options.js";
41
import {isOrdinalScale} from "./scales.js";
52

63
export function inferFontVariant(scale) {
74
return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums";
85
}
9-
10-
// D3 doesn’t provide a tick format for ordinal scales; we want shorthand when
11-
// an ordinal domain is numbers or dates, and we want null to mean the empty
12-
// string, not the default identity format. TODO Remove this in favor of the
13-
// axis mark’s inferTickFormat.
14-
export function maybeAutoTickFormat(tickFormat, domain) {
15-
return tickFormat === undefined
16-
? isTemporal(domain)
17-
? formatIsoDate
18-
: string
19-
: typeof tickFormat === "function"
20-
? tickFormat
21-
: (typeof tickFormat === "string" ? (isTemporal(domain) ? utcFormat : format) : constant)(tickFormat);
22-
}

src/legends/swatches.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {pathRound as path} from "d3";
2-
import {inferFontVariant, maybeAutoTickFormat} from "../axes.js";
3-
import {createContext, create} from "../context.js";
2+
import {inferFontVariant} from "../axes.js";
3+
import {create, createContext} from "../context.js";
44
import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js";
55
import {isOrdinalScale, isThresholdScale} from "../scales.js";
66
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";
7+
import {inferTickFormat} from "../marks/axis.js";
78

89
function maybeScale(scale, key) {
910
if (key == null) return key;
@@ -85,7 +86,7 @@ function legendItems(scale, options = {}, swatch) {
8586
} = options;
8687
const context = createContext(options);
8788
className = maybeClassName(className);
88-
tickFormat = maybeAutoTickFormat(tickFormat, scale.domain);
89+
if (typeof tickFormat !== "function") tickFormat = inferTickFormat(scale.scale, undefined, tickFormat);
8990

9091
const swatches = create("div", context).attr(
9192
"class",

src/marks/axis.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {isIterable, isNoneish, isTemporal, orderof} from "../options.js";
77
import {maybeColorChannel, maybeNumberChannel, maybeRangeInterval} from "../options.js";
88
import {isTemporalScale} from "../scales.js";
99
import {offset} from "../style.js";
10-
import {isTimeYear, isUtcYear} from "../time.js";
10+
import {formatTimeTicks, isTimeYear, isUtcYear} from "../time.js";
1111
import {initializer} from "../transforms/basic.js";
1212
import {ruleX, ruleY} from "./rule.js";
1313
import {text, textX, textY} from "./text.js";
@@ -368,7 +368,7 @@ function axisTextKy(
368368
},
369369
function (scale, ticks, channels) {
370370
if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
371-
if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat);
371+
if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat, anchor);
372372
}
373373
);
374374
}
@@ -415,7 +415,7 @@ function axisTextKx(
415415
},
416416
function (scale, ticks, channels) {
417417
if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
418-
if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat);
418+
if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat, anchor);
419419
}
420420
);
421421
}
@@ -565,15 +565,17 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
565565
return m;
566566
}
567567

568-
function inferTextChannel(scale, ticks, tickFormat) {
569-
return {value: inferTickFormat(scale, ticks, tickFormat)};
568+
function inferTextChannel(scale, ticks, tickFormat, anchor) {
569+
return {value: inferTickFormat(scale, ticks, tickFormat, anchor)};
570570
}
571571

572572
// D3’s ordinal scales simply use toString by default, but if the ordinal scale
573573
// domain (or ticks) are numbers or dates (say because we’re applying a time
574574
// interval to the ordinal scale), we want Plot’s default formatter.
575-
export function inferTickFormat(scale, ticks, tickFormat) {
576-
return scale.tickFormat
575+
export function inferTickFormat(scale, ticks, tickFormat, anchor) {
576+
return tickFormat === undefined && isTemporalScale(scale)
577+
? formatTimeTicks(scale, ticks, anchor)
578+
: scale.tickFormat
577579
? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat)
578580
: tickFormat === undefined
579581
? isUtcYear(scale.interval)

src/time.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
1+
import {bisector, extent, timeFormat, utcFormat} from "d3";
12
import {utcSecond, utcMinute, utcHour, unixDay, utcWeek, utcMonth, utcYear} from "d3";
23
import {utcMonday, utcTuesday, utcWednesday, utcThursday, utcFriday, utcSaturday, utcSunday} from "d3";
34
import {timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear} from "d3";
45
import {timeMonday, timeTuesday, timeWednesday, timeThursday, timeFriday, timeSaturday, timeSunday} from "d3";
6+
import {orderof} from "./options.js";
7+
8+
const durationSecond = 1000;
9+
const durationMinute = durationSecond * 60;
10+
const durationHour = durationMinute * 60;
11+
const durationDay = durationHour * 24;
12+
const durationWeek = durationDay * 7;
13+
const durationMonth = durationDay * 30;
14+
const durationYear = durationDay * 365;
15+
16+
// See https://github.com/d3/d3-time/blob/9e8dc940f38f78d7588aad68a54a25b1f0c2d97b/src/ticks.js#L14-L33
17+
const formats = [
18+
["millisecond", 0.5 * durationSecond],
19+
["second", durationSecond],
20+
["second", 30 * durationSecond],
21+
["minute", durationMinute],
22+
["minute", 30 * durationMinute],
23+
["hour", durationHour],
24+
["hour", 12 * durationHour],
25+
["day", durationDay],
26+
["day", 2 * durationDay],
27+
["week", durationWeek],
28+
["month", durationMonth],
29+
["month", 3 * durationMonth],
30+
["year", durationYear]
31+
];
532

633
const timeIntervals = new Map([
734
["second", timeSecond],
@@ -82,3 +109,49 @@ export function isTimeYear(i) {
82109
const date = i.floor(new Date(2000, 11, 31));
83110
return timeYear(date) >= date; // coercing equality
84111
}
112+
113+
export function formatTimeTicks(scale, ticks, anchor) {
114+
const format = scale.type === "time" ? timeFormat : utcFormat;
115+
const template =
116+
anchor === "left" || anchor === "right"
117+
? (f1, f2) => `\n${f1}\n${f2}` // extra newline to keep f1 centered
118+
: anchor === "top"
119+
? (f1, f2) => `${f2}\n${f1}`
120+
: (f1, f2) => `${f1}\n${f2}`;
121+
switch (getTimeTicksInterval(scale, ticks)) {
122+
case "millisecond":
123+
return formatConditional(format(".%L"), format(":%M:%S"), template);
124+
case "second":
125+
return formatConditional(format(":%S"), format("%-I:%M"), template);
126+
case "minute":
127+
return formatConditional(format("%-I:%M"), format("%p"), template);
128+
case "hour":
129+
return formatConditional(format("%-I %p"), format("%b %-d"), template);
130+
case "day":
131+
return formatConditional(format("%-d"), format("%b"), template);
132+
case "week":
133+
return formatConditional(format("%-d"), format("%b"), template);
134+
case "month":
135+
return formatConditional(format("%b"), format("%Y"), template);
136+
case "year":
137+
return format("%Y");
138+
}
139+
throw new Error("unable to format time ticks");
140+
}
141+
142+
// See https://github.com/d3/d3-time/blob/9e8dc940f38f78d7588aad68a54a25b1f0c2d97b/src/ticks.js#L43-L50
143+
function getTimeTicksInterval(scale, ticks) {
144+
const [start, stop] = extent(scale.domain());
145+
const count = typeof ticks === "number" ? ticks : 10; // TODO detect ticks as time interval?
146+
const step = Math.abs(stop - start) / count;
147+
return formats[bisector(([, step]) => Math.log(step)).center(formats, Math.log(step))][0];
148+
}
149+
150+
function formatConditional(format1, format2, template) {
151+
return (x, i, X) => {
152+
const f1 = format1(x, i); // always shown
153+
const f2 = format2(x, i); // only shown if different
154+
const j = i - orderof(X); // detect reversed domains
155+
return i !== j && X[j] !== undefined && f2 === format2(X[j], j) ? f1 : template(f1, f2);
156+
};
157+
}

test/output/aaplCandlestick.svg

Lines changed: 5 additions & 5 deletions
Loading

test/output/aaplVolumeRect.svg

Lines changed: 8 additions & 8 deletions
Loading

test/output/availability.svg

Lines changed: 6 additions & 6 deletions
Loading

test/output/bin1m.svg

Lines changed: 12 additions & 12 deletions
Loading

test/output/binTimestamps.svg

Lines changed: 8 additions & 8 deletions
Loading

test/output/clamp.svg

Lines changed: 9 additions & 9 deletions
Loading

0 commit comments

Comments
 (0)