# Estimate Chart

> What was expected against what came in, per period: a ring for the estimate and a dot for the actual, colored by beat, miss, or match.

- Name: `estimate-chart`
- Status: beta
- Categories: charts
- File: the install writes `estimate-chart.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `recharts`, `react-is`. The install adds them.
- Other registry items: `@arbokui/chart`. The install adds them.
- Gallery: https://arbokui.com/components/estimate-chart
- Registry item: https://arbokui.com/r/estimate-chart.json

## Install

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

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

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

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui 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 @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/estimate-chart
   ```

## Use it for

- Showing a forecast next to the result for each period, like earnings per share against the estimate.
- Pass data with a period, an estimate, and an actual for each row, oldest first.
- Leave actual out for a period still to come. It shows its estimate ring alone.
- Change data, as from tabs, and each mark glides to its new value in a wave from left to right. Periods are matched by name.
- Pass matchWithin to count a near value as a match.
- Pass valueFormat, like { style: "currency", currency: "USD" }, to format the tooltip and the axis.

## Avoid it for

- One value per period with no forecast. Use LineChart or BarChart.
- Many series at once. This chart compares one estimate with one actual.
- Hundreds of periods. The marks crowd together. Show the latest periods and offer more.

## Props

- `data`: `{ period: string; estimate?: number | null; actual?: number | null }[]`. One row per period, oldest first. Leave actual out for a period still to come. Periods may share a name, like Q1 in two years, and each keeps its place. An actual with no estimate draws a plain dot with an Actual key.
- `title`: `ReactNode`. A visible title. It names the chart. TypeScript requires title, aria-label, or aria-labelledby.
- `aria-label`: `string`. Names the chart when it has no visible title.
- `valueFormat`: `Intl.NumberFormatOptions`. Formats tooltip values, and the value labels in short form, like $120B.
- `locale`: `string`. Default `"en-US"`. The locale for numbers. Fixed, so server and browser print the same text.
- `matchWithin`: `number`. Default `0`. An actual this close to its estimate counts as a match.
- `labels`: `{ estimate?, actual?, beat?, miss?, match?: string }`. The words for each mark, in the legend and the tooltip.
- `legend`: `boolean`. Default `true`. Shows the legend: the estimate ring, then Beat, Miss, and Match once any period is scored. The keys stay put when the data changes. While loading, it shows the estimate ring with Beat, Miss, and Match.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. Height: 5, 7, or 9 times --control-h-md, so density scales it.
- `loading`: `boolean`. Default `false`. Shows skeleton points at the same height. When data arrives, they fade out while the chart draws in.
- `error`: `ReactNode`. Shows this message in place of the chart. true shows a default message.
- `onRetry`: `() => void`. Shows a retry button under the error.
- `emptyText`: `ReactNode`. Default `"No data yet"`. Shown when no period has an estimate or an actual.
- `className`: `string`. Extra classes. Later classes win, so className='h-96' replaces the height.

## Accessibility

- Renders a \<figure> named by its title, or by aria-label. TypeScript requires one.
- Tab focuses the chart. The arrow keys step through the periods, and the tooltip reads out each one: its estimate, its actual, and whether it beat, missed, or matched, with the gap in percent.
- Beat, miss, and match are said in words in the legend and the tooltip, not by color alone. The estimate is a ring and the actual a filled dot, so the two differ by shape too.
- The period under the pointer or the arrow keys grows a little, and a line marks it. The value labels fade back while it is read, since the tooltip carries the numbers. Escape hides the tooltip and brings them back.
- The value labels have their own room at the start of the plot, so no mark covers one. Period labels at either end are nudged inside the chart, and long names are spaced so they never overlap. Right to left, the labels stay in their room. A ring its dot would almost cover is left out, and the tooltip still gives both numbers.
- Marks grow in from left to right once the chart is on screen. When the data changes, each glides to its new value in the same wave, and the value labels cross-fade. When periods come or go, the others slide over first. At 0ms everything draws at once, and Recharts skips the motion for reduced motion.
- Keeps one height while it loads, draws, fails, or has no data, so the page does not move.

## Examples

### Earnings per share

```tsx
<EstimateChart
  title="Earnings per share"
  data={[
    { period: "Q1", estimate: 1.5, actual: 1.6 },
    { period: "Q2", estimate: 1.4, actual: 1.3 },
    { period: "Q3", estimate: 1.7, actual: 1.7 },
    { period: "Q4", estimate: 2.1 },
  ]}
  valueFormat={{ style: "currency", currency: "USD" }}
/>
```

### Data that follows tabs

```tsx
const [measure, setMeasure] = useState("share");

<Tabs value={measure} onValueChange={(next) => setMeasure(String(next))}>
  <TabsList aria-label="Measure">
    <TabsTab value="share">Per share</TabsTab>
    <TabsTab value="revenue">Revenue</TabsTab>
  </TabsList>
  <TabsPanel value={measure}>
    <EstimateChart
      title="Estimate and actual per quarter"
      data={measure === "share" ? perShare : revenue}
      valueFormat={measure === "share"
        ? { style: "currency", currency: "USD" }
        : { style: "currency", currency: "USD", notation: "compact" }}
    />
  </TabsPanel>
</Tabs>
```

### While it loads, and when it fails

```tsx
<EstimateChart
  aria-label="Earnings per share"
  data={data ?? []}
  loading={isLoading}
  error={error ? "Quarters could not load." : undefined}
  onRetry={() => refetch()}
/>
```
