Skip to content

temporal tick format #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions src/axis.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {axisTop, axisBottom, axisRight, axisLeft, create} from "d3";
import {boolean, number, string, keyword, maybeKeyword} from "./mark.js";
import {axisTop, axisBottom, axisRight, axisLeft, create, format, utcFormat} from "d3";
import {formatIsoDate} from "./format.js";
import {boolean, number, string, keyword, maybeKeyword, constant} from "./mark.js";
import {isTemporal} from "./scales.js";

export class AxisX {
constructor({
Expand Down Expand Up @@ -46,10 +48,6 @@ export class AxisX {
) {
const {
axis,
ticks,
tickSize,
tickPadding,
tickFormat,
grid,
label,
labelAnchor,
Expand All @@ -61,13 +59,7 @@ export class AxisX {
const ty = offsetSign * offset + (axis === "top" ? marginTop : height - marginBottom);
return create("svg:g")
.attr("transform", `translate(0,${ty})`)
.call((axis === "top" ? axisTop : axisBottom)(x)
.ticks(Array.isArray(ticks) ? null : ticks, typeof tickFormat === "function" ? null : tickFormat)
.tickFormat(typeof tickFormat === "function" || !x.tickFormat ? tickFormat : null)
.tickSizeInner(tickSize)
.tickSizeOuter(0)
.tickPadding(tickPadding)
.tickValues(Array.isArray(ticks) ? ticks : null))
.call(createAxis(axis === "top" ? axisTop : axisBottom, x, this))
.call(maybeTickRotate, tickRotate)
.attr("font-size", null)
.attr("font-family", null)
Expand Down Expand Up @@ -134,10 +126,6 @@ export class AxisY {
) {
const {
axis,
ticks,
tickSize,
tickPadding,
tickFormat,
grid,
label,
labelAnchor,
Expand All @@ -149,13 +137,7 @@ export class AxisY {
const tx = offsetSign * offset + (axis === "right" ? width - marginRight : marginLeft);
return create("svg:g")
.attr("transform", `translate(${tx},0)`)
.call((axis === "right" ? axisRight : axisLeft)(y)
.ticks(Array.isArray(ticks) ? null : ticks, typeof tickFormat === "function" ? null : tickFormat)
.tickFormat(typeof tickFormat === "function" || !y.tickFormat ? tickFormat : null)
.tickSizeInner(tickSize)
.tickSizeOuter(0)
.tickPadding(tickPadding)
.tickValues(Array.isArray(ticks) ? ticks : null))
.call(createAxis(axis === "right" ? axisRight : axisLeft, y, this))
.call(maybeTickRotate, tickRotate)
.attr("font-size", null)
.attr("font-family", null)
Expand Down Expand Up @@ -213,6 +195,24 @@ function gridFacetY(fx, tx) {
.attr("d", fx.domain().map(v => `M${fx(v) + tx},0h${dx}`).join(""));
}

function createAxis(axis, scale, {ticks, tickSize, tickPadding, tickFormat}) {
if (!scale.tickFormat && typeof tickFormat !== "function") {
// 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.
tickFormat = tickFormat === undefined ? (isTemporal(scale.domain()) ? formatIsoDate : string)
: (typeof tickFormat === "string" ? (isTemporal(scale.domain()) ? utcFormat : format)
: constant)(tickFormat);
}
return axis(scale)
.ticks(Array.isArray(ticks) ? null : ticks, typeof tickFormat === "function" ? null : tickFormat)
.tickFormat(typeof tickFormat === "function" ? tickFormat : null)
.tickSizeInner(tickSize)
.tickSizeOuter(0)
.tickPadding(tickPadding)
.tickValues(Array.isArray(ticks) ? ticks : null);
}

function maybeTickRotate(g, rotate) {
if (!(rotate = +rotate)) return;
const radians = Math.PI / 180;
Expand Down
25 changes: 25 additions & 0 deletions src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,28 @@ export function formatWeekday(locale = "en-US", weekday = "short") {
}
};
}

export function formatIsoDate(date) {
if (isNaN(date)) return "Invalid Date";
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes();
const seconds = date.getUTCSeconds();
const milliseconds = date.getUTCMilliseconds();
return `${formatIsoYear(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}${
hours || minutes || seconds || milliseconds ? `T${pad(hours, 2)}:${pad(minutes, 2)}${
seconds || milliseconds ? `:${pad(seconds, 2)}${
milliseconds ? `.${pad(milliseconds, 3)}` : ``
}` : ``
}Z` : ``
}`;
}

function formatIsoYear(year) {
return year < 0 ? `-${pad(-year, 6)}`
: year > 9999 ? `+${pad(year, 6)}`
: pad(year, 4);
}

function pad(value, width) {
return (value + "").padStart(width, "0");
}
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export {map, mapX, mapY} from "./transforms/map.js";
export {windowX, windowY} from "./transforms/window.js";
export {selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js";
export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js";
export {formatWeekday, formatMonth} from "./format.js";
export {formatIsoDate, formatWeekday, formatMonth} from "./format.js";
1 change: 1 addition & 0 deletions src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const number = x => x == null ? x : +x;
export const boolean = x => x == null ? x : !!x;
export const first = d => d[0];
export const second = d => d[1];
export const constant = x => () => x;

// A few extra color keywords not known to d3-color.
const colors = new Set(["currentColor", "none"]);
Expand Down
4 changes: 2 additions & 2 deletions src/scales.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ function inferScaleType(key, channels, {type, domain, range}) {
return "linear";
}

function isOrdinal(values) {
export function isOrdinal(values) {
for (const value of values) {
if (value == null) continue;
const type = typeof value;
return type === "string" || type === "boolean";
}
}

function isTemporal(values) {
export function isTemporal(values) {
for (const value of values) {
if (value == null) continue;
return value instanceof Date;
Expand Down
2 changes: 1 addition & 1 deletion src/scales/quantitative.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ import {
} from "d3";
import {registry, radius, opacity, color} from "./index.js";
import {positive, negative} from "../defined.js";
import {constant} from "../mark.js";

const constant = x => () => x;
const flip = i => t => i(1 - t);

// TODO Allow this to be extended.
Expand Down
43 changes: 43 additions & 0 deletions test/output/fruitSalesDate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test/plots/crimean-war-overlapped.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default async function() {
const data = causes.flatMap(cause => crimea.map(({date, [cause]: deaths}) => ({date, cause, deaths})));
return Plot.plot({
x: {
tickFormat: d3.utcFormat("%b"),
tickFormat: "%b",
label: null
},
marks: [
Expand Down
2 changes: 1 addition & 1 deletion test/plots/crimean-war-stacked.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default async function() {
const data = causes.flatMap(cause => crimea.map(({date, [cause]: deaths}) => ({date, cause, deaths})));
return Plot.plot({
x: {
tickFormat: d3.utcFormat("%b"),
tickFormat: "%b",
label: null
},
marks: [
Expand Down
12 changes: 12 additions & 0 deletions test/plots/fruit-sales-date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";

export default async function() {
const sales = await d3.csv("data/fruit-sales.csv", d3.autoType);
return Plot.plot({
marks: [
Plot.barY(sales, Plot.stackY({x: "date", y: "units", fill: "fruit"})),
Plot.text(sales, Plot.stackY({x: "date", y: "units", text: "fruit" }))
]
});
}
1 change: 1 addition & 0 deletions test/plots/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {default as empty} from "./empty.js";
export {default as figcaption} from "./figcaption.js";
export {default as figcaptionHtml} from "./figcaption-html.js";
export {default as fruitSales} from "./fruit-sales.js";
export {default as fruitSalesDate} from "./fruit-sales-date.js";
export {default as gistempAnomaly} from "./gistemp-anomaly.js";
export {default as gistempAnomalyMoving} from "./gistemp-anomaly-moving.js";
export {default as hadcrutWarmingStripes} from "./hadcrut-warming-stripes.js";
Expand Down