Skip to content

Commit 1a05337

Browse files
committed
multi-line time format
1 parent 5e716de commit 1a05337

32 files changed

+4185
-221
lines changed

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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
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+
const formats = [
17+
["millisecond", 0.5 * durationSecond],
18+
["second", durationSecond],
19+
["second", 30 * durationSecond],
20+
["minute", durationMinute],
21+
["minute", 30 * durationMinute],
22+
["hour", durationHour],
23+
["hour", 12 * durationHour],
24+
["day", durationDay],
25+
["day", 2 * durationDay],
26+
["week", durationWeek],
27+
["month", durationMonth],
28+
["month", 3 * durationMonth],
29+
["year", durationYear]
30+
];
531

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

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)