# Chart frame

The chassis every chart sits in (Candor's ChartFrame, the honesty chokepoint): title, legend, a chart/table toggle, and the states a chart can be in.

- Registry name: `chart-frame` (`@candor/chart-frame`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/chart-frame

## Install

```bash
npx candor-ui add chart-frame
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/chart-frame`.
It brings in: data, locale, skeleton, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { ChartFrame, type TableView } from "@/components/chart-frame"
import { formatValue, linear, seriesColor } from "@/components/scale"

// A custom plot in the shared chassis: p95 query time per table, as horizontal bars.
const rows: { table: string; p95: number }[] = [
  { table: "deployments", p95: 0.84 },
  { table: "audit_log", p95: 0.61 },
  { table: "projects", p95: 0.32 },
  { table: "users", p95: 0.18 },
]

const table: TableView = { columns: ["Table", "p95"], rows: rows.map((r) => [r.table, formatValue(r.p95, "duration", true)]) }

export default function Example() {
  return (
    <ChartFrame className="max-w-xl" title="Slowest tables" subtitle="p95 query time · Production, last 24 hours" table={table} height={140}>
      {({ width, height }) => {
        const label = 96
        const x = linear(0, 1, 0, width - label - 48)
        const band = height / rows.length
        return (
          <svg width={width} height={height} role="img" aria-label={rows.map((r) => `${r.table} ${formatValue(r.p95, "duration", true)}`).join(", ")}>
            {rows.map((r, i) => (
              <g key={r.table} transform={`translate(0, ${i * band})`}>
                <text x={0} y={band / 2} dominantBaseline="middle" className="fill-muted-foreground text-xs">
                  {r.table}
                </text>
                <rect x={label} y={band / 2 - 8} width={x(r.p95)} height={16} rx={4} fill={seriesColor(0)} />
                <text x={label + x(r.p95) + 6} y={band / 2} dominantBaseline="middle" className="fill-foreground text-xs tabular-nums">
                  {formatValue(r.p95, "duration")}
                </text>
              </g>
            ))}
          </svg>
        )
      }}
    </ChartFrame>
  )
}
```

## <ChartFrame>

Props:

- `title: string`: What the chart shows, above it.
- `subtitle?: string | undefined`: A line under the title, e.g. the period or unit.
- `state?: ChartState | undefined` (default `{ kind: "ready" }`): Loading, empty (with why), partial (with what's missing) or error. Defaults to ready.
- `legend?: LegendItem[] | undefined`: Series names and colours; shown only when there are two or more.
- `table?: TableView | undefined`: Every value as rows, behind a chart/table toggle. Without it there's no toggle.
- `height?: number | undefined` (default `220`): The plot's height in pixels. Defaults to 220.
- `refreshing?: boolean | undefined` (default `false`): Keeps the previous render at reduced opacity while new data loads.
- `defaultWidth?: number | undefined`: The width to draw at until the frame can be measured: on the server, and in test environments like jsdom that measure everything as 0. Left out, it comes from ChartSizeProvider, else 0 (the plot waits for a measurement).
- `children: (size: { width: number; height: number }) => React.ReactNode`: Renders the plot at the measured width.
- `className?: string | undefined`

## <ChartSizeProvider>

A default plot width for every chart inside: for server rendering, or tests whose DOM can't measure.

Props:

- `defaultWidth: number`: The width, in pixels, charts draw at until they can measure themselves.
- `children: React.ReactNode`

## Strings (`chartFrame`)

The fixed words, English by default. Override any of them under `chartFrame` in `<LocaleProvider strings={{ chartFrame: { … } }}>`; the table is exported as `chartFrameStrings`.

- `view`: View
- `chart`: Chart
- `table`: Table
- `legend`: Legend

## Types

### LegendItem

```ts
{ label: string; color: string; shape?: "rect" | "line" | undefined }
```

### TableView

```ts
{ columns: string[]; rows: (string | number)[][] }
```

### TipState

The pointer's position and what to show there.

```ts
{ x: number; y: number; title: string; rows: { label: string; value: string; color?: string | undefined; shape?: "rect" | "line" | undefined }[] } | null
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
