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

Q4 revenue is at 38 and the target is 45: ForecastCone shows the spread of where it lands. History is a solid line, and
the forecast is a fan of prediction bands that **widen over the horizon** under a **dashed** median. Three of those
choices are fixed rather than style options. At most two bands, 50 and 80, because a 95% band reads as false tail
confidence at micro scale. The median is always dashed, so an estimate never renders as fact. And the cone must visibly
widen: bands that narrow or stay flat are 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.

## Try it

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

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

## When to use it

Use it for a "will we hit Q4?" forecast in a KPI card, a projection with its uncertainty inside a sentence, and
band-vs-target landing reads. For a forecast with no uncertainty use Sparkline; for one estimate's spread, GradedBand.

## Sizing

**band vs target**

```tsx
<ForecastCone data={history} forecast={forecast} target={45} />
```

**single band (tightest form)**

```tsx
<ForecastCone data={history} forecast={{ mid, p80 }} />
```

## 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 and renders the shape as given
<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 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 error:
uncertainty that doesn't grow with the horizon misrepresents confidence decay. It renders exactly as supplied, never
auto-inflated, and logs a one-time dev warning so the mistake surfaces in development.

## Four homes

**In a sentence**

```tsx
<p>
  Q4 revenue forecast{" "}
  <span className="mc-inline">
    <ForecastCone data={history} forecast={forecast} summary={false} />
  </span>{" "}
  — median path clears target by week 3.
</p>
```

**In a table cell**

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

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">112%</span>
  <span className="unit">of target (median)</span>
  <ForecastCone data={history} forecast={forecast} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Q4 <ForecastCone data={history} forecast={forecast} />
</button>
```

## Accessibility

The accessible name states the median, the horizon interval, and today's actual. With a `target` it adds 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, press `Escape`, or
press outside the chart. 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).
