diff --git a/src/axes.js b/src/axes.js
index 8ff27f4084..6dc4026230 100644
--- a/src/axes.js
+++ b/src/axes.js
@@ -1,22 +1,5 @@
-import {format, utcFormat} from "d3";
-import {formatIsoDate} from "./format.js";
-import {constant, isTemporal, string} from "./options.js";
import {isOrdinalScale} from "./scales.js";
export function inferFontVariant(scale) {
return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums";
}
-
-// D3 doesn’t provide a tick format for ordinal scales; we want shorthand when
-// an ordinal domain is numbers or dates, and we want null to mean the empty
-// string, not the default identity format. TODO Remove this in favor of the
-// axis mark’s inferTickFormat.
-export function maybeAutoTickFormat(tickFormat, domain) {
- return tickFormat === undefined
- ? isTemporal(domain)
- ? formatIsoDate
- : string
- : typeof tickFormat === "function"
- ? tickFormat
- : (typeof tickFormat === "string" ? (isTemporal(domain) ? utcFormat : format) : constant)(tickFormat);
-}
diff --git a/src/legends/swatches.js b/src/legends/swatches.js
index 560645377f..ac1a26e507 100644
--- a/src/legends/swatches.js
+++ b/src/legends/swatches.js
@@ -1,9 +1,10 @@
import {pathRound as path} from "d3";
-import {inferFontVariant, maybeAutoTickFormat} from "../axes.js";
-import {createContext, create} from "../context.js";
+import {inferFontVariant} from "../axes.js";
+import {create, createContext} from "../context.js";
import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js";
import {isOrdinalScale, isThresholdScale} from "../scales.js";
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";
+import {inferTickFormat} from "../marks/axis.js";
function maybeScale(scale, key) {
if (key == null) return key;
@@ -85,7 +86,7 @@ function legendItems(scale, options = {}, swatch) {
} = options;
const context = createContext(options);
className = maybeClassName(className);
- tickFormat = maybeAutoTickFormat(tickFormat, scale.domain);
+ if (typeof tickFormat !== "function") tickFormat = inferTickFormat(scale.scale, undefined, tickFormat);
const swatches = create("div", context).attr(
"class",
diff --git a/src/marks/axis.js b/src/marks/axis.js
index b10cc1188e..8b96182686 100644
--- a/src/marks/axis.js
+++ b/src/marks/axis.js
@@ -7,7 +7,7 @@ import {isIterable, isNoneish, isTemporal, orderof} from "../options.js";
import {maybeColorChannel, maybeNumberChannel, maybeRangeInterval} from "../options.js";
import {isTemporalScale} from "../scales.js";
import {offset} from "../style.js";
-import {isTimeYear, isUtcYear} from "../time.js";
+import {formatTimeTicks, isTimeYear, isUtcYear} from "../time.js";
import {initializer} from "../transforms/basic.js";
import {ruleX, ruleY} from "./rule.js";
import {text, textX, textY} from "./text.js";
@@ -368,7 +368,7 @@ function axisTextKy(
},
function (scale, ticks, channels) {
if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
- if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat);
+ if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat, anchor);
}
);
}
@@ -415,7 +415,7 @@ function axisTextKx(
},
function (scale, ticks, channels) {
if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
- if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat);
+ if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat, anchor);
}
);
}
@@ -565,15 +565,17 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
return m;
}
-function inferTextChannel(scale, ticks, tickFormat) {
- return {value: inferTickFormat(scale, ticks, tickFormat)};
+function inferTextChannel(scale, ticks, tickFormat, anchor) {
+ return {value: inferTickFormat(scale, ticks, tickFormat, anchor)};
}
// D3’s ordinal scales simply use toString by default, but if the ordinal scale
// domain (or ticks) are numbers or dates (say because we’re applying a time
// interval to the ordinal scale), we want Plot’s default formatter.
-export function inferTickFormat(scale, ticks, tickFormat) {
- return scale.tickFormat
+export function inferTickFormat(scale, ticks, tickFormat, anchor) {
+ return tickFormat === undefined && isTemporalScale(scale)
+ ? formatTimeTicks(scale, ticks, anchor)
+ : scale.tickFormat
? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat)
: tickFormat === undefined
? isUtcYear(scale.interval)
diff --git a/src/time.js b/src/time.js
index fb1cd0d4dc..3020309503 100644
--- a/src/time.js
+++ b/src/time.js
@@ -1,7 +1,34 @@
+import {bisector, extent, timeFormat, utcFormat} from "d3";
import {utcSecond, utcMinute, utcHour, unixDay, utcWeek, utcMonth, utcYear} from "d3";
import {utcMonday, utcTuesday, utcWednesday, utcThursday, utcFriday, utcSaturday, utcSunday} from "d3";
import {timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear} from "d3";
import {timeMonday, timeTuesday, timeWednesday, timeThursday, timeFriday, timeSaturday, timeSunday} from "d3";
+import {orderof} from "./options.js";
+
+const durationSecond = 1000;
+const durationMinute = durationSecond * 60;
+const durationHour = durationMinute * 60;
+const durationDay = durationHour * 24;
+const durationWeek = durationDay * 7;
+const durationMonth = durationDay * 30;
+const durationYear = durationDay * 365;
+
+// See https://github.com/d3/d3-time/blob/9e8dc940f38f78d7588aad68a54a25b1f0c2d97b/src/ticks.js#L14-L33
+const formats = [
+ ["millisecond", 0.5 * durationSecond],
+ ["second", durationSecond],
+ ["second", 30 * durationSecond],
+ ["minute", durationMinute],
+ ["minute", 30 * durationMinute],
+ ["hour", durationHour],
+ ["hour", 12 * durationHour],
+ ["day", durationDay],
+ ["day", 2 * durationDay],
+ ["week", durationWeek],
+ ["month", durationMonth],
+ ["month", 3 * durationMonth],
+ ["year", durationYear]
+];
const timeIntervals = new Map([
["second", timeSecond],
@@ -82,3 +109,49 @@ export function isTimeYear(i) {
const date = i.floor(new Date(2000, 11, 31));
return timeYear(date) >= date; // coercing equality
}
+
+export function formatTimeTicks(scale, ticks, anchor) {
+ const format = scale.type === "time" ? timeFormat : utcFormat;
+ const template =
+ anchor === "left" || anchor === "right"
+ ? (f1, f2) => `\n${f1}\n${f2}` // extra newline to keep f1 centered
+ : anchor === "top"
+ ? (f1, f2) => `${f2}\n${f1}`
+ : (f1, f2) => `${f1}\n${f2}`;
+ switch (getTimeTicksInterval(scale, ticks)) {
+ case "millisecond":
+ return formatConditional(format(".%L"), format(":%M:%S"), template);
+ case "second":
+ return formatConditional(format(":%S"), format("%-I:%M"), template);
+ case "minute":
+ return formatConditional(format("%-I:%M"), format("%p"), template);
+ case "hour":
+ return formatConditional(format("%-I %p"), format("%b %-d"), template);
+ case "day":
+ return formatConditional(format("%-d"), format("%b"), template);
+ case "week":
+ return formatConditional(format("%-d"), format("%b"), template);
+ case "month":
+ return formatConditional(format("%b"), format("%Y"), template);
+ case "year":
+ return format("%Y");
+ }
+ throw new Error("unable to format time ticks");
+}
+
+// See https://github.com/d3/d3-time/blob/9e8dc940f38f78d7588aad68a54a25b1f0c2d97b/src/ticks.js#L43-L50
+function getTimeTicksInterval(scale, ticks) {
+ const [start, stop] = extent(scale.domain());
+ const count = typeof ticks === "number" ? ticks : 10; // TODO detect ticks as time interval?
+ const step = Math.abs(stop - start) / count;
+ return formats[bisector(([, step]) => Math.log(step)).center(formats, Math.log(step))][0];
+}
+
+function formatConditional(format1, format2, template) {
+ return (x, i, X) => {
+ const f1 = format1(x, i); // always shown
+ const f2 = format2(x, i); // only shown if different
+ const j = i - orderof(X); // detect reversed domains
+ return i !== j && X[j] !== undefined && f2 === format2(X[j], j) ? f1 : template(f1, f2);
+ };
+}
diff --git a/test/output/aaplCandlestick.svg b/test/output/aaplCandlestick.svg
index 92e6ddda36..b60901d70f 100644
--- a/test/output/aaplCandlestick.svg
+++ b/test/output/aaplCandlestick.svg
@@ -63,11 +63,11 @@
- December
- 2018
- February
- March
- April
+ Dec2017
+ Jan2018
+ Feb
+ Mar
+ Apr
May
diff --git a/test/output/aaplVolumeRect.svg b/test/output/aaplVolumeRect.svg
index 42a109c0a3..85fb7accd7 100644
--- a/test/output/aaplVolumeRect.svg
+++ b/test/output/aaplVolumeRect.svg
@@ -75,14 +75,14 @@
- Mar 18
- Mar 25
- April
- Apr 08
- Apr 15
- Apr 22
- Apr 29
- May 06
+ 18Mar
+ 25
+ 1Apr
+ 8
+ 15
+ 22
+ 29
+ 6May
diff --git a/test/output/availability.svg b/test/output/availability.svg
index b153c1f0a3..fe2ff96428 100644
--- a/test/output/availability.svg
+++ b/test/output/availability.svg
@@ -41,12 +41,12 @@
- 2020
- April
- July
- October
- 2021
- April
+ Jan2020
+ Apr
+ Jul
+ Oct
+ Jan2021
+ Apr
diff --git a/test/output/bin1m.svg b/test/output/bin1m.svg
index 12449de3d3..96c219a75c 100644
--- a/test/output/bin1m.svg
+++ b/test/output/bin1m.svg
@@ -60,19 +60,19 @@
- 2020
- February
- March
- April
+ Jan2020
+ Feb
+ Mar
+ Apr
May
- June
- July
- August
- September
- October
- November
- December
- 2021
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
+ Jan2021
diff --git a/test/output/binTimestamps.svg b/test/output/binTimestamps.svg
index dab9e7e3fd..2870d55f6f 100644
--- a/test/output/binTimestamps.svg
+++ b/test/output/binTimestamps.svg
@@ -53,14 +53,14 @@
- 2021
- Sat 02
- Jan 03
- Mon 04
- Tue 05
- Wed 06
- Thu 07
- Fri 08
+ 1Jan
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
diff --git a/test/output/clamp.svg b/test/output/clamp.svg
index 0247b29d4f..b7d622255c 100644
--- a/test/output/clamp.svg
+++ b/test/output/clamp.svg
@@ -25,15 +25,15 @@
- 2006
- April
- July
- October
- 2007
- April
- July
- October
- 2008
+ Jan2006
+ Apr
+ Jul
+ Oct
+ Jan2007
+ Apr
+ Jul
+ Oct
+ Jan2008
diff --git a/test/output/covidIhmeProjectedDeaths.svg b/test/output/covidIhmeProjectedDeaths.svg
index d09f543845..c1838e9c5f 100644
--- a/test/output/covidIhmeProjectedDeaths.svg
+++ b/test/output/covidIhmeProjectedDeaths.svg
@@ -115,28 +115,6 @@
↑ Deaths per day to COVID-19 (projected)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -160,27 +138,49 @@
- 01March
- 08
+ 1Mar
+ 8
15
22
29
- 05April
+ 5Apr
12
19
26
- 03May
+ 3May
10
17
24
31
- 07June
+ 7Jun
14
21
28
- 05July
+ 5Jul
12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
cone of uncertainty
diff --git a/test/output/googleTrendsRidgeline.svg b/test/output/googleTrendsRidgeline.svg
index c8cee5c6e2..9684601e88 100644
--- a/test/output/googleTrendsRidgeline.svg
+++ b/test/output/googleTrendsRidgeline.svg
@@ -225,18 +225,18 @@
- 2020
- February
- March
- April
+ 2020Jan
+ Feb
+ Mar
+ Apr
May
- June
- July
- August
- September
- October
- November
- December
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
diff --git a/test/output/seattlePrecipitationSum.svg b/test/output/seattlePrecipitationSum.svg
index da4dfb089c..1585a8495c 100644
--- a/test/output/seattlePrecipitationSum.svg
+++ b/test/output/seattlePrecipitationSum.svg
@@ -62,17 +62,17 @@
- Fri 11
- Dec 13
- Tue 15
- Thu 17
- Sat 19
- Mon 21
- Wed 23
- Fri 25
- Dec 27
- Tue 29
- Thu 31
+ 11Dec
+ 13
+ 15
+ 17
+ 19
+ 21
+ 23
+ 25
+ 27
+ 29
+ 31
diff --git a/test/output/sfCovidDeaths.svg b/test/output/sfCovidDeaths.svg
index 54bb86183c..8ea55d9093 100644
--- a/test/output/sfCovidDeaths.svg
+++ b/test/output/sfCovidDeaths.svg
@@ -49,12 +49,12 @@
- April
- July
- October
- 2021
- April
- July
+ Apr2020
+ Jul
+ Oct
+ Jan2021
+ Apr
+ Jul
specimen_collection_date →
diff --git a/test/output/sfTemperatureBand.svg b/test/output/sfTemperatureBand.svg
index 5538505657..395e0ea167 100644
--- a/test/output/sfTemperatureBand.svg
+++ b/test/output/sfTemperatureBand.svg
@@ -61,15 +61,15 @@
- October
- 2011
- April
- July
- October
- 2012
- April
- July
- October
+ Oct2010
+ Jan2011
+ Apr
+ Jul
+ Oct
+ Jan2012
+ Apr
+ Jul
+ Oct
diff --git a/test/output/sfTemperatureBandArea.svg b/test/output/sfTemperatureBandArea.svg
index c109fd42ac..f55308e19a 100644
--- a/test/output/sfTemperatureBandArea.svg
+++ b/test/output/sfTemperatureBandArea.svg
@@ -79,15 +79,15 @@
- October
- 2011
- April
- July
- October
- 2012
- April
- July
- October
+ Oct2010
+ Jan2011
+ Apr
+ Jul
+ Oct
+ Jan2012
+ Apr
+ Jul
+ Oct
diff --git a/test/output/sfTemperatureWindow.svg b/test/output/sfTemperatureWindow.svg
index 9ba17a2d0e..32e1398581 100644
--- a/test/output/sfTemperatureWindow.svg
+++ b/test/output/sfTemperatureWindow.svg
@@ -73,15 +73,15 @@
- October
- 2011
- April
- July
- October
- 2012
- April
- July
- October
+ Oct2010
+ Jan2011
+ Apr
+ Jul
+ Oct
+ Jan2012
+ Apr
+ Jul
+ Oct
diff --git a/test/output/shorthandArea.svg b/test/output/shorthandArea.svg
index ad3565ece5..016d698ab2 100644
--- a/test/output/shorthandArea.svg
+++ b/test/output/shorthandArea.svg
@@ -46,14 +46,14 @@
- Jan 07
- Jan 14
- Jan 21
- Jan 28
- Feb 04
- Feb 11
- Feb 18
- Feb 25
+ 7Jan
+ 14
+ 21
+ 28
+ 4Feb
+ 11
+ 18
+ 25
diff --git a/test/output/shorthandDot.svg b/test/output/shorthandDot.svg
index f9548f1faf..2b8dfb76a4 100644
--- a/test/output/shorthandDot.svg
+++ b/test/output/shorthandDot.svg
@@ -52,14 +52,14 @@
- Jan 07
- Jan 14
- Jan 21
- Jan 28
- Feb 04
- Feb 11
- Feb 18
- Feb 25
+ 7Jan
+ 14
+ 21
+ 28
+ 4Feb
+ 11
+ 18
+ 25
diff --git a/test/output/shorthandLine.svg b/test/output/shorthandLine.svg
index ccfa5c0ed0..967e97d799 100644
--- a/test/output/shorthandLine.svg
+++ b/test/output/shorthandLine.svg
@@ -52,14 +52,14 @@
- Jan 07
- Jan 14
- Jan 21
- Jan 28
- Feb 04
- Feb 11
- Feb 18
- Feb 25
+ 7Jan
+ 14
+ 21
+ 28
+ 4Feb
+ 11
+ 18
+ 25
diff --git a/test/output/shorthandText.svg b/test/output/shorthandText.svg
index 7e8b1a4bbe..7b9a6c043b 100644
--- a/test/output/shorthandText.svg
+++ b/test/output/shorthandText.svg
@@ -52,14 +52,14 @@
- Jan 07
- Jan 14
- Jan 21
- Jan 28
- Feb 04
- Feb 11
- Feb 18
- Feb 25
+ 7Jan
+ 14
+ 21
+ 28
+ 4Feb
+ 11
+ 18
+ 25
0
diff --git a/test/output/shorthandVector.svg b/test/output/shorthandVector.svg
index e96509c09e..a0be321182 100644
--- a/test/output/shorthandVector.svg
+++ b/test/output/shorthandVector.svg
@@ -52,14 +52,14 @@
- Jan 07
- Jan 14
- Jan 21
- Jan 28
- Feb 04
- Feb 11
- Feb 18
- Feb 25
+ 7Jan
+ 14
+ 21
+ 28
+ 4Feb
+ 11
+ 18
+ 25
diff --git a/test/output/stargazers.svg b/test/output/stargazers.svg
index d649ab397c..545faf5c44 100644
--- a/test/output/stargazers.svg
+++ b/test/output/stargazers.svg
@@ -65,13 +65,13 @@
- December
- 2021
- February
- March
- April
+ Dec2020
+ Jan2021
+ Feb
+ Mar
+ Apr
May
- June
+ Jun
diff --git a/test/output/stargazersBinned.svg b/test/output/stargazersBinned.svg
index b85fbac89e..66b295ca41 100644
--- a/test/output/stargazersBinned.svg
+++ b/test/output/stargazersBinned.svg
@@ -72,14 +72,14 @@
- November
- December
- 2021
- February
- March
- April
+ Nov2020
+ Dec
+ Jan2021
+ Feb
+ Mar
+ Apr
May
- June
+ Jun
2020-11-01 to 2020-11-08
diff --git a/test/output/timeAxisBottom.svg b/test/output/timeAxisBottom.svg
new file mode 100644
index 0000000000..9a4d080945
--- /dev/null
+++ b/test/output/timeAxisBottom.svg
@@ -0,0 +1,1386 @@
+
\ No newline at end of file
diff --git a/test/output/timeAxisLeft.svg b/test/output/timeAxisLeft.svg
new file mode 100644
index 0000000000..868e69fa0a
--- /dev/null
+++ b/test/output/timeAxisLeft.svg
@@ -0,0 +1,521 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .000:00:00
+ .100
+ .200
+ .300
+ .400
+ .500
+ .600
+ .700
+ .800
+ .900
+ .000:00:01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :0012:00
+ :05
+ :10
+ :15
+ :20
+ :25
+ :30
+ :35
+ :40
+ :45
+ :50
+ :55
+ :0012:01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00AM
+ 12:01
+ 12:02
+ 12:03
+ 12:04
+ 12:05
+ 12:06
+ 12:07
+ 12:08
+ 12:09
+ 12:10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00AM
+ 12:30
+ 1:00
+ 1:30
+ 2:00
+ 2:30
+ 3:00
+ 3:30
+ 4:00
+ 4:30
+ 5:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12 AMJan 1
+ 3 AM
+ 6 AM
+ 9 AM
+ 12 PM
+ 3 PM
+ 6 PM
+ 9 PM
+ 12 AMJan 2
+ 3 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12 AMJan 5
+ 12 PM
+ 12 AMJan 6
+ 12 PM
+ 12 AMJan 7
+ 12 PM
+ 12 AMJan 8
+ 12 PM
+ 12 AMJan 9
+ 12 PM
+ 12 AMJan 10
+ 12 PM
+ 12 AMJan 11
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17Jan
+ 19
+ 21
+ 23
+ 25
+ 27
+ 29
+ 31
+ 2Feb
+ 4
+ 6
+ 8
+ 10
+ 12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Apr2023
+ May
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
+ Jan2024
+ Feb
+ Mar
+ Apr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2020
+ 2025
+ 2030
+ 2035
+ 2040
+ 2045
+ 2050
+ 2055
+ 2060
+ 2065
+ 2070
+
+
+
\ No newline at end of file
diff --git a/test/output/timeAxisRight.svg b/test/output/timeAxisRight.svg
new file mode 100644
index 0000000000..c745687eb1
--- /dev/null
+++ b/test/output/timeAxisRight.svg
@@ -0,0 +1,521 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .000:00:00
+ .100
+ .200
+ .300
+ .400
+ .500
+ .600
+ .700
+ .800
+ .900
+ .000:00:01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :0012:00
+ :05
+ :10
+ :15
+ :20
+ :25
+ :30
+ :35
+ :40
+ :45
+ :50
+ :55
+ :0012:01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00AM
+ 12:01
+ 12:02
+ 12:03
+ 12:04
+ 12:05
+ 12:06
+ 12:07
+ 12:08
+ 12:09
+ 12:10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00AM
+ 12:30
+ 1:00
+ 1:30
+ 2:00
+ 2:30
+ 3:00
+ 3:30
+ 4:00
+ 4:30
+ 5:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12 AMJan 1
+ 3 AM
+ 6 AM
+ 9 AM
+ 12 PM
+ 3 PM
+ 6 PM
+ 9 PM
+ 12 AMJan 2
+ 3 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12 AMJan 5
+ 12 PM
+ 12 AMJan 6
+ 12 PM
+ 12 AMJan 7
+ 12 PM
+ 12 AMJan 8
+ 12 PM
+ 12 AMJan 9
+ 12 PM
+ 12 AMJan 10
+ 12 PM
+ 12 AMJan 11
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17Jan
+ 19
+ 21
+ 23
+ 25
+ 27
+ 29
+ 31
+ 2Feb
+ 4
+ 6
+ 8
+ 10
+ 12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Apr2023
+ May
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
+ Jan2024
+ Feb
+ Mar
+ Apr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2020
+ 2025
+ 2030
+ 2035
+ 2040
+ 2045
+ 2050
+ 2055
+ 2060
+ 2065
+ 2070
+
+
+
\ No newline at end of file
diff --git a/test/output/timeAxisTop.svg b/test/output/timeAxisTop.svg
new file mode 100644
index 0000000000..07030bbaee
--- /dev/null
+++ b/test/output/timeAxisTop.svg
@@ -0,0 +1,1386 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :00:00.000
+ .100
+ .200
+ .300
+ .400
+ .500
+ .600
+ .700
+ .800
+ .900
+ :00:01.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :00:00.000
+ .500
+ :00:01.000
+ .500
+ :00:02.000
+ .500
+ :00:03.000
+ .500
+ :00:04.000
+ .500
+ :00:05.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00:00
+ :01
+ :02
+ :03
+ :04
+ :05
+ :06
+ :07
+ :08
+ :09
+ :10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00:00
+ :05
+ :10
+ :15
+ :20
+ :25
+ :30
+ :35
+ :40
+ :45
+ :50
+ :55
+ 12:01:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00:00
+ :15
+ :30
+ :45
+ 12:01:00
+ :15
+ :30
+ :45
+ 12:02:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12:00:00
+ :30
+ 12:01:00
+ :30
+ 12:02:00
+ :30
+ 12:03:00
+ :30
+ 12:04:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AM12:00
+ 12:01
+ 12:02
+ 12:03
+ 12:04
+ 12:05
+ 12:06
+ 12:07
+ 12:08
+ 12:09
+ 12:10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AM12:00
+ 12:05
+ 12:10
+ 12:15
+ 12:20
+ 12:25
+ 12:30
+ 12:35
+ 12:40
+ 12:45
+ 12:50
+ 12:55
+ 1:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AM12:00
+ 12:15
+ 12:30
+ 12:45
+ 1:00
+ 1:15
+ 1:30
+ 1:45
+ 2:00
+ 2:15
+ 2:30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AM12:00
+ 12:30
+ 1:00
+ 1:30
+ 2:00
+ 2:30
+ 3:00
+ 3:30
+ 4:00
+ 4:30
+ 5:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AM10:00
+ 10:30
+ 11:00
+ 11:30
+ PM12:00
+ 12:30
+ 1:00
+ 1:30
+ 2:00
+ 2:30
+ 3:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan 112 AM
+ 1 AM
+ 2 AM
+ 3 AM
+ 4 AM
+ 5 AM
+ 6 AM
+ 7 AM
+ 8 AM
+ 9 AM
+ 10 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan 112 AM
+ 3 AM
+ 6 AM
+ 9 AM
+ 12 PM
+ 3 PM
+ 6 PM
+ 9 PM
+ Jan 212 AM
+ 3 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan 112 AM
+ 6 AM
+ 12 PM
+ 6 PM
+ Jan 212 AM
+ 6 AM
+ 12 PM
+ 6 PM
+ Jan 312 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan 112 AM
+ 12 PM
+ Jan 212 AM
+ 12 PM
+ Jan 312 AM
+ 12 PM
+ Jan 412 AM
+ 12 PM
+ Jan 512 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan1
+ 3
+ 5
+ 7
+ 9
+ 11
+ 13
+ 15
+ 17
+ 19
+ 21
+ 23
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan17
+ 19
+ 21
+ 23
+ 25
+ 27
+ 29
+ 31
+ Feb2
+ 4
+ 6
+ 8
+ 10
+ 12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jan1
+ 8
+ 15
+ 22
+ 29
+ Feb5
+ 12
+ 19
+ 26
+ Mar5
+ 12
+ 19
+ 26
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2023Jan
+ Feb
+ Mar
+ Apr
+ May
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
+ 2024Jan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2023Apr
+ May
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
+ 2024Jan
+ Feb
+ Mar
+ Apr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2023Jan
+ Apr
+ Jul
+ Oct
+ 2024Jan
+ Apr
+ Jul
+ Oct
+ 2025Jan
+ Apr
+ Jul
+ Oct
+ 2026Jan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2023
+ 2024
+ 2025
+ 2026
+ 2027
+ 2028
+ 2029
+ 2030
+ 2031
+ 2032
+ 2033
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2020
+ 2025
+ 2030
+ 2035
+ 2040
+ 2045
+ 2050
+ 2055
+ 2060
+ 2065
+ 2070
+
+
+
\ No newline at end of file
diff --git a/test/output/trafficHorizon.html b/test/output/trafficHorizon.html
index bb11d9b198..dc2d132b0f 100644
--- a/test/output/trafficHorizon.html
+++ b/test/output/trafficHorizon.html
@@ -68,17 +68,17 @@
- Mon 04
+ Jan 412 AM
12 PM
- Tue 05
+ Jan 512 AM
12 PM
- Wed 06
+ Jan 612 AM
12 PM
- Thu 07
+ Jan 712 AM
12 PM
- Fri 08
+ Jan 812 AM
12 PM
- Sat 09
+ Jan 912 AM
12 PM
diff --git a/test/output/travelersCovidDrop.svg b/test/output/travelersCovidDrop.svg
index bb821a9386..bb5c75e408 100644
--- a/test/output/travelersCovidDrop.svg
+++ b/test/output/travelersCovidDrop.svg
@@ -65,16 +65,16 @@
- March
- April
+ Mar2020
+ Apr
May
- June
- July
- August
- September
- October
- November
- December
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
diff --git a/test/output/travelersYearOverYear.svg b/test/output/travelersYearOverYear.svg
index df259a5e29..8ec3bbcab5 100644
--- a/test/output/travelersYearOverYear.svg
+++ b/test/output/travelersYearOverYear.svg
@@ -83,16 +83,16 @@
- March
- April
+ Mar2020
+ Apr
May
- June
- July
- August
- September
- October
- November
- December
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
diff --git a/test/plots/covid-ihme-projected-deaths.ts b/test/plots/covid-ihme-projected-deaths.ts
index 13657b047c..b8bfd20552 100644
--- a/test/plots/covid-ihme-projected-deaths.ts
+++ b/test/plots/covid-ihme-projected-deaths.ts
@@ -15,12 +15,6 @@ export async function covidIhmeProjectedDeaths() {
},
marks: [
Plot.gridX(),
- Plot.axisX({
- tickFormat: (
- (fm, fd) => (x, i) =>
- i === 0 || d3.utcDay.count(d3.utcMonth(x), x) < 7 ? `${fd(x)}\n${fm(x)}` : fd(x)
- )(d3.utcFormat("%B"), d3.utcFormat("%d"))
- }),
Plot.areaY(data, {
x: "date",
y1: (d) => Math.max(1, d.lower), // avoid zero
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 5bfd5b464f..bba13661b0 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -283,6 +283,7 @@ export * from "./stargazers.js";
export * from "./stocks-index.js";
export * from "./text-overflow.js";
export * from "./this-is-just-to-say.js";
+export * from "./time-axis.js";
export * from "./tip.js";
export * from "./traffic-horizon.js";
export * from "./travelers-covid-drop.js";
diff --git a/test/plots/time-axis.ts b/test/plots/time-axis.ts
new file mode 100644
index 0000000000..264bda2175
--- /dev/null
+++ b/test/plots/time-axis.ts
@@ -0,0 +1,76 @@
+import * as Plot from "@observablehq/plot";
+import {svg} from "htl";
+
+const domains = [
+ [new Date("2023-01-01"), new Date("2023-01-01T00:00:01Z")], // 100ms
+ [new Date("2023-01-01"), new Date("2023-01-01T00:00:05Z")], // 500ms
+ [new Date("2023-01-01"), new Date("2023-01-01T00:00:10Z")], // 1s
+ [new Date("2023-01-01"), new Date("2023-01-01T00:01Z")], // 5s
+ [new Date("2023-01-01"), new Date("2023-01-01T00:02Z")], // 15s
+ [new Date("2023-01-01"), new Date("2023-01-01T00:04Z")], // 30s
+ [new Date("2023-01-01"), new Date("2023-01-01T00:10Z")], // 1m
+ [new Date("2023-01-01"), new Date("2023-01-01T01:00Z")], // 5m
+ [new Date("2023-01-01"), new Date("2023-01-01T02:30Z")], // 15m
+ [new Date("2023-01-01"), new Date("2023-01-01T05:00Z")], // 30m
+ [new Date("2023-01-01T10:00Z"), new Date("2023-01-01T15:00Z")], // 30m
+ [new Date("2023-01-01"), new Date("2023-01-01T10:00Z")], // 1h
+ [new Date("2023-01-01"), new Date("2023-01-02T05:00Z")], // 3h
+ [new Date("2023-01-01"), new Date("2023-01-03")], // 6h
+ [new Date("2023-01-01"), new Date("2023-01-05")], // 12h
+ [new Date("2023-01-05"), new Date("2023-01-11")], // 1d
+ [new Date("2023-01-01"), new Date("2023-01-11")], // 1d
+ [new Date("2023-01-01"), new Date("2023-01-23")], // 2d
+ [new Date("2023-01-17"), new Date("2023-02-13")], // 2d
+ [new Date("2023-01-01"), new Date("2023-04-01")], // 1w
+ [new Date("2023-01-01"), new Date("2024-01-01")], // 1m
+ [new Date("2023-04-01"), new Date("2024-04-01")], // 1m
+ [new Date("2023-01-01"), new Date("2026-01-01")], // 3m
+ [new Date("2023-01-01"), new Date("2033-01-01")], // 1y
+ [new Date("2020-01-01"), new Date("2070-01-01")] // 5y
+];
+
+export async function timeAxisBottom() {
+ return svg`${domains.map(
+ (domain, i) =>
+ svg`${Plot.plot({
+ marginBottom: 40,
+ height: 60,
+ x: {axis: "bottom", grid: true, type: "utc", domain}
+ })}`
+ )}`;
+}
+
+export async function timeAxisTop() {
+ return svg`${domains.map(
+ (domain, i) =>
+ svg`${Plot.plot({
+ marginTop: 40,
+ height: 60,
+ x: {axis: "top", grid: true, type: "utc", domain}
+ })}`
+ )}`;
+}
+
+export async function timeAxisLeft() {
+ const somedomains = domains.filter((d, i) => i % 3 === 0);
+ return svg`${somedomains.map(
+ (domain, i) =>
+ svg`${Plot.plot({
+ marginLeft: 60,
+ width: 80,
+ y: {grid: true, axis: "left", type: "utc", domain}
+ })}`
+ )}`;
+}
+
+export async function timeAxisRight() {
+ const somedomains = domains.filter((d, i) => i % 3 === 0);
+ return svg`${somedomains.map(
+ (domain, i) =>
+ svg`${Plot.plot({
+ marginRight: 60,
+ width: 80,
+ y: {grid: true, axis: "right", type: "utc", domain}
+ })}`
+ )}`;
+}