# Chart focus

Whether two data are the same mark: the same series and category.

- Registry name: `chart-focus` (`@candor/chart-focus`)
- 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-focus

## Install

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

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/chart-focus`.
It brings in: locale, tokens.

## Example

```tsx
"use client"

import { ChartFrame } from "@/components/candor/chart-frame"
import { useChartFocus } from "@/components/candor/chart-focus"
import { formatValue, linear, seriesColor } from "@/components/candor/scale"

// A custom plot that every keyboard user can read: one tab stop, then the arrows, each bar named with its value.
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 },
]

function Bars({ width, height }: { width: number; height: number }) {
  // One mark per row, all in one column, so up and down (and left and right) move between them.
  const focus = useChartFocus(rows.length, { columns: 1, flips: false })
  const label = 96
  const x = linear(0, 1, 0, width - label - 48)
  const band = height / rows.length
  return (
    <svg width={width} height={height} role="group" aria-label="Slowest tables">
      {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
            {...focus.markProps(i)}
            role="img"
            aria-label={`${r.table}: ${formatValue(r.p95, "duration", true)}`}
            x={label}
            y={band / 2 - 8}
            width={x(r.p95)}
            height={16}
            rx={4}
            fill={seriesColor(0)}
            className="outline-none focus-visible:stroke-ring focus-visible:stroke-2"
          />
        </g>
      ))}
    </svg>
  )
}

export default function Example() {
  return (
    <ChartFrame className="max-w-xl" title="Slowest tables" subtitle="p95 query time · Production, last 24 hours" height={140}>
      {(size) => <Bars {...size} />}
    </ChartFrame>
  )
}
```

## Functions

### sameDatum

```ts
sameDatum(a: ChartDatum | null | undefined, b: ChartDatum | null | undefined)
```

### useChartFocus

```ts
useChartFocus(count: number, options?: ChartFocusOptions | undefined)
```

Every datum in a chart reachable by keyboard, one tab stop for the whole plot (a roving tab index). Arrows move between marks, left and right swapped in a right-to-left language where the marks follow it; up and down, and PageUp and PageDown, move between rows (series), up to the row drawn above with `rowsUp`; Home and End go to the row's ends (a single column's ends), with Ctrl to the first and last mark. Spread `markProps(i)` on the nth mark, numbered row by row; `focused` is the mark with focus, for a tooltip. With `chartSelection`, the same marks can be pressed to drill down.

### chartSelection

```ts
chartSelection(select: ChartSelect | undefined, filterTo: (datum: ChartDatum) => string, noData: string)
```

What a chart's marks need to be pressed: whether they can be (`on`), the words for what pressing one does (`hint`, "No data" for a missing value or one that isn't a finite number, which can't be selected), whether one is the selected one (`pressed`), and the keys and click that select (`keys`, `choose`). `filterTo` is the chart's own "Filter to {category}", `noData` its "No data". A plain function, not a hook, so a chart can call it anywhere.

### markRole

```ts
markRole(selection: ReturnType<typeof chartSelection>, datum: ChartDatum, describedBy: string)
```

The attributes a mark carries: `role="img"` when it can't be pressed; with `onSelect`, `role="button"`, pressed when selected, `aria-disabled` when its value is missing, and described by the element `describedBy` names (which holds the hint).

## Types

### ChartFocusOptions

How a chart's marks are laid out for the keyboard.

```ts
{
  /**
   * Marks per row: a row is one series (bars, lines) or one row of cells (a heatmap). Left out, every mark is in one
   * row (a donut's slices).
   */
  columns?: number | undefined;
  /**
   * Whether the marks are laid out in the reading direction, so left and right swap in a right-to-left language: true
   * for an HTML grid (a heatmap). An SVG plot drawn left to right in every language passes false, so the arrows follow
   * what is on screen. Defaults to true.
   */
  flips?: boolean | undefined;
  /**
   * Whether rows are drawn bottom to top, each over the one before (a stacked bar's series), so ArrowUp and PageUp move
   * to the next row and ArrowDown and PageDown to the one before: the arrows follow what is on screen. Defaults to false.
   */
  rowsUp?: boolean | undefined;
  /** The mark the tab stop rests on until one takes focus: the current page in a list of pages. Defaults to the first. */
  start?: number | undefined;
}
```

### ChartMarkProps

What one mark spreads onto its element: its place in the tab order, its keys and its ref.

```ts
{
  tabIndex: 0 | -1
  ref: (el: Element | null) => void
  onKeyDown: (event: React.KeyboardEvent) => void
  onFocus: () => void
  onBlur: () => void
}
```

### ChartDatum

One datum a chart draws: its series, its category (or x label, or column) and its value; null where there's none.

```ts
{ series: string; category: string; value: number | null }
```

### ChartSelect

What a chart's marks do when pressed: the props every chart that can drill down takes.

```ts
{
  /** Called with the datum a person clicks, or presses Enter or Space on. Without it, marks can't be selected. */
  onSelect?: ((datum: ChartDatum) => void) | undefined;
  /** What selecting a datum does, in the mark's description and tooltip. Defaults to "Filter to {category}". */
  selectLabel?: ((datum: ChartDatum) => string) | undefined;
  /** The datum selected now, marked as pressed. `null` or left out: none. */
  selected?: ChartDatum | null | undefined;
}
```

## 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`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
