@tanstack/charts
Version:
A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.
223 lines (161 loc) • 7.09 kB
Markdown
name: build-chart-interactions
description: >
Compose TanStack Charts focus, tooltips, controlled selections, cursors,
brushes, zoom, keyboard behavior, and coordinated views. Load for hover,
focus, pinning, crosshairs, clipping, gesture state, or chart-table
coordination.
metadata:
type: core
library: '@tanstack/charts'
library_version: '0.9.0'
sources:
- 'TanStack/charts:docs/guides/tooltips-and-focus.md'
- 'TanStack/charts:docs/guides/interactions-and-selections.md'
- 'TanStack/charts:docs/reference/focus-and-interaction.md'
- 'TanStack/charts:docs/reference/marks/focus-guide.md'
# Build Chart Interactions
Use **trigger → inspect → decide → build → verify**. Charts owns renderer-neutral interaction mechanics; the application owns accepted semantic state, persistence, product policy, and equivalent non-pointer controls.
## Setup
Start with native focus and tooltip behavior before adding controlled state:
```ts
import { defineChart, lineY } from '@tanstack/charts'
import { crosshair } from '@tanstack/charts/crosshair'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { scalePoint } from '@tanstack/charts/scales/point'
import { tooltip } from '@tanstack/charts/tooltip'
const rows = [
{ week: 'May 4', value: 820 },
{ week: 'May 11', value: 960 },
{ week: 'May 18', value: 1_140 },
]
export const chart = defineChart({
marks: [
lineY(rows, { x: 'week', y: 'value', points: true }),
crosshair({ x: { label: true }, y: false }),
],
x: { scale: scalePoint },
y: { scale: scaleLinear },
focus: 'nearest-x',
maxFocusDistance: Number.POSITIVE_INFINITY,
tooltip,
})
```
Keep the finite default focus distance when empty space should clear inspection.
## Core Patterns
### Choose interaction ownership
| Need | Owner |
| ---------------------------------------------------------------------------- | ------------------------------------------ |
| Nearest datum, grouped tooltip, snapped crosshair, keyboard point navigation | chart focus |
| Semantic selection, free cursor, handle, brush, zoom, interactive legend | first-party control plus controlled signal |
| Shared accepted range, persistence, playback, editing, rich pinned details | application state/UI |
Read [the interaction state matrix](references/interaction-state.md) before combining controls.
### Coordinate chart and application selection by key
```ts
import { defineChart, dot } from '@tanstack/charts'
import { controlledSignal } from '@tanstack/charts/interaction/signal'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { keyedSelection, whenSelected } from '@tanstack/charts/selection'
const observations = [
{ id: 'a', speed: 12, efficiency: 32 },
{ id: 'b', speed: 18, efficiency: 27 },
]
let selectedId: string | null = null
const selection = keyedSelection<
(typeof observations)[number],
string,
number,
number
>({
selected: controlledSignal(selectedId, (next) => {
selectedId = next
}),
key: (datum) => datum.id,
})
export const chart = defineChart({
marks: [
dot(observations, {
id: 'observations',
x: 'speed',
y: 'efficiency',
key: 'id',
}),
whenSelected(
dot(observations, {
id: 'selected-observation',
x: 'speed',
y: 'efficiency',
key: 'id',
r: 7,
}),
selection,
),
],
x: { scale: scaleLinear },
y: { scale: scaleLinear },
selection,
})
```
Rebuild the definition with the accepted controlled value. A signal is a snapshot and callback, not a hidden store.
### Synchronize semantic values, not pixels
Use one `createChartCursor` controller across definitions when charts should resolve the same x/y value through their own scales. Keep crosshair presentation in each definition. Never copy DOM coordinates or mutate another chart's SVG.
### Add portaling only for containment boundaries
Use native tooltip content first. Add `portal` when overflow, transforms, or stacking contexts clip the surface. Use framework adapter tooltip bodies only when the product requires rich interactive content.
## Common Mistakes
### CRITICAL Mutating SVG for focus presentation
Wrong:
```ts
onRender={({ svg }) => svg.insertBefore(activeBand, svg.firstChild)}
```
Correct:
```ts
marks: [
whenFocused(bandX(rows, { x: 'date' }), { match: 'x' }),
lineY(rows, channels),
]
```
DOM mutation bypasses scene identity, SSR, Canvas, React Native, motion, and cleanup. Current focus marks replace an older workaround that still appears in issue examples.
Source: GitHub issue 9; `API-FRICTION.md` F-178
### HIGH Focusing a point-less rule
Wrong:
```ts
whenFocused(ruleX(dates), { match: 'x' })
```
Correct:
```ts
focusGuideX(rows, { x: 'date', y: 'value', xRule: {} })
```
Rules emit no interaction points, so a focus filter has no candidate identity. Current focus-guide primitives replace this legacy pattern.
Source: GitHub issue 32; `API-FRICTION.md` F-237
### CRITICAL Treating callbacks as complete behavior
Wrong: attach only an `onRangeChange` or key callback to an overlay.
Correct: use the matching controlled control (`keyedSelection`, `continuousCursor`, `handleX`, `brushX`, or `zoomX`) and store its accepted semantic value in application state.
A callback alone does not define capture, clamping, cancellation, keyboard operations, or ownership.
Source: `API-FRICTION.md` F-075; `docs/guides/interactions-and-selections.md`
### HIGH Keeping tooltips inside clipped ancestors
Wrong:
```ts
tooltip: {
use: tooltip
}
```
Correct:
```ts
tooltip: {
use: (tooltip, portal)
}
```
Overflow, transforms, and stacking contexts can trap a correctly positioned tooltip.
Source: `API-FRICTION.md` F-133; `docs/guides/tooltips-and-focus.md`
### HIGH Letting decorative layers own duplicate points
Wrong: make every area, line, dot, label, and highlight layer over one observation independently focusable.
Correct: choose one semantic interaction owner and use `decorative`, `whenFocused`, or `whenSelected` for supporting presentation.
Duplicate points create repeated keyboard stops, focus candidates, activations, and tooltip rows.
Source: `API-FRICTION.md` F-218; `docs/guides/tooltips-and-focus.md`
### HIGH Tension: rich interaction versus portable rendering
Prefer marks, controls, semantic state, and host extensions over DOM-only overlays. Verify pointer and keyboard paths, static fallback, teardown, and any native equivalent.
See also: `ship-accessible-charts/SKILL.md` and `extend-tanstack-charts/SKILL.md`
## References
- [Interaction ownership and state machines](references/interaction-state.md)
See also: `coordinate-charts-with-tanstack/SKILL.md`, `ship-accessible-charts/SKILL.md`, and `update-and-animate-charts/SKILL.md` — interaction requires explicit cross-surface ownership, keyboard parity, and stable identity across updates.