Brush & Zoom
Data zoom with animated transitions and adaptive downsampling.
The useBrush hook and BrushSlider component provide data zooming with scroll-to-zoom, drag handles, spring-animated transitions, and optional LTTB downsampling for large datasets.
Quick start
import { BrushSlider, Chart, LineMark, useBrush, XAxis, YAxis } from "@exhibit/charts";
function ZoomableChart({ data }) {
const { visibleData, startIndex, endIndex, onBrushChange, containerRef } =
useBrush({
data,
defaultZoom: { start: 20, end: 80 },
pointBudget: 200,
pointBudgetKey: "value",
});
return (
<div ref={containerRef}>
<Chart data={visibleData} height={300} xDataKey="date">
<XAxis />
<YAxis />
<LineMark dataKey="value" />
</Chart>
<BrushSlider
data={data}
dataKey="value"
startIndex={startIndex}
endIndex={endIndex}
onChange={onBrushChange}
/>
</div>
);
}useBrush options
| Option | Type | Default | Description |
|---|---|---|---|
data | T[] | required | Full dataset |
defaultZoom | { start: number; end: number } | full range | Initial visible range as percentages (0-100) |
minVisibleItems | number | 20 | Minimum points visible when zoomed in |
zoomFactor | number | 0.08 | Scroll-to-zoom sensitivity (0-1) |
pointBudget | number | — | Max visible points; applies LTTB downsampling when exceeded |
pointBudgetKey | string | auto-detected | Numeric key for the LTTB accessor |
useBrush return value
| Field | Type | Description |
|---|---|---|
visibleData | T[] | Sliced (and optionally downsampled) data for the current window |
startIndex | number | Current window start index (animated) |
endIndex | number | Current window end index (animated) |
onBrushChange | (range) => void | Handler for BrushSlider |
containerRef | RefObject | Attach to the scroll container for wheel-to-zoom |
Adaptive point budget (LTTB)
Large datasets can have thousands of points, but a 600px-wide chart can only display ~600 distinguishable pixels. Rendering more points than that wastes SVG complexity without visual benefit.
The pointBudget option caps the visible point count using LTTB (Largest-Triangle-Three-Buckets) — the industry-standard algorithm used by Grafana, Plotly, and other visualization tools. LTTB preserves peaks, valleys, and visual shape far better than uniform decimation.
useBrush({
data: thousandPoints,
pointBudget: 200, // never render more than 200 SVG points
pointBudgetKey: "price", // optimise for this numeric field
});When zoomed out (many points visible), LTTB reduces to the budget. When zoomed in past the budget, all actual data points are shown — no downsampling occurs.
The lttbDownsample function is also exported for standalone use:
import { lttbDownsample } from "@exhibit/charts";
const reduced = lttbDownsample(data, 100, (d) => d.value);BrushSlider props
| Prop | Type | Default | Description |
|---|---|---|---|
data | Record<string, unknown>[] | required | Full dataset (always the complete array) |
dataKey | string | required | Key for the sparkline preview |
startIndex | number | required | Current start index |
endIndex | number | required | Current end index |
onChange | (range) => void | required | Called on handle/region drag |
color | string | var(--color-chart-1) | Sparkline colour |
height | number | 48 | Slider height in pixels |