diff --git a/src/legends/swatches.js b/src/legends/swatches.js index 5685f5c9e3..e5876aa546 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -86,7 +86,7 @@ function legendItems(scale, options = {}, swatch) { } = options; const context = createContext(options); className = maybeClassName(className); - if (typeof tickFormat !== "function") tickFormat = inferTickFormat(scale.scale, scale.domain, undefined, tickFormat); + tickFormat = inferTickFormat(scale.scale, scale.domain, undefined, tickFormat); const swatches = create("div", context).attr( "class", diff --git a/src/marks/axis.js b/src/marks/axis.js index 28c5180d06..6a3d51f847 100644 --- a/src/marks/axis.js +++ b/src/marks/axis.js @@ -1,14 +1,14 @@ -import {extent, format, timeFormat, utcFormat} from "d3"; +import {InternSet, extent, format, utcFormat} 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, isInterval, orderof} from "../options.js"; import {maybeColorChannel, maybeNumberChannel, maybeRangeInterval} from "../options.js"; -import {isTemporalScale} from "../scales.js"; import {offset} from "../style.js"; -import {formatTimeTicks, isTimeYear, isUtcYear} from "../time.js"; +import {generalizeTimeInterval, inferTimeFormat, intervalDuration} from "../time.js"; import {initializer} from "../transforms/basic.js"; +import {warn} from "../warnings.js"; import {ruleX, ruleY} from "./rule.js"; import {text, textX, textY} from "./text.js"; import {vectorX, vectorY} from "./vector.js"; @@ -277,7 +277,7 @@ function axisTickKy( ...options } ) { - return axisMark(vectorY, k, `${k}-axis tick`, data, { + return axisMark(vectorY, k, anchor, `${k}-axis tick`, data, { strokeWidth, strokeLinecap, strokeLinejoin, @@ -311,7 +311,7 @@ function axisTickKx( ...options } ) { - return axisMark(vectorX, k, `${k}-axis tick`, data, { + return axisMark(vectorX, k, anchor, `${k}-axis tick`, data, { strokeWidth, strokeLinejoin, strokeLinecap, @@ -336,8 +336,7 @@ function axisTextKy( tickSize, tickRotate = 0, tickPadding = Math.max(3, 9 - tickSize) + (Math.abs(tickRotate) > 60 ? 4 * Math.cos(tickRotate * radians) : 0), - tickFormat, - text = typeof tickFormat === "function" ? tickFormat : undefined, + text, textAnchor = Math.abs(tickRotate) > 60 ? "middle" : anchor === "left" ? "end" : "start", lineAnchor = tickRotate > 60 ? "top" : tickRotate < -60 ? "bottom" : "middle", fontVariant, @@ -352,12 +351,13 @@ function axisTextKy( return axisMark( textY, k, + anchor, `${k}-axis tick label`, data, { facetAnchor, frameAnchor, - text: text === undefined ? null : text, + text, textAnchor, lineAnchor, fontVariant, @@ -366,7 +366,7 @@ function axisTextKy( ...options, dx: anchor === "left" ? +dx - tickSize - tickPadding + +insetLeft : +dx + +tickSize + +tickPadding - insetRight }, - function (scale, data, ticks, channels) { + function (scale, data, ticks, tickFormat, channels) { if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale); if (text === undefined) channels.text = inferTextChannel(scale, data, ticks, tickFormat, anchor); } @@ -383,8 +383,7 @@ function axisTextKx( tickSize, tickRotate = 0, tickPadding = Math.max(3, 9 - tickSize) + (Math.abs(tickRotate) >= 10 ? 4 * Math.cos(tickRotate * radians) : 0), - tickFormat, - text = typeof tickFormat === "function" ? tickFormat : undefined, + text, textAnchor = Math.abs(tickRotate) >= 10 ? ((tickRotate < 0) ^ (anchor === "bottom") ? "start" : "end") : "middle", lineAnchor = Math.abs(tickRotate) >= 10 ? "middle" : anchor === "bottom" ? "top" : "bottom", fontVariant, @@ -399,6 +398,7 @@ function axisTextKx( return axisMark( textX, k, + anchor, `${k}-axis tick label`, data, { @@ -413,7 +413,7 @@ function axisTextKx( ...options, dy: anchor === "bottom" ? +dy + +tickSize + +tickPadding - insetBottom : +dy - tickSize - tickPadding + +insetTop }, - function (scale, data, ticks, channels) { + function (scale, data, ticks, tickFormat, channels) { if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale); if (text === undefined) channels.text = inferTextChannel(scale, data, ticks, tickFormat, anchor); } @@ -452,7 +452,7 @@ function gridKy( ...options } ) { - return axisMark(ruleY, k, `${k}-grid`, data, {y, x1, x2, ...gridDefaults(options)}); + return axisMark(ruleY, k, anchor, `${k}-grid`, data, {y, x1, x2, ...gridDefaults(options)}); } function gridKx( @@ -467,7 +467,7 @@ function gridKx( ...options } ) { - return axisMark(ruleX, k, `${k}-grid`, data, {x, y1, y2, ...gridDefaults(options)}); + return axisMark(ruleX, k, anchor, `${k}-grid`, data, {x, y1, y2, ...gridDefaults(options)}); } function gridDefaults({ @@ -517,38 +517,75 @@ function labelOptions( }; } -function axisMark(mark, k, ariaLabel, data, options, initialize) { +function axisMark(mark, k, anchor, ariaLabel, data, options, initialize) { let channels; function axisInitializer(data, facets, _channels, scales, dimensions, context) { 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); + const domain = scale.domain(); + let {interval, ticks, tickFormat, tickSpacing = k === "x" ? 80 : 35} = options; + // For a scale with a temporal domain, also allow the ticks to be specified + // as a string which is promoted to a time interval. In the case of ordinal + // scales, the interval is interpreted as UTC. + if (typeof ticks === "string" && hasTemporalDomain(scale)) (interval = ticks), (ticks = undefined); + // The interval axis option is an alternative method of specifying ticks; + // for example, for a numeric scale, ticks = 5 means “about 5 ticks” whereas + // interval = 5 means “ticks every 5 units”. (This is not to be confused + // with the interval scale option, which affects the scale’s behavior!) + // Lastly use the tickSpacing option to infer the desired tick count. + if (ticks === undefined) ticks = maybeRangeInterval(interval, scale.type) ?? inferTickCount(scale, tickSpacing); if (data == null) { if (isIterable(ticks)) { + // Use explicit ticks, if specified. data = arrayify(ticks); - } else if (scale.ticks) { - if (ticks !== undefined) { - data = scale.ticks(ticks); + } else if (isInterval(ticks)) { + // Use the tick interval, if specified. + data = inclusiveRange(ticks, ...extent(domain)); + } else if (scale.interval) { + // If the scale interval is a standard time interval such as "day", we + // may be able to generalize the scale interval it to a larger aligned + // time interval to create the desired number of ticks. + let interval = scale.interval; + if (scale.ticks) { + const [min, max] = extent(domain); + const n = (max - min) / interval[intervalDuration]; // current tick count + // We don’t explicitly check that given interval is a time interval; + // in that case the generalized interval will be undefined, just like + // a nonstandard interval. TODO Generalize integer intervals, too. + interval = generalizeTimeInterval(interval, n / ticks) ?? interval; + data = inclusiveRange(interval, min, max); } else { - interval = maybeRangeInterval(interval === undefined ? scale.interval : interval, scale.type); - if (interval !== undefined) { - // For time scales, we could pass the interval directly to - // scale.ticks because it’s supported by d3.utcTicks; but - // quantitative scales and d3.ticks do not support numeric - // intervals for scale.ticks, so we compute them here. - const [min, max] = extent(scale.domain()); - 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); - data = scale.ticks(ticks); - } + data = domain; + const n = data.length; // current tick count + interval = generalizeTimeInterval(interval, n / ticks) ?? interval; + if (interval !== scale.interval) data = inclusiveRange(interval, ...extent(data)); + } + if (interval === scale.interval) { + // If we weren’t able to generalize the scale’s interval, compute the + // positive number n such that taking every nth value from the scale’s + // domain produces as close as possible to the desired number of + // ticks. For example, if the domain has 100 values and 5 ticks are + // desired, n = 20. + const n = Math.round(data.length / ticks); + if (n > 1) data = data.filter((d, i) => i % n === 0); } + } else if (scale.ticks) { + data = scale.ticks(ticks); } else { - data = scale.domain(); + // For ordinal scales, the domain will already be generated using the + // scale’s interval, if any. + data = domain; + } + if (!scale.ticks && data.length && data !== domain) { + // For ordinal scales, intersect the ticks with the scale domain since + // the scale is only defined on its domain. If all of the ticks are + // removed, then warn that the ticks and scale domain may be misaligned + // (e.g., "year" ticks and "4 weeks" interval). + const domainSet = new InternSet(domain); + data = data.filter((d) => domainSet.has(d)); + if (!data.length) warn(`Warning: the ${k}-axis ticks appear to not align with the scale domain, resulting in no ticks. Try different ticks?`); // prettier-ignore } if (k === "y" || k === "x") { facets = [range(data)]; @@ -556,7 +593,7 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) { channels[k] = {scale: k, value: identity}; } } - initialize?.call(this, scale, data, ticks, channels); + initialize?.call(this, scale, data, ticks, tickFormat, channels); const initializedChannels = Object.fromEntries( Object.entries(channels).map(([name, channel]) => { return [name, {...channel, value: valueof(data, channel.value)}]; @@ -580,29 +617,39 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) { return m; } +function inferTickCount(scale, tickSpacing) { + const [min, max] = extent(scale.range()); + return (max - min) / tickSpacing; +} + function inferTextChannel(scale, data, ticks, tickFormat, anchor) { return {value: inferTickFormat(scale, data, 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. +// interval to the ordinal scale), we want Plot’s default formatter. And for +// time ticks, we want to use the multi-line time format (e.g., Jan 26) if +// possible, or the default ISO format (2014-01-26). TODO We need a better way +// to infer whether the ordinal scale is UTC or local time. export function inferTickFormat(scale, data, ticks, tickFormat, anchor) { - return tickFormat === undefined && isTemporalScale(scale) - ? formatTimeTicks(scale, data, ticks, anchor) + return typeof tickFormat === "function" + ? tickFormat + : tickFormat === undefined && data && isTemporal(data) + ? inferTimeFormat(data, anchor) ?? formatDefault : scale.tickFormat - ? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat) + ? scale.tickFormat(typeof ticks === "number" ? ticks : null, tickFormat) : tickFormat === undefined - ? isUtcYear(scale.interval) - ? utcFormat("%Y") - : isTimeYear(scale.interval) - ? timeFormat("%Y") - : formatDefault + ? formatDefault : typeof tickFormat === "string" ? (isTemporal(scale.domain()) ? utcFormat : format)(tickFormat) : constant(tickFormat); } +function inclusiveRange(interval, min, max) { + return interval.range(min, interval.offset(interval.floor(max))); +} + const shapeTickBottom = { draw(context, l) { context.moveTo(0, 0); @@ -647,7 +694,7 @@ function inferScaleOrder(scale) { // Takes the scale label, and if this is not an ordinal scale and the label was // inferred from an associated channel, adds an orientation-appropriate arrow. function formatAxisLabel(k, scale, {anchor, label = scale.label, labelAnchor, labelArrow} = {}) { - if (label == null || (label.inferred && isTemporalish(scale) && /^(date|time|year)$/i.test(label))) return; + if (label == null || (label.inferred && hasTemporalDomain(scale) && /^(date|time|year)$/i.test(label))) return; label = String(label); // coerce to a string after checking if inferred if (labelArrow === "auto") labelArrow = (!scale.bandwidth || scale.interval) && !/[↑↓→←]/.test(label); if (!labelArrow) return label; @@ -684,6 +731,6 @@ function maybeLabelArrow(labelArrow = "auto") { : keyword(labelArrow, "labelArrow", ["auto", "up", "right", "down", "left"]); } -function isTemporalish(scale) { - return isTemporalScale(scale) || scale.interval != null; +function hasTemporalDomain(scale) { + return isTemporal(scale.domain()); } diff --git a/src/options.js b/src/options.js index 3fdcbd8bff..a391623c61 100644 --- a/src/options.js +++ b/src/options.js @@ -357,6 +357,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..fcfccc8980 100644 --- a/src/time.js +++ b/src/time.js @@ -1,4 +1,4 @@ -import {bisector, extent, median, pairs, timeFormat, utcFormat} from "d3"; +import {bisector, max, pairs, 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"; @@ -14,37 +14,76 @@ 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], +const tickIntervals = [ + ["millisecond", 1], + ["2 milliseconds", 2], + ["5 milliseconds", 5], + ["10 milliseconds", 10], + ["20 milliseconds", 20], + ["50 milliseconds", 50], + ["100 milliseconds", 100], + ["200 milliseconds", 200], + ["500 milliseconds", 500], ["second", durationSecond], - ["second", 30 * durationSecond], + ["5 seconds", 5 * durationSecond], + ["15 seconds", 15 * durationSecond], + ["30 seconds", 30 * durationSecond], ["minute", durationMinute], - ["minute", 30 * durationMinute], + ["5 minutes", 5 * durationMinute], + ["15 minutes", 15 * durationMinute], + ["30 minutes", 30 * durationMinute], ["hour", durationHour], - ["hour", 12 * durationHour], + ["3 hours", 3 * durationHour], + ["6 hours", 6 * durationHour], + ["12 hours", 12 * durationHour], ["day", durationDay], - ["day", 2 * durationDay], + ["2 days", 2 * durationDay], ["week", durationWeek], + ["2 weeks", 2 * durationWeek], // https://github.com/d3/d3-time/issues/46 ["month", durationMonth], - ["month", 3 * durationMonth], - ["year", durationYear] + ["3 months", 3 * durationMonth], + ["6 months", 6 * durationMonth], // https://github.com/d3/d3-time/issues/46 + ["year", durationYear], + ["2 years", 2 * durationYear], + ["5 years", 5 * durationYear], + ["10 years", 10 * durationYear], + ["20 years", 20 * durationYear], + ["50 years", 50 * durationYear], + ["100 years", 100 * durationYear] // TODO generalize to longer time scales ]; +const durations = new Map([ + ["second", durationSecond], + ["minute", durationMinute], + ["hour", durationHour], + ["day", durationDay], + ["monday", durationWeek], + ["tuesday", durationWeek], + ["wednesday", durationWeek], + ["thursday", durationWeek], + ["friday", durationWeek], + ["saturday", durationWeek], + ["sunday", durationWeek], + ["week", durationWeek], + ["month", durationMonth], + ["year", durationYear] +]); + const timeIntervals = new Map([ ["second", timeSecond], ["minute", timeMinute], ["hour", timeHour], - ["day", timeDay], // TODO local time equivalent of unixDay? - ["week", timeWeek], - ["month", timeMonth], - ["year", timeYear], + ["day", timeDay], // https://github.com/d3/d3-time/issues/62 ["monday", timeMonday], ["tuesday", timeTuesday], ["wednesday", timeWednesday], ["thursday", timeThursday], ["friday", timeFriday], ["saturday", timeSaturday], - ["sunday", timeSunday] + ["sunday", timeSunday], + ["week", timeWeek], + ["month", timeMonth], + ["year", timeYear] ]); const utcIntervals = new Map([ @@ -52,19 +91,58 @@ const utcIntervals = new Map([ ["minute", utcMinute], ["hour", utcHour], ["day", unixDay], - ["week", utcWeek], - ["month", utcMonth], - ["year", utcYear], ["monday", utcMonday], ["tuesday", utcTuesday], ["wednesday", utcWednesday], ["thursday", utcThursday], ["friday", utcFriday], ["saturday", utcSaturday], - ["sunday", utcSunday] + ["sunday", utcSunday], + ["week", utcWeek], + ["month", utcMonth], + ["year", utcYear] ]); -function parseInterval(input, intervals) { +// These hidden fields describe standard intervals so that we can, for example, +// generalize a scale’s time interval to a larger ticks time interval to reduce +// the number of displayed ticks. TODO We could instead allow the interval +// implementation to expose a “generalize” method that returns a larger, aligned +// interval; that would allow us to move this logic to D3, and allow +// generalization even when a custom interval is provided. +export const intervalDuration = Symbol("intervalDuration"); +export const intervalType = Symbol("intervalType"); + +// We greedily mutate D3’s standard intervals on load so that the hidden fields +// are available even if specified as e.g. d3.utcMonth instead of "month". +for (const [name, interval] of timeIntervals) { + interval[intervalDuration] = durations.get(name); + interval[intervalType] = "time"; +} +for (const [name, interval] of utcIntervals) { + interval[intervalDuration] = durations.get(name); + interval[intervalType] = "utc"; +} + +// An interleaved array of UTC and local time intervals, in descending order +// from largest to smallest, used to determine the most specific standard time +// format for a given array of dates. This is a subset of the tick intervals +// listed above; we only need the breakpoints where the format changes. +const formatIntervals = [ + ["year", utcYear, "utc"], + ["year", timeYear, "time"], + ["month", utcMonth, "utc"], + ["month", timeMonth, "time"], + ["day", unixDay, "utc", 6 * durationMonth], + ["day", timeDay, "time", 6 * durationMonth], + // Below day, local time typically has an hourly offset from UTC and hence the + // two are aligned and indistinguishable; therefore, we only consider UTC, and + // we don’t consider these if the domain only has a single value. + ["hour", utcHour, "utc", 3 * durationDay], + ["minute", utcMinute, "utc", 6 * durationHour], + ["second", utcSecond, "utc", 30 * durationMinute] +]; + +function parseInterval(input, intervals, type) { let name = `${input}`.toLowerCase(); if (name.endsWith("s")) name = name.slice(0, -1); // drop plural let period = 1; @@ -85,40 +163,38 @@ function parseInterval(input, intervals) { } let interval = intervals.get(name); if (!interval) throw new Error(`unknown interval: ${input}`); - if (!(period > 1)) return interval; - if (!interval.every) throw new Error(`non-periodic interval: ${name}`); - return interval.every(period); + if (period > 1) { + if (!interval.every) throw new Error(`non-periodic interval: ${name}`); + interval = interval.every(period); + interval[intervalDuration] = durations.get(name) * period; + interval[intervalType] = type; + } + return interval; } export function maybeTimeInterval(interval) { - return parseInterval(interval, timeIntervals); + return parseInterval(interval, timeIntervals, "time"); } export function maybeUtcInterval(interval) { - return parseInterval(interval, utcIntervals); -} - -export function isUtcYear(i) { - if (!i) return false; - const date = i.floor(new Date(Date.UTC(2000, 11, 31))); - return utcYear(date) >= date; // coercing equality + return parseInterval(interval, utcIntervals, "utc"); } -export function isTimeYear(i) { - if (!i) return false; - const date = i.floor(new Date(2000, 11, 31)); - return timeYear(date) >= date; // coercing equality +// If the given interval is a standard time interval, we may be able to promote +// it a larger aligned time interval, rather than showing every nth tick. +export function generalizeTimeInterval(interval, n) { + if (!(n > 1)) return; // no need to generalize + const duration = interval[intervalDuration]; + if (!tickIntervals.some(([, d]) => d === duration)) return; // nonstandard or unknown interval + if (duration % durationDay === 0 && durationDay < duration && duration < durationMonth) return; // not generalizable + const [i] = tickIntervals[bisector(([, step]) => Math.log(step)).center(tickIntervals, Math.log(duration * n))]; + return (interval[intervalType] === "time" ? maybeTimeInterval : maybeUtcInterval)(i); } -export function formatTimeTicks(scale, data, 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, data, ticks)) { +function formatTimeInterval(name, type, anchor) { + const format = type === "time" ? timeFormat : utcFormat; + const template = getTimeTemplate(anchor); + switch (name) { case "millisecond": return formatConditional(format(".%L"), format(":%M:%S"), template); case "second": @@ -129,8 +205,6 @@ export function formatTimeTicks(scale, data, ticks, anchor) { 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": @@ -139,18 +213,25 @@ export function formatTimeTicks(scale, data, ticks, anchor) { throw new Error("unable to format time ticks"); } -// Compute the median difference between adjacent ticks, ignoring repeated -// ticks; this implies an effective time interval, assuming that ticks are -// regularly spaced; choose the largest format less than this interval so that -// 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; - return formats[bisector(([, step]) => Math.log(step)).center(formats, Math.log(step))][0]; +function getTimeTemplate(anchor) { + return 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}`; +} + +// Given an array of dates, returns the largest compatible standard time +// interval. If no standard interval is compatible (other than milliseconds, +// which is universally compatible), returns undefined. +export function inferTimeFormat(dates, anchor) { + const step = max(pairs(dates, (a, b) => Math.abs(b - a))); // maybe undefined! + if (step < 1000) return formatTimeInterval("millisecond", "utc", anchor); + for (const [name, interval, type, maxStep] of formatIntervals) { + if (step > maxStep) break; // e.g., 52 weeks + if (name === "hour" && !step) break; // e.g., domain with a single date + if (dates.every((d) => interval.floor(d) >= d)) return formatTimeInterval(name, type, anchor); + } } function formatConditional(format1, format2, template) { 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/src/warnings.js b/src/warnings.js index 6c13e8db1e..0b06df538c 100644 --- a/src/warnings.js +++ b/src/warnings.js @@ -1,12 +1,16 @@ let warnings = 0; +let lastMessage; export function consumeWarnings() { const w = warnings; warnings = 0; + lastMessage = undefined; return w; } export function warn(message) { + if (message === lastMessage) return; + lastMessage = message; console.warn(message); ++warnings; } diff --git a/test/output/autoBarTimeSeries.svg b/test/output/autoBarTimeSeries.svg index 6a7dbd2ed1..b8ec70a104 100644 --- a/test/output/autoBarTimeSeries.svg +++ b/test/output/autoBarTimeSeries.svg @@ -51,15 +51,12 @@ - 2023-04-01 - 2023-04-05 - 2023-04-10 - 2023-04-15 - 2023-04-20 - 2023-04-25 - - - date + 1Apr + 5 + 10 + 15 + 20 + 25 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/boxplotFacetInterval.svg b/test/output/boxplotFacetInterval.svg index 054a9cf1e6..473440c09a 100644 --- a/test/output/boxplotFacetInterval.svg +++ b/test/output/boxplotFacetInterval.svg @@ -17,33 +17,18 @@ - - - - - - - - - - - - - - - @@ -52,33 +37,18 @@ 2.2 - - 2.1 - 2 - - 1.9 - 1.8 - - 1.7 - 1.6 - - 1.5 - 1.4 - - 1.3 - 1.2 diff --git a/test/output/boxplotFacetNegativeInterval.svg b/test/output/boxplotFacetNegativeInterval.svg index 054a9cf1e6..473440c09a 100644 --- a/test/output/boxplotFacetNegativeInterval.svg +++ b/test/output/boxplotFacetNegativeInterval.svg @@ -17,33 +17,18 @@ - - - - - - - - - - - - - - - @@ -52,33 +37,18 @@ 2.2 - - 2.1 - 2 - - 1.9 - 1.8 - - 1.7 - 1.6 - - 1.5 - 1.4 - - 1.3 - 1.2 diff --git a/test/output/downloadsOrdinal.svg b/test/output/downloadsOrdinal.svg index acdff086bb..ca2ae40de0 100644 --- a/test/output/downloadsOrdinal.svg +++ b/test/output/downloadsOrdinal.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/output/fruitSalesDate.svg b/test/output/fruitSalesDate.svg index 4425c34153..7d988241d2 100644 --- a/test/output/fruitSalesDate.svg +++ b/test/output/fruitSalesDate.svg @@ -39,11 +39,8 @@ - 2021-03-15 - 2021-03-16 - - - date + 15Mar + 16 diff --git a/test/output/fruitSalesSingleDate.svg b/test/output/fruitSalesSingleDate.svg new file mode 100644 index 0000000000..a610c90a99 --- /dev/null +++ b/test/output/fruitSalesSingleDate.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + + + ↑ units + + + + + + 15Mar + + + + + + + + apples + oranges + grapes + + \ No newline at end of file diff --git a/test/output/ibmTrading.svg b/test/output/ibmTrading.svg index acea2508f6..8275902e65 100644 --- a/test/output/ibmTrading.svg +++ b/test/output/ibmTrading.svg @@ -1,4 +1,4 @@ - + - - - - - - - - - - - - - - - 2018-04-16 - 2018-04-17 - 2018-04-18 - 2018-04-19 - 2018-04-20 - 2018-04-21 - 2018-04-22 - 2018-04-23 - 2018-04-24 - 2018-04-25 - 2018-04-26 - 2018-04-27 - 2018-04-28 - 2018-04-29 - 2018-04-30 - 2018-05-01 - 2018-05-02 - 2018-05-03 - 2018-05-04 - 2018-05-05 - 2018-05-06 - 2018-05-07 - 2018-05-08 - 2018-05-09 - 2018-05-10 - 2018-05-11 + + 17Apr + 19 + 21 + 23 + 25 + 27 + 29 + 1May + 3 + 5 + 7 + 9 + 11 diff --git a/test/output/intervalAwareBin.svg b/test/output/intervalAwareBin.svg index e7cfdef76f..2edc29437c 100644 --- a/test/output/intervalAwareBin.svg +++ b/test/output/intervalAwareBin.svg @@ -44,36 +44,22 @@ - - - - - - - 30 - 40 50 - 60 70 - 80 90 - 100 110 - 120 130 - 140 150 - 160 170 diff --git a/test/output/intervalAwareGroup.svg b/test/output/intervalAwareGroup.svg index b3d2848ca0..b1ee14d218 100644 --- a/test/output/intervalAwareGroup.svg +++ b/test/output/intervalAwareGroup.svg @@ -42,28 +42,18 @@ - - - - - 1950 - 1955 1960 - 1965 1970 - 1975 1980 - 1985 1990 - 1995 2000 diff --git a/test/output/intervalAwareStack.svg b/test/output/intervalAwareStack.svg index 3f7f2d8c80..f19c5032d6 100644 --- a/test/output/intervalAwareStack.svg +++ b/test/output/intervalAwareStack.svg @@ -39,28 +39,18 @@ - - - - - 1950 - 1955 1960 - 1965 1970 - 1975 1980 - 1985 1990 - 1995 2000 diff --git a/test/output/penguinNA3.svg b/test/output/penguinNA3.svg index 288a036ea3..484cffe8df 100644 --- a/test/output/penguinNA3.svg +++ b/test/output/penguinNA3.svg @@ -24,7 +24,6 @@ - 3,000 3,500 4,000 diff --git a/test/output/sparseCell.svg b/test/output/sparseCell.svg index 75a91cfb21..7dbf7cf39f 100644 --- a/test/output/sparseCell.svg +++ b/test/output/sparseCell.svg @@ -45,63 +45,35 @@ - - - - - - - - - - - - - - 1 - 2 3 - 4 5 - 6 7 - 8 9 - 10 11 - 12 13 - 14 15 - 16 17 - 18 19 - 20 21 - 22 23 - 24 25 - 26 27 - 28 ← Season @@ -132,50 +104,22 @@ - - - - - - - - - - - - - - 1 - 2 - 3 4 - 5 - 6 7 - 8 - 9 10 - 11 - 12 13 - 14 - 15 16 - 17 - 18 19 - 20 - 21 22 diff --git a/test/output/stargazersHourly.svg b/test/output/stargazersHourly.svg index fcf3e6e5c4..8f1660a95b 100644 --- a/test/output/stargazersHourly.svg +++ b/test/output/stargazersHourly.svg @@ -88,6 +88,7 @@ 8 9 10+ + New stargazers per hour → diff --git a/test/output/stargazersHourlyGroup.svg b/test/output/stargazersHourlyGroup.svg index 04891e747f..2c1bff38a1 100644 --- a/test/output/stargazersHourlyGroup.svg +++ b/test/output/stargazersHourlyGroup.svg @@ -88,6 +88,7 @@ 8 9 10+ + New stargazers per hour → diff --git a/test/output/timeAxisExplicitInterval.svg b/test/output/timeAxisExplicitInterval.svg index ec925189ec..64fa3febb9 100644 --- a/test/output/timeAxisExplicitInterval.svg +++ b/test/output/timeAxisExplicitInterval.svg @@ -40,61 +40,1293 @@ ↑ Close - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - Jul2013 - Oct - Jan2014 - Apr - Jul - Oct - Jan2015 - Apr - Jul - Oct - Jan2016 - Apr - Jul - Oct - Jan2017 - Apr - Jul - Oct - Jan2018 - Apr + Jul2013 + Jan2014 + Jul + Jan2015 + Jul + Jan2016 + Jul + Jan2017 + Jul + Jan2018 - - - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/timeAxisExplicitNonstandardInterval.svg b/test/output/timeAxisExplicitNonstandardInterval.svg new file mode 100644 index 0000000000..eae1469602 --- /dev/null +++ b/test/output/timeAxisExplicitNonstandardInterval.svg @@ -0,0 +1,1328 @@ + + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + 180 + + + ↑ Close + + + + + + + + + + + + + 2013-04-21 + 2013-12-29 + 2014-09-07 + 2015-05-17 + 2016-01-24 + 2016-10-02 + 2017-06-11 + 2018-02-18 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/timeAxisExplicitNonstandardIntervalTicks.svg b/test/output/timeAxisExplicitNonstandardIntervalTicks.svg new file mode 100644 index 0000000000..00c643c07b --- /dev/null +++ b/test/output/timeAxisExplicitNonstandardIntervalTicks.svg @@ -0,0 +1,1329 @@ + + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + 180 + + + ↑ Close + + + + + + + + + + + + + + + + + 2014 + 2015 + 2016 + 2017 + 2018 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/timeAxisOrdinal.svg b/test/output/timeAxisOrdinal.svg new file mode 100644 index 0000000000..131cbf4b34 --- /dev/null +++ b/test/output/timeAxisOrdinal.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + 180 + + + ↑ Close + + + + + + + + + + + + + + + Jul2013 + Jan2014 + Jul + Jan2015 + Jul + Jan2016 + Jul + Jan2017 + Jul + Jan2018 + + + 2013-05-13 + 2013-06-03 + 2013-07-01 + 2013-08-01 + 2013-09-03 + 2013-10-01 + 2013-11-01 + 2013-12-02 + 2014-01-02 + 2014-02-03 + 2014-03-03 + 2014-04-01 + 2014-05-01 + 2014-06-02 + 2014-07-01 + 2014-08-01 + 2014-09-02 + 2014-10-01 + 2014-11-03 + 2014-12-01 + 2015-01-02 + 2015-02-02 + 2015-03-02 + 2015-04-01 + 2015-05-01 + 2015-06-01 + 2015-07-01 + 2015-08-03 + 2015-09-01 + 2015-10-01 + 2015-11-02 + 2015-12-01 + 2016-01-04 + 2016-02-01 + 2016-03-01 + 2016-04-01 + 2016-05-02 + 2016-06-01 + 2016-07-01 + 2016-08-01 + 2016-09-01 + 2016-10-03 + 2016-11-01 + 2016-12-01 + 2017-01-03 + 2017-02-01 + 2017-03-01 + 2017-04-03 + 2017-05-01 + 2017-06-01 + 2017-07-03 + 2017-08-01 + 2017-09-01 + 2017-10-02 + 2017-11-01 + 2017-12-01 + 2018-01-02 + 2018-02-01 + 2018-03-01 + 2018-04-02 + 2018-05-01 + + \ No newline at end of file diff --git a/test/output/timeAxisOrdinalSparseInterval.svg b/test/output/timeAxisOrdinalSparseInterval.svg new file mode 100644 index 0000000000..70fcaad5e5 --- /dev/null +++ b/test/output/timeAxisOrdinalSparseInterval.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + + + ↑ Close + + + + + + + + + + + 2012-11-04 + 2013-11-03 + 2014-11-02 + 2015-11-01 + 2016-10-30 + 2017-10-29 + + + 2013-05-13 + 2013-11-04 + 2014-11-03 + 2015-11-02 + 2016-10-31 + 2017-10-30 + + \ No newline at end of file diff --git a/test/output/timeAxisOrdinalSparseTicks.svg b/test/output/timeAxisOrdinalSparseTicks.svg new file mode 100644 index 0000000000..a68625dfdc --- /dev/null +++ b/test/output/timeAxisOrdinalSparseTicks.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + + + ↑ Close + + + + + + + + + + 2013-11-03 + 2014-11-02 + 2015-11-01 + 2016-10-30 + 2017-10-29 + + + 2013-05-13 + 2013-05-20 + 2013-06-17 + 2013-07-15 + 2013-08-12 + 2013-09-09 + 2013-10-07 + 2013-11-04 + 2013-12-02 + 2013-12-30 + 2014-01-27 + 2014-02-24 + 2014-03-24 + 2014-04-21 + 2014-05-19 + 2014-06-16 + 2014-07-14 + 2014-08-11 + 2014-09-08 + 2014-10-06 + 2014-11-03 + 2014-12-01 + 2014-12-29 + 2015-01-26 + 2015-02-23 + 2015-03-23 + 2015-04-20 + 2015-05-18 + 2015-06-15 + 2015-07-13 + 2015-08-10 + 2015-09-08 + 2015-10-05 + 2015-11-02 + 2015-11-30 + 2015-12-28 + 2016-01-25 + 2016-02-22 + 2016-03-21 + 2016-04-18 + 2016-05-16 + 2016-06-13 + 2016-07-11 + 2016-08-08 + 2016-09-06 + 2016-10-03 + 2016-10-31 + 2016-11-28 + 2016-12-27 + 2017-01-23 + 2017-02-21 + 2017-03-20 + 2017-04-17 + 2017-05-15 + 2017-06-12 + 2017-07-10 + 2017-08-07 + 2017-09-05 + 2017-10-02 + 2017-10-30 + 2017-11-27 + 2017-12-26 + 2018-01-22 + 2018-02-20 + 2018-03-19 + 2018-04-16 + + \ No newline at end of file diff --git a/test/output/timeAxisOrdinalTicks.svg b/test/output/timeAxisOrdinalTicks.svg new file mode 100644 index 0000000000..e0628b2332 --- /dev/null +++ b/test/output/timeAxisOrdinalTicks.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + 180 + + + ↑ Close + + + + + + + + + + + + + + + + + + + + + + + + + Jul2013 + Oct + Jan2014 + Apr + Jul + Oct + Jan2015 + Apr + Jul + Oct + Jan2016 + Apr + Jul + Oct + Jan2017 + Apr + Jul + Oct + Jan2018 + Apr + + + 2013-05-13 + 2013-06-03 + 2013-07-01 + 2013-08-01 + 2013-09-03 + 2013-10-01 + 2013-11-01 + 2013-12-02 + 2014-01-02 + 2014-02-03 + 2014-03-03 + 2014-04-01 + 2014-05-01 + 2014-06-02 + 2014-07-01 + 2014-08-01 + 2014-09-02 + 2014-10-01 + 2014-11-03 + 2014-12-01 + 2015-01-02 + 2015-02-02 + 2015-03-02 + 2015-04-01 + 2015-05-01 + 2015-06-01 + 2015-07-01 + 2015-08-03 + 2015-09-01 + 2015-10-01 + 2015-11-02 + 2015-12-01 + 2016-01-04 + 2016-02-01 + 2016-03-01 + 2016-04-01 + 2016-05-02 + 2016-06-01 + 2016-07-01 + 2016-08-01 + 2016-09-01 + 2016-10-03 + 2016-11-01 + 2016-12-01 + 2017-01-03 + 2017-02-01 + 2017-03-01 + 2017-04-03 + 2017-05-01 + 2017-06-01 + 2017-07-03 + 2017-08-01 + 2017-09-01 + 2017-10-02 + 2017-11-01 + 2017-12-01 + 2018-01-02 + 2018-02-01 + 2018-03-01 + 2018-04-02 + 2018-05-01 + + \ No newline at end of file diff --git a/test/output/walmartsAdditions.svg b/test/output/walmartsAdditions.svg new file mode 100644 index 0000000000..c501455553 --- /dev/null +++ b/test/output/walmartsAdditions.svg @@ -0,0 +1,112 @@ + + + + + 2005— + + + 2000— + + + 1995— + + + 1990— + + + 1985— + + + 1980— + + + 1975— + + + 1970— + + + 1965— + + + 1960— + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/warnTimeAxisOrdinalExplicitIncompatibleTicks.svg b/test/output/warnTimeAxisOrdinalExplicitIncompatibleTicks.svg new file mode 100644 index 0000000000..d706820281 --- /dev/null +++ b/test/output/warnTimeAxisOrdinalExplicitIncompatibleTicks.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + + + ↑ Close + + + 2013-05-13 + 2013-05-20 + 2013-06-17 + 2013-07-15 + 2013-08-12 + 2013-09-09 + 2013-10-07 + 2013-11-04 + 2013-12-02 + 2013-12-30 + 2014-01-27 + 2014-02-24 + 2014-03-24 + 2014-04-21 + 2014-05-19 + 2014-06-16 + 2014-07-14 + 2014-08-11 + 2014-09-08 + 2014-10-06 + 2014-11-03 + 2014-12-01 + 2014-12-29 + 2015-01-26 + 2015-02-23 + 2015-03-23 + 2015-04-20 + 2015-05-18 + 2015-06-15 + 2015-07-13 + 2015-08-10 + 2015-09-08 + 2015-10-05 + 2015-11-02 + 2015-11-30 + 2015-12-28 + 2016-01-25 + 2016-02-22 + 2016-03-21 + 2016-04-18 + 2016-05-16 + 2016-06-13 + 2016-07-11 + 2016-08-08 + 2016-09-06 + 2016-10-03 + 2016-10-31 + 2016-11-28 + 2016-12-27 + 2017-01-23 + 2017-02-21 + 2017-03-20 + 2017-04-17 + 2017-05-15 + 2017-06-12 + 2017-07-10 + 2017-08-07 + 2017-09-05 + 2017-10-02 + 2017-10-30 + 2017-11-27 + 2017-12-26 + 2018-01-22 + 2018-02-20 + 2018-03-19 + 2018-04-16 + + ⚠️1 warning. Please check the console. + \ No newline at end of file diff --git a/test/output/warnTimeAxisOrdinalIncompatible.svg b/test/output/warnTimeAxisOrdinalIncompatible.svg new file mode 100644 index 0000000000..d706820281 --- /dev/null +++ b/test/output/warnTimeAxisOrdinalIncompatible.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + 160 + + + ↑ Close + + + 2013-05-13 + 2013-05-20 + 2013-06-17 + 2013-07-15 + 2013-08-12 + 2013-09-09 + 2013-10-07 + 2013-11-04 + 2013-12-02 + 2013-12-30 + 2014-01-27 + 2014-02-24 + 2014-03-24 + 2014-04-21 + 2014-05-19 + 2014-06-16 + 2014-07-14 + 2014-08-11 + 2014-09-08 + 2014-10-06 + 2014-11-03 + 2014-12-01 + 2014-12-29 + 2015-01-26 + 2015-02-23 + 2015-03-23 + 2015-04-20 + 2015-05-18 + 2015-06-15 + 2015-07-13 + 2015-08-10 + 2015-09-08 + 2015-10-05 + 2015-11-02 + 2015-11-30 + 2015-12-28 + 2016-01-25 + 2016-02-22 + 2016-03-21 + 2016-04-18 + 2016-05-16 + 2016-06-13 + 2016-07-11 + 2016-08-08 + 2016-09-06 + 2016-10-03 + 2016-10-31 + 2016-11-28 + 2016-12-27 + 2017-01-23 + 2017-02-21 + 2017-03-20 + 2017-04-17 + 2017-05-15 + 2017-06-12 + 2017-07-10 + 2017-08-07 + 2017-09-05 + 2017-10-02 + 2017-10-30 + 2017-11-27 + 2017-12-26 + 2018-01-22 + 2018-02-20 + 2018-03-19 + 2018-04-16 + + ⚠️1 warning. Please check the console. + \ No newline at end of file diff --git a/test/output/yearlyRequests.svg b/test/output/yearlyRequests.svg index 5ed80fd36d..0e0d182b1e 100644 --- a/test/output/yearlyRequests.svg +++ b/test/output/yearlyRequests.svg @@ -41,43 +41,25 @@ - - - - - - - - - 2002 - 2003 2004 - 2005 2006 - 2007 2008 - 2009 2010 - 2011 2012 - 2013 2014 - 2015 2016 - 2017 2018 - 2019 diff --git a/test/output/yearlyRequestsDate.svg b/test/output/yearlyRequestsDate.svg new file mode 100644 index 0000000000..0e0d182b1e --- /dev/null +++ b/test/output/yearlyRequestsDate.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + + + + + + + + + + + + + + 2002 + 2004 + 2006 + 2008 + 2010 + 2012 + 2014 + 2016 + 2018 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/yearlyRequestsLine.svg b/test/output/yearlyRequestsLine.svg index 9804ba1259..5770119d14 100644 --- a/test/output/yearlyRequestsLine.svg +++ b/test/output/yearlyRequestsLine.svg @@ -41,43 +41,19 @@ - - - - - - - - - - - - 2002 - 2003 - 2004 2005 - 2006 - 2007 2008 - 2009 - 2010 2011 - 2012 - 2013 2014 - 2015 - 2016 2017 - 2018 - 2019 diff --git a/test/plots/downloads-ordinal.ts b/test/plots/downloads-ordinal.ts index 3cadf0715b..dd0f6657a9 100644 --- a/test/plots/downloads-ordinal.ts +++ b/test/plots/downloads-ordinal.ts @@ -6,13 +6,7 @@ export async function downloadsOrdinal() { (d) => d.date.getUTCFullYear() === 2019 && d.date.getUTCMonth() <= 1 && d.downloads > 0 ); return Plot.plot({ - width: 960, - marginBottom: 55, - x: { - interval: "day", - tickRotate: -90, - tickFormat: "%b %d" - }, + x: {interval: "day"}, marks: [ Plot.barY(downloads, {x: "date", y: "downloads", fill: "#ccc"}), Plot.tickY(downloads, {x: "date", y: "downloads"}), diff --git a/test/plots/fruit-sales-date.ts b/test/plots/fruit-sales-date.ts index 0aabf16283..3961aa6ab9 100644 --- a/test/plots/fruit-sales-date.ts +++ b/test/plots/fruit-sales-date.ts @@ -13,3 +13,16 @@ export async function fruitSalesDate() { ] }); } + +export async function fruitSalesSingleDate() { + const sales = (await d3.csv("data/fruit-sales.csv", d3.autoType)).slice(0, 3); + return Plot.plot({ + x: { + type: "band" // treat dates as ordinal, not temporal + }, + marks: [ + Plot.barY(sales, Plot.stackY({x: "date", y: "units", fill: "fruit"})), + Plot.text(sales, Plot.stackY({x: "date", y: "units", text: "fruit"})) + ] + }); +} diff --git a/test/plots/ibm-trading.ts b/test/plots/ibm-trading.ts index 6eb507ae74..b276c930b2 100644 --- a/test/plots/ibm-trading.ts +++ b/test/plots/ibm-trading.ts @@ -4,12 +4,7 @@ import * as d3 from "d3"; export async function ibmTrading() { const ibm = await d3.csv("data/ibm.csv", d3.autoType).then((data) => data.slice(-20)); return Plot.plot({ - marginBottom: 65, - x: { - interval: "day", - tickRotate: -40, - label: null - }, + x: {interval: "day"}, y: { transform: (d) => d / 1e6, label: "Volume (USD, millions)", diff --git a/test/plots/integer-interval.ts b/test/plots/integer-interval.ts index 25ecb32175..76de1c5c99 100644 --- a/test/plots/integer-interval.ts +++ b/test/plots/integer-interval.ts @@ -8,12 +8,8 @@ export async function integerInterval() { [5, 12] ]; return Plot.plot({ - x: { - interval: 1 - }, - y: { - zero: true - }, + x: {interval: 1}, + y: {zero: true}, marks: [Plot.line(requests)] }); } diff --git a/test/plots/sparse-cell.ts b/test/plots/sparse-cell.ts index 68a8087417..ec9cbfd5c9 100644 --- a/test/plots/sparse-cell.ts +++ b/test/plots/sparse-cell.ts @@ -5,7 +5,7 @@ export async function sparseCell() { const simpsons = d3.sort(await d3.csv("data/simpsons.csv", d3.autoType), (d) => d.number_in_series); const data = [...simpsons.slice(0, 26), ...simpsons.slice(-10)]; return Plot.plot({ - grid: true, + grid: 20, padding: 0.05, x: { label: "Episode", diff --git a/test/plots/time-axis.ts b/test/plots/time-axis.ts index dcb19f7fb0..803d113b0d 100644 --- a/test/plots/time-axis.ts +++ b/test/plots/time-axis.ts @@ -79,6 +79,72 @@ export async function timeAxisRight() { export async function timeAxisExplicitInterval() { const aapl = await d3.csv("data/aapl.csv", d3.autoType); return Plot.plot({ - marks: [Plot.ruleY([0]), Plot.axisX({ticks: "3 months"}), Plot.gridX(), Plot.line(aapl, {x: "Date", y: "Close"})] + x: {interval: "month"}, + marks: [Plot.ruleY([0]), Plot.dot(aapl, {x: "Date", y: "Close"})] + }); +} + +export async function timeAxisExplicitNonstandardInterval() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "4 weeks"}, // does not align with months + marks: [Plot.ruleY([0]), Plot.dot(aapl, {x: "Date", y: "Close"})] + }); +} + +export async function timeAxisExplicitNonstandardIntervalTicks() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "4 weeks", grid: true, ticks: "year"}, // no years start on Sunday + marks: [Plot.ruleY([0]), Plot.dot(aapl, {x: "Date", y: "Close"})] + }); +} + +export async function timeAxisOrdinal() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "month"}, + marks: [Plot.barY(aapl, Plot.groupX({y: "median", title: "min"}, {title: "Date", x: "Date", y: "Close"}))] + }); +} + +export async function warnTimeAxisOrdinalIncompatible() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "4 weeks", ticks: "year"}, // ⚠️ no years start on Sunday + marks: [Plot.barY(aapl, Plot.groupX({y: "median", title: "min"}, {title: "Date", x: "Date", y: "Close"}))] + }); +} + +export async function timeAxisOrdinalSparseTicks() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "4 weeks", ticks: "52 weeks"}, + marks: [Plot.barY(aapl, Plot.groupX({y: "median", title: "min"}, {title: "Date", x: "Date", y: "Close"}))] + }); +} + +export async function timeAxisOrdinalSparseInterval() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "52 weeks"}, + marks: [Plot.barY(aapl, Plot.groupX({y: "median", title: "min"}, {title: "Date", x: "Date", y: "Close"}))] + }); +} + +export async function timeAxisOrdinalTicks() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + x: {interval: "month", ticks: "3 months"}, + marks: [Plot.barY(aapl, Plot.groupX({y: "median", title: "min"}, {title: "Date", x: "Date", y: "Close"}))] + }); +} + +export async function warnTimeAxisOrdinalExplicitIncompatibleTicks() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + const [start, stop] = d3.extent(aapl, (d) => d.Date); + return Plot.plot({ + x: {interval: "4 weeks", ticks: d3.utcYear.range(start, stop)}, // ⚠️ no years start on Sunday + marks: [Plot.barY(aapl, Plot.groupX({y: "median", title: "min"}, {title: "Date", x: "Date", y: "Close"}))] }); } diff --git a/test/plots/walmarts-decades.ts b/test/plots/walmarts-decades.ts index 3bec23210f..51ee6e52bb 100644 --- a/test/plots/walmarts-decades.ts +++ b/test/plots/walmarts-decades.ts @@ -38,3 +38,31 @@ export async function walmartsDecades() { ] }); } + +export async function walmartsAdditions() { + const [walmarts, statemesh] = await Promise.all([ + d3.tsv("data/walmarts.tsv", d3.autoType), + d3.json("data/us-counties-10m.json").then((us) => + mesh(us, { + type: "GeometryCollection", + geometries: us.objects.states.geometries.filter((d) => d.id !== "02" && d.id !== "15") + }) + ) + ]); + return Plot.plot({ + width: 200, + projection: "albers-usa", + fy: {interval: "5 years", axis: "right", tickFormat: "%Y—", reverse: true}, + marks: [ + Plot.geo(statemesh, {strokeOpacity: 0.25}), + Plot.raster(walmarts, { + pixelSize: 1.5, + imageRendering: "pixelated", + fy: "date", + x: "longitude", + y: "latitude", + fill: "date" + }) + ] + }); +} diff --git a/test/plots/yearly-requests.ts b/test/plots/yearly-requests.ts index 5fc05f5f7d..2a72b96a7d 100644 --- a/test/plots/yearly-requests.ts +++ b/test/plots/yearly-requests.ts @@ -1,26 +1,32 @@ import * as Plot from "@observablehq/plot"; +const requests = [ + [new Date("2002-01-01"), 9], + [new Date("2003-01-01"), 17], + [new Date("2004-01-01"), 12], + [new Date("2005-01-01"), 5], + [new Date("2006-01-01"), 12], + [new Date("2007-01-01"), 18], + [new Date("2008-01-01"), 16], + [new Date("2009-01-01"), 11], + [new Date("2010-01-01"), 9], + [new Date("2011-01-01"), 8], + [new Date("2012-01-01"), 9], + [new Date("2019-01-01"), 20] +]; + export async function yearlyRequests() { - const requests = [ - [2002, 9], - [2003, 17], - [2004, 12], - [2005, 5], - [2006, 12], - [2007, 18], - [2008, 16], - [2009, 11], - [2010, 9], - [2011, 8], - [2012, 9], - [2019, 20] - ]; return Plot.plot({ label: null, - x: { - interval: 1, - tickFormat: "" // TODO https://github.com/observablehq/plot/issues/768 - }, + x: {interval: 1, tickFormat: ""}, // TODO https://github.com/observablehq/plot/issues/768 + marks: [Plot.barY(requests, {x: ([date]) => date.getUTCFullYear(), y: "1"})] + }); +} + +export async function yearlyRequestsDate() { + return Plot.plot({ + label: null, + x: {interval: "year"}, marks: [Plot.barY(requests, {x: "0", y: "1"})] }); }