Chart focus
ProWhether two data are the same mark: the same series and category.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2 and 4.1.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/chart-focus
Usage
"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(a: ChartDatum | null | undefined, b: ChartDatum | null | undefined)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.
useChartSelect({ onSelect, selectLabel, selected, defaultSelected, }: ChartSelect & { /** The datum selected at first, when the chart keeps the selection itself. */ defaultSelected?: ChartDatum | null | undefined; })A chart's selection: yours through selected (null or left out is none), or, when you pass defaultSelected and no selected, the chart's own, which starts there and follows each press. onSelect is still called with every press. A chart given no defaultSelected keeps no selection of its own, as in 5.0, so an app that clears selected to undefined sees none marked. Without onSelect the marks can't be selected, as before.
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(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.
{ /** * 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.
{ 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.
{ 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.
{ /** 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; }