Skip to content

Commit 75a2dd2

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

26 files changed

+304
-220
lines changed

src/marks/axis.js

Lines changed: 8 additions & 6 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";
@@ -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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,39 @@
1+
import {bisector, extent, tickStep, 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+
// TODO prune because we don’t need steps?
17+
const formats = [
18+
["second", 1, durationSecond],
19+
["second", 5, 5 * durationSecond],
20+
["second", 15, 15 * durationSecond],
21+
["second", 30, 30 * durationSecond],
22+
["minute", 1, durationMinute],
23+
["minute", 5, 5 * durationMinute],
24+
["minute", 15, 15 * durationMinute],
25+
["minute", 30, 30 * durationMinute],
26+
["hour", 1, durationHour],
27+
["hour", 3, 3 * durationHour],
28+
["hour", 6, 6 * durationHour],
29+
["hour", 12, 12 * durationHour],
30+
["day", 1, durationDay],
31+
["day", 2, 2 * durationDay],
32+
["week", 1, durationWeek],
33+
["month", 1, durationMonth],
34+
["month", 3, 3 * durationMonth],
35+
["year", 1, durationYear]
36+
];
537

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

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)