# Heat Calendar

> Weeks across and days down, in one hue whose strength is how much happened, or each day in its event type's color, with a legend that filters and a span you can measure.

- Name: `heat-calendar`
- Status: beta
- Categories: charts
- File: the install writes `heat-calendar.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@txbrendan/chart`, `@txbrendan/motion-timing`, `@txbrendan/rolling-number`. The install adds them.
- Gallery: https://txbrendan.dev/components/heat-calendar
- Registry item: https://txbrendan.dev/r/heat-calendar.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @txbrendan/heat-calendar
   ```

## Use it for

- Activity over months, like training attendance, logins, or match minutes, where the pattern matters more than one day.
- Pass values as values\[week\]\[day\] from 0 to 1, oldest week first and Monday first, with a title or aria-label.
- Pass maxCount and unit, like 8 and 'sessions', so a day reads '4 sessions', not '50%'.
- For a club's calendar, pass events: an ISO day to its event or events. Each day takes its type's color.
- Pass types to name event types and set their order. Each color defaults to the next chart color.
- Pass endDate for the last day shown. Without it the calendar ends today, once the page is in the browser.

## Avoid it for

- Exact numbers people must compare. Use BarChart or BreakdownList.
- A trend over a few points. Use LineChart or Sparkline.
- Booking or choosing a day. Use Calendar.
- More than about five event types. The colors stop being easy to tell apart.

## Props

- `values`: `number[][]`. Intensity mode. values\[week\]\[day\] from 0 to 1, Monday first. The last row is the week that holds endDate.
- `events`: `Record<string, HeatEvent | HeatEvent[]>`. Event mode. An ISO day, like '2026-09-26', to its event or events. An event is a type id, like 'training', or { type, label?, opponent?, score? }. A score of \[24, 17\] reads 'Won 24–17 vs Harbour Town'.
- `types`: `{ id: string; label: string; color?: string }[]`. Event types in legend order. Default: each type in events as it first appears. A color is a theme token, like 'var(--chart-3)'.
- `weeks`: `number`. Default `values.length, or 26`. How many weeks to show.
- `maxCount`: `number`. The count a full cell stands for. With 8, a value of 0.5 reads '4 sessions'.
- `unit`: `string`. The noun after counts, plural, like 'sessions'. A count of one drops the final s.
- `endDate`: `Date | string`. Default `today`. The last day shown, as an ISO day or a Date. One it cannot read falls back to today. Days after it in its week are left blank.
- `title`: `ReactNode`. Names the calendar and its grid. Pass this, aria-label, or aria-labelledby.
- `locale`: `string`. Default `"en-US"`. The locale for day and month names. The default keeps the server and the browser the same.
- `className`: `string`. Extra classes for the figure. '\[--heat-calendar-color:var(--chart-3)\]' sets the one hue in intensity mode. It defaults to --chart-1.

## Accessibility

- The days are a role="grid", one row per weekday. Each day is a gridcell named by its date and what happened, like 'Sat, Sep 26: 4 sessions'.
- The grid is one Tab stop. Left and Right move a week, Up and Down move a day, and Home and End go to the first and last days.
- Enter or Space starts a span, and again ends it. A status line says the span's total. Escape clears it and goes no further.
- The legend steps are buttons. Hovering or focusing one shows only its days. Pressing one keeps it on, with aria-pressed.
- The readout over a day is hidden from screen readers, since each day already says the same.
- In forced colors, the days, the legend swatches, and the readout's dots keep their hues, so each day still matches its legend step or type. Empty days and the None step mix the user's page and text colors, so they stay faint. The level steps and the event colors, including colors set in types, pull toward the text color. So on a light or a dark page, the steps stay in order above None, and event days stand out from the page. Words, outlines, and the focus ring take the user's colors.
- With reduced motion, the cells fade in without the spring, and nothing lifts or glides.

## Examples

### Training attendance

```tsx
<HeatCalendar
  title="Training attendance"
  values={weeks}
  maxCount={8}
  unit="sessions"
  endDate="2026-09-27"
/>
```

### A club's calendar by event type

```tsx
<HeatCalendar
  title="Club calendar"
  weeks={12}
  endDate="2026-09-27"
  types={[
    { id: "training", label: "Training" },
    { id: "match", label: "Match" },
  ]}
  events={{
    "2026-09-22": { type: "training", label: "Scrums and lineouts" },
    "2026-09-26": { type: "match", score: [24, 17], opponent: "Harbour Town" },
  }}
/>
```

### In another hue

```tsx
<HeatCalendar aria-label="Logins" values={weeks} className="[--heat-calendar-color:var(--chart-3)]" />
```
