# SpiralYear (/docs/charts/spiral-year)

SpiralYear winds a calendar series onto an Archimedean spiral. The angle is the position in the year (January at 12
o'clock, running clockwise) and each turn outward is the next year, so the radius encodes time only, never value: an
outer mark is a later date, not a bigger number. The value becomes a five-step opacity, and busy stretches render as
darker arcs. Two adjacent turns place different years at the same angle; read that radial adjacency as calendar
alignment. This is a **pattern instrument**: opacity is the weakest ordered channel, so for an exact day's value reach
for `ActivityGrid` or `HeatStrip`.

```tsx
<SpiralYear
  data={Array.from({ length: 52 }, (_, i) => {
    const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2));
    return i === 29 ? 480 : s;
  })}
  title="Seasonality"
  size={80}
/>
```

## Install

```tsx
import { SpiralYear } from "@microcharts/react/spiral-year";

<SpiralYear data={byWeek} title="Seasonality" />
```

Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.

## Try it

```tsx
import { SpiralYear } from "@microcharts/react/spiral-year/interactive";

<SpiralYear
  data={byWeek}
/>
```

## When to use it

Use it for the seasonal shape of a year, for spotting a busy season or a quiet stretch, or as a compact "the year in one
square". For an exact day's value use `ActivityGrid` or `HeatStrip`, and for a non-cyclic trend use `Sparkline`. Past
about three years the turns pack too tightly to read.

## Sizing

**arc marks for a continuous-ribbon feel**

```tsx
<SpiralYear data={byWeek} mark="arc" />
```

**three steps for the smallest sizes**

```tsx
<SpiralYear data={byWeek} steps={3} />
```

## Variants

Month ticks are on by default: without a temporal anchor the spiral is texture, and the ticks are what make "when"
readable. Five opacity steps is the ceiling, since ordered opacity supports about five discriminable levels; `steps={3}`
drops to three for tiny sizes.

```tsx
const byWeek = Array.from({ length: 52 }, (_, i) => {
  const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2));
  return i === 29 ? 480 : s; // a summer peak in week 30
});

<SpiralYear data={byWeek} mark="arc" />
<SpiralYear data={byWeek} steps={3} />
```

`locale` changes no in-chart mark, since opacity steps carry the value and no number is printed. It does localize the
peak and low values named in the accessible summary:

```tsx
const byWeek = Array.from({ length: 52 }, (_, i) => {
  const s = Math.round(200 + 140 * Math.sin(((i - 8) / 52) * Math.PI * 2));
  return i === 29 ? 1480 : s;
});

<SpiralYear data={byWeek} locale="de-DE" />
```

## Edge cases

```tsx
<SpiralYear data={[80, 120, null, 90, 140]} title="With a gap" />
```

```tsx
<SpiralYear data={[]} title="Empty" />
```

A `null` day or week leaves a gap in the spiral, distinct from a faint step-one mark, because missing is not the same as
low.

## Four homes

**In a sentence**

```tsx
<p>
  Revenue seasonality{" "}
  <span className="mc-inline">
    <SpiralYear data={byWeek} summary={false} />
  </span>{" "}
  — summer swell, outlier week 29.
</p>
```

**In a table cell**

```tsx
<td>
  <SpiralYear data={byWeek} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">30</span>
  <span className="unit">summer peak</span>
  <SpiralYear data={byWeek} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  2024 <SpiralYear data={byWeek} />
</button>
```

Year spiral needs card scale — weekly rings collapse below ~36px.

## Accessibility

The accessible name states the count, the peak, and the low: **"52 weeks; peak 1.480 in week 30, low in week 48."** (the
`de-DE` localized demo). The interactive entry lets you arrow along the spiral chronologically, and announces each mark
with its period and value through a polite live region.

The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again, press `Escape`, or
press outside the chart. On touch, a tap pins and a drag scrubs.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | One value per day or week (cadence inferred from length). |
| `cadence` | `"day" \| "week"` | Data cadence; inferred from length (≈52 → week, else day) when omitted. |
| `startDate` | `string` | ISO date anchoring index 0 to a calendar angle. |
| `steps` | `3 \| 5` | Opacity quantization (default 5). |
| `monthTicks` | `boolean` | Faint radial month ticks (default true). |
| `mark` | `"dot" \| "arc"` | Dots (default) or short arc segments. |
| `size` | `number` | Spiral box edge in viewBox units (default 24). |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |

Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
