|
| 1 | +import {bisector, extent, tickStep, 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 | +// 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 | +]; |
5 | 37 |
|
6 | 38 | const timeIntervals = new Map([
|
7 | 39 | ["second", timeSecond],
|
@@ -82,3 +114,53 @@ export function isTimeYear(i) {
|
82 | 114 | const date = i.floor(new Date(2000, 11, 31));
|
83 | 115 | return timeYear(date) >= date; // coercing equality
|
84 | 116 | }
|
| 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 | +} |
0 commit comments