# ForecastCone (/docs/charts/forecast-cone)

ForecastCone answers "will we land where we need to?". History is a solid line; the forecast is a fan of prediction
bands that **widen over the horizon**, with a **dashed** median — because an estimate should never render as fact. The
whole point of a fan chart is visible confidence decay, so three rules are not style options: at most two bands (50/80 —
a 95% band reads as false tail confidence at micro scale), the median is always dashed, and a cone that fails to widen
is flagged, never quietly inflated.

```tsx
<ForecastCone
  data={[30, 32, 31, 34, 36, 35, 38]}
  forecast={{
    mid: [39, 40, 41, 42],
    p80: [
      [36, 42],
      [35, 45],
      [34, 50],
      [33, 55],
    ],
    p50: [
      [37, 41],
      [37, 43],
      [36, 46],
      [35, 49],
    ],
  }}
  target={45}
  label="landing"
  title="Q4 revenue"
  width={260}
  height={28}
/>
```

## Install

```tsx
import { ForecastCone } from "@microcharts/react/forecast-cone";

<ForecastCone data={history} forecast={forecast} target={45} title="Q4 revenue" />
```

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


## When to use it

- **Good for** — a "will we hit Q4?" forecast in a KPI card, a projection with honest uncertainty in a sentence,
  band-vs-target landing reads.
- **Avoid for** — a forecast with no uncertainty (Sparkline) or one estimate's spread (GradedBand).


## Variants

```tsx
const history = [30, 32, 31, 34, 36, 35, 38];
const forecast = {
  mid: [39, 40, 41, 42],
  p80: [[36, 42], [35, 45], [34, 50], [33, 55]],
};

<ForecastCone data={history} forecast={forecast} />
```

```tsx
<ForecastCone
  data={[3000, 3200, 3100, 3400, 3600, 3500, 3800]}
  forecast={{
    mid: [3900, 4000, 4100, 4200],
    p80: [[3600, 4200], [3500, 4500], [3400, 5000], [3300, 5500]],
  }}
  format={{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }}
  locale="de-DE"
/>
```

`format` also takes `Intl.NumberFormatOptions` — with a `locale`, the in-chart landing label and the accessible
summary's median, interval, and target numbers all follow that locale's own grouping and currency placement (the label
above reads "4.200 €", not "€4,200").

## Edge cases

```tsx
<ForecastCone
  data={[]}
  forecast={{ mid: [10, 12, 14], p80: [[8, 12], [8, 15], [7, 19]] }}
  title="No history"
/>
```

```tsx
// bands that narrow instead of widen render exactly as given — the
// component flags this in dev, it never inflates the shape to look honest
<ForecastCone
  data={[10, 11]}
  forecast={{ mid: [12, 13, 14], p80: [[9, 15], [10, 14], [11, 13]] }}
/>
```

With no history the cone still draws from the first forecast point, and the accessible summary quietly drops its "from N
today" clause instead of naming a value that doesn't exist. A cone whose bands narrow instead of widen is an
input-honesty failure — it renders exactly as supplied (never auto-inflated to look right) and logs a one-time dev
warning so the mistake surfaces in development rather than silently misleading a reader.


## Why this default

50/80 bands, not 95, because micro-scale tails invite overreading — a 95% band paints false confidence about the
extremes. The median is dashed so it never reads as a committed number, and the cone must visibly widen: if the supplied
bands don't (they narrow, or stay flat), that is an input-honesty failure the component surfaces rather than papering
over — a forecast whose uncertainty doesn't grow with the horizon is misrepresenting confidence decay.

## Accessibility

The accessible name states the median, the horizon interval, today's actual, and — with a target — whether the band
clears it: **"Median forecast 42 by week 11 (80% between 33 and 55), from 38 today. The 80% band straddles the 50
target."**. The interactive entry is region-aware: history points announce a value, forecast points announce the median
and 80% interval.

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 or press `Escape`.
On touch, a tap pins and a drag scrubs.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Historical actuals. |
| `forecast` (required) | `{ mid: number[]; p80: [lo,hi][]; p50?: [lo,hi][] }` | Median + prediction bands (at most 2: 50/80). |
| `target` | `number` | The landing reference the cone must clear (adds a clearance clause). |
| `unit` | `string` | Period noun for the summary (default "week"). |
| `label` | `"landing" \| "none"` | Median endpoint value in a right gutter. |
| `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).
