Skip to content

Calendar ​

Partitions of the time line into calendar cells: seconds, minutes, hours, days, days of the week, weeks, months, quarters and years, over a time column (see Schema.time). A Calendar value has two uses. It bins a time column's rows into cells, as the partition of field(name).bin(...). And it names a level of an axis, an entry of the levels of an axis in the axes option of chart: a time axis draws one row of labels per level.

ts
import { chart, line, scatter } from "gofish-graphics";

const readings = (() => {
  const start = Date.UTC(2024, 1, 28);
  const out: { time: Date; temp: number }[] = [];
  for (let h = 0; h <= 84; h++) {
    const temp =
      4 - 5 * Math.cos(((h - 3) / 24) * 2 * Math.PI) + 0.4 * Math.sin(h * 1.7);
    out.push({
      time: new Date(start + h * 36e5),
      temp: Math.round(temp * 10) / 10,
    });
  }
  return out;
})();

const container = document.getElementById("app");
chart(readings)
  .flow(scatter({ by: "time", x: "time", y: "temp" }))
  .mark(line({ stroke: "steelblue", strokeWidth: 2 }))
  .render(container, { w: 560, h: 200 });

Signature ​

ts
Calendar.second;
Calendar.minute;
Calendar.hour;
Calendar.day;
Calendar.weekday; // the day of the week, from Monday
Calendar.weekday({ start: "sunday" });
Calendar.week; // weeks start on Monday
Calendar.week({ start: "sunday" });
Calendar.month;
Calendar.quarter;
Calendar.year;

Calendar.month({ every: 3 }); // cells of 3 months
Calendar.week({ every: 2, start: "sunday" });
Calendar.quarter.format(fn); // custom labels, fn: (cell) => string
Calendar.month({ every: 3 }).format(fn);

field("date").bin(Calendar.month); // the month of the year
field("date").bin(Calendar.weekday); // the day of the week
field("date").bin([Calendar.year, Calendar.month]); // each month of each year
field("date").bin(Calendar.year).bin(Calendar.month); // the months of each year
chart(data, { axes: { x: { levels: [Calendar.month, Calendar.year] } } });

No Calendar kind has a required option, so each is a Calendar value as it is, and calling it sets options: Calendar.month and Calendar.month() are the same value. This holds in every strategy family, such as Curve. (#1145 moves the examples to the bare spelling.) .format(fn) is a method of any Calendar value, bare or called.

Parameters ​

ParameterTypeDescription
everynumberCalendar.month({ every }): how many units one cell spans. A whole number, 1 or more. Default 1. Every kind but weekday.
start"monday" | "sunday"Calendar.week({ start }), Calendar.weekday({ start }): the first day of a week. Default "monday", as in ISO 8601.
fn(cell: CalendarCell | CalendarPart) => string.format(fn): the label of each cell. Default: the level's label (see Behavior).

fn takes a cell of the time line, a CalendarCell, or a part of a cycle, a CalendarPart. A level gives parts when it is binned alone and is a part of a larger unit (field("date").bin(Calendar.month), the month of the year), and cells after a coarser level in a chain (.bin(Calendar.year).bin(Calendar.month)) or as an entry of an axis's levels. The two have a kind, "cell" or "part", which tells them apart:

ts
Calendar.month.format((cell) =>
  cell.kind === "cell" ? `${cell.month}/${cell.year}` : `${cell.month}`
);

A CalendarCell (kind: "cell") has these fields. The calendar fields are those of the cell's start in the zone of the axis's column, named like pandas' and polars' dt fields.

FieldTypeDescription
kind"cell"A cell of the time line.
startnumberThe instant the cell starts, in epoch milliseconds.
endnumberThe instant the next cell starts.
unitCalendarUnitThe cell's level: "second", "minute", ... "year".
yearnumberThe year.
quarternumberThe quarter, 1 to 4.
monthnumberThe month, 1 to 12.
weeknumberThe ISO 8601 week number.
daynumberThe day of the month.
weekdaynumberThe ISO 8601 day of the week, 1 (Monday) to 7 (Sunday), whatever day a week starts on.
hournumberThe hour, 0 to 23.
minutenumberThe minute.
secondnumberThe second.

A CalendarPart (kind: "part", a month of the year, see Bins) has no one place on the time line, so it has no start, end or week, and no field of its cycle's unit or above (no year for a month of the year). It has kind, unit, and the fields below its cycle, those of its first instant (day is 1 for a month): quarter, month, day, hour, minute and second for a part of a year, day, hour, minute and second for a part of a month, and for a part of a week (a weekday, or an hour of a weekday), which holds days, weekday, hour, minute and second.

Behavior ​

Cells.

  • A cell starts where the calendar of the column's zone says the level starts: a day at local midnight, a month on its first day, a week on its first day. A day in a zone with daylight saving can last 23 or 25 hours.
  • every counts from the level above: Calendar.month({ every: 3 }) starts its cells in January, April, July and October, Calendar.hour({ every: 6 }) at 0, 6, 12 and 18 o'clock, and Calendar.day({ every: 2 }) on days 1, 3, 5, ... of each month, so the last cell of a 31-day month is one day long. Calendar.year({ every: 5 }) starts on years divisible by 5. Weeks count from the week of 1970-01-01.
  • Calendar.quarter is months in steps of 3, labeled Q1 to Q4.
  • Calendar.weekday is days, labeled by the day of the week (Mon). It has no steps: Calendar.weekday({ every: n }) is an error.
  • .format(fn) returns the same partition with fn as its label. It keeps the options, so Calendar.month({ every: 3 }).format(fn) has cells of 3 months.
  • An unknown option is an error that lists the kind's options. The step was once a method, .every(n); Calendar.month.every(3) is now an error that names Calendar.month({ every: 3 }).
  • A partition with a .format is JS-only: a function has no wire form, so serializing it (to Python or the IR) is an error that names the entry.

Bins.

  • A level binned alone is a part of the next larger level, a cycle: all the cells with the same part are one cell, in any year. Every part is a cell, in the cycle's order, whatever the data holds.

    Level aloneCellsLabels
    Calendar.secondthe 60 seconds of a minute00 to 59
    Calendar.minutethe 60 minutes of an hour00 to 59
    Calendar.hourthe 24 hours of a day12 AM to 11 PM
    Calendar.daythe 31 days of a month1 to 31
    Calendar.weekdaythe 7 days of a weekMon to Sun
    Calendar.monththe 12 months of a yearJan to Dec
    Calendar.quarterthe 4 quarters of a yearQ1 to Q4
    Calendar.weekweeks on the time lineits first day, Jan 1
    Calendar.yearyears on the time line2024

    A weekday's days start on the week's first day: with Calendar.weekday({ start: "sunday" }) they are Sun to Sat. A week is a part of nothing (weeks do not nest in months or years), and nothing holds a year, so they stay on the time line. A step counts from the level above: Calendar.month({ every: 3 }) alone is 4 cells, starting in January, April, July and October.

  • A list is a product, [Calendar.year, Calendar.month]: one cell where one cell of each level meets one of the next, each level after the first a part of the one before it. Its cells are the finest level's cells (the June of 2015), labeled by every level at once (Jun 2015, Q2 2015, Jun 15, 2015). With a first level on the time line, the cells are every cell in the range of the data, the empty ones too, and each has one region on the time line. With a first level that is a part, [Calendar.month, Calendar.day], the cells are the parts of a cycle (the 366 days of a year).

  • A weekday is a part of a week that starts on the same day, so [Calendar.week, Calendar.weekday] is each day, labeled Mon, Jan 1. A weekday is a day, so an hour is a part of it: [Calendar.weekday, Calendar.hour] is the 168 hours of a week.

  • A second .bin(...) splits each cell of the first, a chain: .bin(Calendar.year).bin(Calendar.month) gives the months of each year. On the time line, the next bin must be finer, and a cell it shares with two cells of the bin before it is cut in two (a week across New Year). On a cycle, the next bin must be a part of the one before it, as in a product. Each cell keeps the cell it was split from, and an axis over the key shows one level per bin (a month level, then a year level).

  • A product's levels must be in order, at a step of 1 before the last, with default labels: [Calendar.year, Calendar.day] is an error (a day is a part of a month), and so are [Calendar.month], a numeric bin, a level with a .format, and a weekday of a week that starts on another day. A chain whose next bin is not finer is an error.

  • A part of a cycle has no one region on the time line, so partition over one is an error that names the product and the chain.

Levels of a time axis.

  • A time axis is continuous: every tick and label sits where the axis's scale puts its instant.
  • The axis's domain is rounded outward to the cells of the inner row, as a numeric axis rounds its domain to its tick step, so both ends of the axis are ticks.
  • Each row is one partition. Its ticks are the starts of its cells inside the domain. Each label is centered on its cell's start tick. An outer row's first cell can start before the domain: its label is centered on the first tick, under the inner row's first label. The labels at the ends may reach past the axis, as a numeric axis's do.
  • The axis labels every tick, as a numeric axis does.
  • Rows need not nest: [Calendar.week, Calendar.month] is valid.
  • The first row sits next to the axis line, and each further row sits past the one before it. An outer-row tick that falls between inner ticks is longer.
  • By default the axis has two rows. The inner row gives about 10 ticks over the data, as a numeric axis does: it is the level and step whose cells are nearest in length to a tenth of the data's span, out of seconds and minutes (1, 5, 15, 30), hours (1, 3, 6, 12), days (1, 2), months (1, 2, 3), and years (1, 2, 5, 10, 20, 50, ...). The outer row is the inner row's parent: minutes for seconds, hours for minutes, days for hours, months for days and weeks, weeks for weekdays (with the same first day), years for months and quarters. Year has no parent, so a year row is the only row.
  • In levels, a Calendar value names a row's partition, and the outer row follows the inner row as named. false hides a row: its ticks and labels are not drawn, and the domain is still rounded to the inner row's cells. On the outermost axis of its dimension, Calendar values past the rows add rows.
  • A domain of one instant spans the day that holds it.
  • Default labels come from Intl.DateTimeFormat in English (en-US), whatever the runtime's locale: 12 AM for an hour, Feb 29 for a day or week, Mon for a weekday, Jan for a month, 2024 for a year. A locale option is #1098.
  • label.angle does not rotate the labels of a time axis.
  • In a faceted chart, the rows of the time axes inside the facets are the first levels of the dimension, and the facets' category level comes after them.

Example ​

ts
chart(prices, {
  schema: { date: Schema.time() },
  axes: {
    x: {
      levels: [
        Calendar.quarter.format((cell) =>
          cell.kind === "cell"
            ? `Q${cell.quarter} '${String(cell.year % 100).padStart(2, "0")}`
            : `Q${cell.quarter}`
        ),
        false, // no year row
      ],
    },
    y: true,
  },
})
  .flow(scatter({ by: "date", x: "date", y: "price" }))
  .mark(line())
  .render(container, { w: 560, h: 200 });
ts
chart(weather, { schema: { date: Schema.time() }, axes: true })
  .flow(spread({ by: field("date").bin(Calendar.month), dir: "x" }))
  .mark(rect({ h: field("precipitation").mean() }));
ts
import { Calendar, Schema, chart, field, rect, spread } from "gofish-graphics";
import { seattleWeather } from "./dataset";

const weather = seattleWeather;

const container = document.getElementById("app");
chart(weather, { schema: { date: Schema.time() }, axes: true })
  .flow(spread({ by: field("date").bin(Calendar.month), dir: "x" }))
  .mark(rect({ h: field("precipitation").mean(), fill: "steelblue" }))
  .render(container, { w: 560, h: 220 });
Dataset
ts
export type SeattleWeather = {
  date: string;
  precipitation: number;
  temp_max: number;
  temp_min: number;
  wind: number;
  weather: string;
};
export const seattleWeather: SeattleWeather[] = [
  { date: "2012-01-01T00:00:00.000Z", precipitation: 0, temp_max: 12.8, temp_min: 5, wind: 4.7, weather: "drizzle" },
  { date: "2012-01-02T00:00:00.000Z", precipitation: 10.9, temp_max: 10.6, temp_min: 2.8, wind: 4.5, weather: "rain" },
  { date: "2012-01-03T00:00:00.000Z", precipitation: 0.8, temp_max: 11.7, temp_min: 7.2, wind: 2.3, weather: "rain" },
  { date: "2012-01-04T00:00:00.000Z", precipitation: 20.3, temp_max: 12.2, temp_min: 5.6, wind: 4.7, weather: "rain" },
  { date: "2012-01-05T00:00:00.000Z", precipitation: 1.3, temp_max: 8.9, temp_min: 2.8, wind: 6.1, weather: "rain" },
  // … 1,456 more rows
];