Brush chart
A dense time series with an overview strip: drag a window to zoom, resize it by its handles, and read events in place.
datachartnew
Preview
Brush chart preview
Live · keyboard ready
Daily active users, Jul 1, 2026 to Aug 29, 2026. Showing Jul 21, 2026 to Aug 15, 2026.
| Date | Daily active users | Event |
|---|---|---|
| Jul 21, 2026 | 1,217 users | v2 launch. The redesign shipped on this day. |
| Jul 22, 2026 | 1,184 users | |
| Jul 23, 2026 | 1,157 users | |
| Jul 24, 2026 | 1,137 users | |
| Jul 25, 2026 | 1,124 users | |
| Jul 26, 2026 | 1,119 users | |
| Jul 27, 2026 | 1,122 users | |
| Jul 28, 2026 | 1,134 users | |
| Jul 29, 2026 | 1,154 users | |
| Jul 30, 2026 | 1,183 users | |
| Jul 31, 2026 | 1,218 users | |
| Aug 1, 2026 | 1,260 users | |
| Aug 2, 2026 | 1,307 users | |
| Aug 3, 2026 | 1,358 users | |
| Aug 4, 2026 | 1,412 users | |
| Aug 5, 2026 | 1,466 users | |
| Aug 6, 2026 | 1,519 users | |
| Aug 7, 2026 | 1,571 users | |
| Aug 8, 2026 | 1,618 users | |
| Aug 9, 2026 | 1,661 users | |
| Aug 10, 2026 | 1,697 users | |
| Aug 11, 2026 | 1,727 users | |
| Aug 12, 2026 | 1,748 users | |
| Aug 13, 2026 | 1,761 users | |
| Aug 14, 2026 | 1,765 users | |
| Aug 15, 2026 | 1,761 users |
When to use
- Product analytics with a year or more of daily data.
- Incident reviews where events need to be read against a metric.
- Any chart where people ask to zoom into a stretch without losing context.
When not to use
- Use line-chart for a few weeks of data or several series.
- Use sparkline for a small inline trend without interaction.
- Avoid it for data that dips below zero or needs a log scale.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "brush-chart",
"type": "registry:ui",
"title": "Brush chart",
"description": "A dense time series with an overview strip: drag a window to zoom, resize it by its handles, and read events in place.",
"categories": [
"data"
],
"dependencies": [
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/brush-chart/brush-chart.tsx",
"type": "registry:component",
"target": "@components/manicat/brush-chart/brush-chart.tsx"
},
{
"path": "registry/components/brush-chart/brush-chart.module.css",
"type": "registry:component",
"target": "@components/manicat/brush-chart/brush-chart.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/brush-chart",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/brush-chart",
"markdown": "https://uiarc.dev/components/brush-chart/markdown",
"tags": [
"data",
"chart",
"new"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { BrushChart } from "@/components/manicat/brush-chart/brush-chart";
import type { BrushChartDatum } from "@/components/manicat/brush-chart/brush-chart";
const DAY = 86_400_000;
function range(from: Date, days: number): Date[] {
return Array.from({ length: days }, (_, index) => new Date(from.getTime() + index * DAY));
}
const brushData: BrushChartDatum[] = range(new Date(Date.UTC(2026, 6, 1, 12)), 60).map((date, index) => ({
date: date.getTime(),
value: Math.max(400, 1200 + Math.round(260 * Math.sin(index / 5.5) + index * 7)),
}));
export function BrushChartExample() {
return (
<BrushChart
data={brushData}
label="Daily active users"
unit="users"
annotations={[{ date: brushData[20].date, label: "v2 launch", description: "The redesign shipped on this day." }]}
defaultRange={[brushData[20].date as number, brushData[45].date as number]}
/>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Zoomable timeline Drag the brush to focus on a range; an annotation marks the launch day.
Zoomable timeline
Drag the brush to focus on a range; an annotation marks the launch day. preview
Live · keyboard ready
Daily active users, Jul 1, 2026 to Aug 29, 2026. Showing Jul 21, 2026 to Aug 15, 2026.
| Date | Daily active users | Event |
|---|---|---|
| Jul 21, 2026 | 1,217 users | v2 launch. The redesign shipped on this day. |
| Jul 22, 2026 | 1,184 users | |
| Jul 23, 2026 | 1,157 users | |
| Jul 24, 2026 | 1,137 users | |
| Jul 25, 2026 | 1,124 users | |
| Jul 26, 2026 | 1,119 users | |
| Jul 27, 2026 | 1,122 users | |
| Jul 28, 2026 | 1,134 users | |
| Jul 29, 2026 | 1,154 users | |
| Jul 30, 2026 | 1,183 users | |
| Jul 31, 2026 | 1,218 users | |
| Aug 1, 2026 | 1,260 users | |
| Aug 2, 2026 | 1,307 users | |
| Aug 3, 2026 | 1,358 users | |
| Aug 4, 2026 | 1,412 users | |
| Aug 5, 2026 | 1,466 users | |
| Aug 6, 2026 | 1,519 users | |
| Aug 7, 2026 | 1,571 users | |
| Aug 8, 2026 | 1,618 users | |
| Aug 9, 2026 | 1,661 users | |
| Aug 10, 2026 | 1,697 users | |
| Aug 11, 2026 | 1,727 users | |
| Aug 12, 2026 | 1,748 users | |
| Aug 13, 2026 | 1,761 users | |
| Aug 14, 2026 | 1,765 users | |
| Aug 15, 2026 | 1,761 users |
zoomable-timeline-drag-the-brush-to-focus-on-a-range-an-annotation-marks-the-launch-day-.tsxtsx
"use client";
import { BrushChart } from "@/components/manicat/brush-chart/brush-chart";
import type { BrushChartDatum } from "@/components/manicat/brush-chart/brush-chart";
const DAY = 86_400_000;
const daily: BrushChartDatum[] = Array.from({ length: 60 }, (_, index) => ({
date: Date.UTC(2026, 6, 1, 12) + index * DAY,
value: 1200 + Math.round(260 * Math.sin(index / 5.5) + index * 7),
}));
export function BrushChartUsersExample() {
return (
<BrushChart
data={daily}
label="Daily active users"
unit="users"
annotations={[{ date: daily[20].date, label: "v2 launch", description: "The redesign shipped on this day." }]}
defaultRange={[daily[20].date as number, daily[45].date as number]}
/>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| ArrowLeft / ArrowRight (plot) | Moves the crosshair between points in view. |
| ArrowLeft / ArrowRight (window) | Pans by a tenth of the window; Shift or PageUp and PageDown pan by half. |
| + / - (window) | Zooms in or out around the window's centre. |
| ArrowLeft / ArrowRight (handles) | Moves that edge by one point; Shift moves by ten. |
| Home / End | Moves the window or edge to the start or end. |
| Escape / 0 | Resets the window to the whole series. |
Accessibility
- The window and both handles are sliders with dates as aria-valuetext.
- The plot is a focusable group; each reading is announced with its date, value, and any event on that day.
- A visually hidden table lists every point in the window with its event.
- Event markers pair the second series hue with a text label or tooltip; they never rely on color alone.
Motion
- The line draws in from the left once, the first time it is seen.
- Dragging tracks the pointer 1:1; presets, resets, and controlled changes glide the window on a spring.
- The value axis springs to the tallest point in view, and the raw line crossfades with the smoothed trend as density changes.
- Reduced motion places the window directly and keeps the crosshair without travel.
Responsive behavior
- Both charts fill their container; date ticks pick days, weeks, months, or years to fit the width.
- Handles have a 28 by 44px hit area for touch, and the strip keeps vertical page scroll free.
- Event labels are placed left to right and skip themselves when they would collide.
Performance
- Past two points per pixel, each pixel column keeps only its low and high, so spikes survive and paths stay small.
- The overview path is built once per width; the rolling average is computed once per data change.
- Window moves re-render one component; a few thousand points stay smooth.
Notes for AI
- Choose it for long daily or hourly series where people need both the whole history and a close look.
- Control range to sync presets (30D, 90D, 1Y) or other charts with the window.
- Use annotations for launches, incidents, and pricing changes; keep labels to a word or two.
- The value axis starts at zero, so it suits counts and totals rather than prices that hover far from zero.