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.

import { BrushChart } from "@sagui/ui";
Markdown
Loading demo

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.

Installation

Install the package and import the styles once. The installation guide covers the Tailwind setup.

$ npm install @sagui/ui

Usage

example.tsx
import { useState } from "react";
import { BrushChart } from "@sagui/ui";

export function ActiveUsers({ days }: { days: { date: number; value: number }[] }) {
  const [range, setRange] = useState<[number, number]>([days[days.length - 90].date, days[days.length - 1].date]);
  return <BrushChart data={days} label="Daily active users" unit="users" range={range} onRangeChange={setRange}
    annotations={[{ date: Date.UTC(2026, 2, 24), label: "v2", description: "Offline mode and shared spaces" }]} />;
}

Examples

Controlled range

Own the window with range and onRangeChange, for example to show the dates it covers.

Loading demo

API reference

BrushChart

A detailed time series with an overview strip. Drag the window to pan, its handles to resize, or across empty track to draw a new one; double click resets. Zoomed out, a seven point average and band replace the raw line.

Prop Type Default Description
data (required) { date: Date | number; value: number }[] – Points in ascending time order.
label (required) string – Names the chart for assistive technology.
unit string "" Unit after values in the tooltip and table.
formatValue (value: number) => string – Formats values in the tooltip and table.
formatTick (value: number) => string – Formats the value axis. Defaults to a compact number.
formatDate (date: Date) => string – Formats dates in the tooltip and announcements.
annotations { date: Date | number; label: string; description?: string }[] [] Events drawn as markers on both charts; labels show where they fit.
range [number, number] – Controlled window as epoch milliseconds. A new value glides the window there.
defaultRange [number, number] – Initial window when uncontrolled. Defaults to the whole series.
onRangeChange (range: [number, number]) => void – Called while the window is dragged, resized, stepped, or reset.
minSpan number 7 days Smallest window in milliseconds.
height number 240 Main plot height in pixels.
overviewHeight number 52 Overview strip height in pixels.
emptyLabel string "No data yet" Shown with fewer than two points.
className string – Extra class on the figure.

Keyboard interactions

Keys Action
ArrowLeftorArrowRightplot Moves the crosshair between points in view.
ArrowLeftorArrowRightwindow Pans by a tenth of the window; Shift or PageUp and PageDown pan by half.
+or-window Zooms in or out around the window's centre.
ArrowLeftorArrowRighthandles Moves that edge by one point; Shift moves by ten.
HomeorEnd Moves the window or edge to the start or end.
Escapeor0 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

  • 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.

Also in charts