|
| 1 | +import {bisector, extent, timeFormat, utcFormat} from "d3"; |
1 | 2 | import {utcSecond, utcMinute, utcHour, unixDay, utcWeek, utcMonth, utcYear} from "d3";
|
2 | 3 | import {utcMonday, utcTuesday, utcWednesday, utcThursday, utcFriday, utcSaturday, utcSunday} from "d3";
|
3 | 4 | import {timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear} from "d3";
|
4 | 5 | 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 | +]; |
5 | 31 |
|
6 | 32 | const timeIntervals = new Map([
|
7 | 33 | ["second", timeSecond],
|
@@ -82,3 +108,48 @@ export function isTimeYear(i) {
|
82 | 108 | const date = i.floor(new Date(2000, 11, 31));
|
83 | 109 | return timeYear(date) >= date; // coercing equality
|
84 | 110 | }
|
| 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 | +} |
0 commit comments