---
id: "packages/active-charts/date-axes"
title: "Date axes"
description: "Calendar days and instants on an x axis, and how ticks and labels follow from them."
url: "/packages/active-charts/date-axes"
locale: "en"
revision: "e5f106afe14a0c0f"
---

# Date axes

> [!NOTE]
> **The package's own guide**
>
> This page is the package's `docs/recipes/DATE-AXES.md`, included unchanged
> from the package as it is in Foundation. Links to the package's internal
> design documents keep their text.

**Example: A first chart**

Client: Runs entirely in your browser. Source: `examples/charts/first-chart.tsx`.

```tsx
// apps/docs/examples/charts/first-chart.tsx
'use client';

import {
  ChartDefaults,
  englishChartTranslator,
  LineChart,
} from '@workspace/active-charts';

const data = [
  {date: new Date(Date.UTC(2026, 0, 2)), price: 100.5},
  {date: new Date(Date.UTC(2026, 0, 5)), price: 101.2},
  {date: new Date(Date.UTC(2026, 0, 6)), price: 102.0},
  {date: new Date(Date.UTC(2026, 0, 7)), price: 101.4},
  {date: new Date(Date.UTC(2026, 0, 8)), price: 103.1},
  {date: new Date(Date.UTC(2026, 0, 9)), price: 104.0},
];

/** The smallest chart: a presentation and a translator once, a line per series. */
export default function FirstChart() {
  return (
    <ChartDefaults locale="en-GB" t={englishChartTranslator}>
      <LineChart
        data={data}
        xKey="date"
        yKey="price"
        name="Price"
        // The x values are calendar days, not moments in time.
        xAxis={{temporalSemantic: 'calendarDate'}}
        height={260}
      />
    </ChartDefaults>
  );
}
```

How to put dates on a Cartesian x-axis without hitting the silent
invalid-date trap. Background and the full per-axis reference live in
`../API.md` §9.1.

## The trap in one sentence

Bar-class charts (`BarChart`, `Histogram`, `WaterfallChart`,
`BoxPlotChart`, `RangeBarChart`, `DumbbellChart`, `LollipopChart`) default
to a `category` (`scaleBand`) x-axis, which stringifies a `Date` xKey to a
non-ISO string (`"Fri May 01 2026 …"`); a `formatDate(v as Date, …)`
formatter then cannot read it and returns an empty string, so the tick
label is blank. (On the bar-class wrappers such a formatter no longer
compiles; the trap remains reachable through `as`/`any`.)

The fix is always one of two patterns: **pre-format to a category string**
(best for bars) or **declare an explicit time scale** (best for
lines/areas).

## Monthly bars

Bars need a band scale for their width, so use a `category` axis and feed a
pre-formatted month label.

```tsx
const chartData = report.monthlyDistribution.map((entry) => ({
  ...entry,
  label: formatDateOnly(`${entry.month}-01`, 'month', locale),
}));

<BarChart
  data={chartData}
  xKey="label"
  xAxis="category"
  series={[
    {yKey: 'processed', name: t('chart.processed')},
    {yKey: 'failed', name: t('chart.failed')},
  ]}
  stacked
  height={220}
/>;
```

Why not keep the `Date` and pass a `formatter`? On a band scale the
formatter receives the stringified date, not a `Date` — see the trap
above. Pre-formatting keeps a single, locale-aware label per bar.

## Daily lines

Line and area charts use an `ordinal-time` (`scalePoint`) scale by default.
That axis is temporal, so it always declares whether its values are calendar
dates or instants (`../API.md` §9.1, ADR-027). A
daily price is a calendar date:

```tsx
<LineChart
  data={dailyPrices}
  xKey="date"
  xAxis={{
    temporalSemantic: 'calendarDate',
    formatter: (v) => formatDateOnly(v, 'day-month', locale),
  }}
  series={[{yKey: 'close', name: t('chart.close')}]}
  height={240}
/>
```

A custom formatter reads the value the way the axis declares it:
`formatDateOnly` for a calendar date (the day never moves), `formatDate` for an
instant (read in the chart zone). Without a formatter, every surface already
does this for you.

## Range comparison (range bars)

`RangeBarChart` is a bar-class chart, so the same category rule applies:
pre-format the category label, keep the numeric low/high keys for the bar
extent.

```tsx
const chartData = quarters.map((q) => ({
  ...q,
  label: formatDateOnly(`${q.period}-01`, 'quarter-year', locale),
}));

<RangeBarChart
  data={chartData}
  xKey="label"
  xAxis="category"
  yLowKey="min"
  yHighKey="max"
  height={260}
/>;
```

## Quick reference

| Goal | Chart | x-axis | xKey holds |
| --- | --- | --- | --- |
| Monthly / quarterly bars | `BarChart`, `RangeBarChart` | `xAxis="category"` | pre-formatted label string |
| Daily / monthly line/area | `LineChart`, `AreaChart` | `xAxis={{temporalSemantic: 'calendarDate'}}` | `Date` (or ISO string) |
| Moments in time | `LineChart`, `AreaChart` | `xAxis={{temporalSemantic: 'instant'}}` | `Date` (or ISO string) |
| Real calendar spacing | `LineChart`, `AreaChart` | `xAxis={{type: 'time', temporalSemantic: …}}` | `Date` |

## See also

- `../API.md` §9.1 — Axis Types — default x-axis
  per chart and the formatter runtime-value table.
- `../API.md` §10.1 — `format` string shorthand — locale-neutral
  numeric formatting for value axes.
