diff --git a/src/marks/axis.js b/src/marks/axis.js
index 11984ca935..5d5c8190bd 100644
--- a/src/marks/axis.js
+++ b/src/marks/axis.js
@@ -1,11 +1,11 @@
-import {extent, format, timeFormat, utcFormat} from "d3";
+import {extent, format, timeFormat, timeTicks, utcFormat, utcTicks} from "d3";
import {formatDefault} from "../format.js";
import {marks} from "../mark.js";
import {radians} from "../math.js";
import {arrayify, constant, identity, keyword, number, range, valueof} from "../options.js";
-import {isIterable, isNoneish, isTemporal, orderof} from "../options.js";
+import {isIterable, isNoneish, isTemporal, isTimeInterval, orderof} from "../options.js";
import {maybeColorChannel, maybeNumberChannel, maybeRangeInterval} from "../options.js";
-import {isTemporalScale} from "../scales.js";
+import {isOrdinalScale, isTemporalScale} from "../scales.js";
import {offset} from "../style.js";
import {formatTimeTicks, isTimeYear, isUtcYear} from "../time.js";
import {initializer} from "../transforms/basic.js";
@@ -512,8 +512,10 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
const initializeFacets = data == null && (k === "fx" || k === "fy");
const {[k]: scale} = scales;
if (!scale) throw new Error(`missing scale: ${k}`);
- let {ticks, tickSpacing, interval} = options;
- if (isTemporalScale(scale) && typeof ticks === "string") (interval = ticks), (ticks = undefined);
+ let {ticks, tickSpacing = k === "x" ? 80 : 35, interval} = options;
+ // TODO what if ticks is a time interval implementation?
+ // TODO allow ticks to be a function?
+ if (isTemporalish(scale) && typeof ticks === "string") (interval = ticks), (ticks = undefined);
if (data == null) {
if (isIterable(ticks)) {
data = arrayify(ticks);
@@ -531,12 +533,30 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
data = interval.range(min, interval.offset(interval.floor(max))); // inclusive max
} else {
const [min, max] = extent(scale.range());
- ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing);
+ ticks = (max - min) / tickSpacing;
data = scale.ticks(ticks);
}
}
} else {
data = scale.domain();
+ // ordinal time
+ // TODO determine whether the interval is utc or local time?
+ // TODO assert that the interval is one of utcTick’s known intervals?
+ // TODO add ceil to the RangeIntervalImplementation interface?
+ if (isTimeInterval(scale.interval)) {
+ const type = "utc";
+ const [start, stop] = extent(data);
+ if (interval) {
+ data = maybeRangeInterval(interval, type).range(start, stop);
+ data = data.map(scale.interval.ceil ?? scale.interval.floor, scale.interval);
+ } else {
+ if (ticks === undefined) {
+ const [min, max] = extent(scale.range());
+ ticks = (max - min) / tickSpacing;
+ }
+ data = ticks < data.length ? (type === "time" ? timeTicks : utcTicks)(start, stop, ticks) : data;
+ }
+ }
}
if (k === "y" || k === "x") {
facets = [range(data)];
@@ -575,7 +595,7 @@ function inferTextChannel(scale, data, ticks, tickFormat, anchor) {
// 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, data, ticks, tickFormat, anchor) {
- return tickFormat === undefined && isTemporalScale(scale)
+ return tickFormat === undefined && isTemporalish(scale)
? formatTimeTicks(scale, data, ticks, anchor)
: scale.tickFormat
? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat)
@@ -672,5 +692,5 @@ function maybeLabelArrow(labelArrow = "auto") {
}
function isTemporalish(scale) {
- return isTemporalScale(scale) || scale.interval != null;
+ return isTemporalScale(scale) || (isOrdinalScale(scale) && isTimeInterval(scale.interval));
}
diff --git a/src/options.js b/src/options.js
index f70a118ef8..5565f02e9e 100644
--- a/src/options.js
+++ b/src/options.js
@@ -348,6 +348,14 @@ export function maybeNiceInterval(interval, type) {
return interval;
}
+export function isTimeInterval(t) {
+ return isInterval(t) && typeof t?.floor === "function" && t.floor() instanceof Date;
+}
+
+export function isInterval(t) {
+ return typeof t?.range === "function";
+}
+
// This distinguishes between per-dimension options and a standalone value.
export function maybeValue(value) {
return value === undefined || isOptions(value) ? value : {value};
diff --git a/src/time.js b/src/time.js
index 661a928184..4cf8fe84c2 100644
--- a/src/time.js
+++ b/src/time.js
@@ -110,7 +110,7 @@ export function isTimeYear(i) {
return timeYear(date) >= date; // coercing equality
}
-export function formatTimeTicks(scale, data, ticks, anchor) {
+export function formatTimeTicks(scale, data = scale.domain(), ticks, anchor) {
const format = scale.type === "time" ? timeFormat : utcFormat;
const template =
anchor === "left" || anchor === "right"
@@ -145,11 +145,12 @@ export function formatTimeTicks(scale, data, ticks, anchor) {
// the ticks show the field that is changing. If the ticks are not available,
// fallback to an approximation based on the desired number of ticks.
function getTimeTicksInterval(scale, data, ticks) {
- const medianStep = median(pairs(data, (a, b) => Math.abs(b - a) || NaN));
- if (medianStep > 0) return formats[bisector(([, step]) => step).right(formats, medianStep, 1, formats.length) - 1][0];
- const [start, stop] = extent(scale.domain());
- const count = typeof ticks === "number" ? ticks : 10;
- const step = Math.abs(stop - start) / count;
+ let step = median(pairs(data, (a, b) => Math.abs(b - a) || NaN));
+ if (!(step > 0)) {
+ const [start, stop] = extent(scale.domain());
+ const count = typeof ticks === "number" ? ticks : 10;
+ step = Math.abs(stop - start) / count;
+ }
return formats[bisector(([, step]) => Math.log(step)).center(formats, Math.log(step))][0];
}
diff --git a/src/transforms/bin.js b/src/transforms/bin.js
index f929abfedf..a33e9642d5 100644
--- a/src/transforms/bin.js
+++ b/src/transforms/bin.js
@@ -13,8 +13,10 @@ import {
coerceDate,
coerceNumbers,
identity,
+ isInterval,
isIterable,
isTemporal,
+ isTimeInterval,
labelof,
map,
maybeApplyInterval,
@@ -361,14 +363,6 @@ function isTimeThresholds(t) {
return isTimeInterval(t) || (isIterable(t) && isTemporal(t));
}
-function isTimeInterval(t) {
- return isInterval(t) && typeof t === "function" && t() instanceof Date;
-}
-
-function isInterval(t) {
- return typeof t?.range === "function";
-}
-
function bing(EX, EY) {
return EX && EY
? function* (I) {
diff --git a/test/output/bandClip2.svg b/test/output/bandClip2.svg
index 08d095600a..28d7962ade 100644
--- a/test/output/bandClip2.svg
+++ b/test/output/bandClip2.svg
@@ -72,12 +72,12 @@
- 2022-12-01
- 2022-12-02
- 2022-12-03
- 2022-12-04
- 2022-12-05
- 2022-12-06
+ 1Dec
+ 2
+ 3
+ 4
+ 5
+ 6
diff --git a/test/output/downloadsOrdinal.svg b/test/output/downloadsOrdinal.svg
index acdff086bb..57332d56bc 100644
--- a/test/output/downloadsOrdinal.svg
+++ b/test/output/downloadsOrdinal.svg
@@ -1,4 +1,4 @@
-