Freehand
Brush and highlighter strokes for marking up a chart by hand.
"use client";
import { ChartCanvas, Pane } from "@reincharts/core";
import { LineSeries } from "@reincharts/series";
import { Grid, XAxis, YAxis } from "@reincharts/axes";
import { Crosshair } from "@reincharts/coordinates";
import { type Drawing, Drawings, DrawingStyleToolbar, DrawingToolbar, useDrawings } from "@reincharts/interactive";
import { weather, type WeatherDay } from "@/lib/data-general";
import { useDocsChartTheme } from "@/lib/use-docs-theme";
const last = weather.length - 1;
// time `back` days before the last day (fractions land between days)
const timeAt = (back: number) => {
const i = Math.floor(last - back);
const t = weather[i].date.getTime();
return i >= last ? t : t + (last - back - i) * (weather[i + 1].date.getTime() - t);
};
// a slightly wobbly, hand-drawn loop around a point
const loop = (back: number, value: number, rx: number, ry: number) =>
Array.from({ length: 44 }, (_, k) => {
const a = -0.6 + (k / 43) * (Math.PI * 2 + 0.9);
const wobble = 1 + 0.06 * Math.sin(a * 3) + k * 0.002;
return { time: timeAt(back - rx * wobble * Math.cos(a)), price: value + ry * wobble * Math.sin(a) };
});
// a stroke that follows the data between two days
const trace = (from: number, to: number) =>
Array.from({ length: from - to + 1 }, (_, i) => ({ time: timeAt(from - i), price: weather[last - from + i].temp }));
const defaultDrawings: Drawing[] = [
{ id: "peak", type: "brush", pane: "main", points: loop(108, 22.3, 10, 2), style: { color: "#ef4444", width: 2 } },
{
id: "spring",
type: "highlighter",
pane: "main",
points: trace(186, 170),
style: { color: "rgba(255, 193, 7, 0.4)", width: 14 },
},
{
id: "cold",
type: "highlighter",
pane: "main",
points: trace(30, 22),
style: { color: "rgba(56, 189, 248, 0.4)", width: 18 },
},
];
export default function FreehandTools() {
const theme = useDocsChartTheme();
const drawings = useDrawings({ defaultDrawings });
return (
<div className="flex size-full flex-col">
{/* drag on the chart to draw; the highlighter draws thick, translucent strokes */}
<DrawingToolbar controller={drawings} tools={["brush", "highlighter"]} orientation="horizontal" />
<div className="min-h-0 flex-1">
<ChartCanvas data={weather} theme={theme} barSpacing={3}>
<Pane id="main">
<Grid />
<LineSeries<WeatherDay> name="Mean temperature" y="temp" curve="monotone" />
<YAxis tickFormat={(t) => `${t.label}°`} />
</Pane>
<XAxis />
<Crosshair />
<Drawings controller={drawings} />
<DrawingStyleToolbar controller={drawings} />
</ChartCanvas>
</div>
</div>
);
}Pick a freehand tool and drag on the chart. The stroke is stored as a list of points in data units, so it stays on the data while users pan and zoom, and can be selected, moved, recolored, undone and saved like any other drawing.
| Tool | Type | Notes |
|---|---|---|
| Brush | brush | Smooth stroke, width 2 by default; an optional fill closes the shape |
| Highlighter | highlighter | Thick translucent stroke (width 14, semi-transparent yellow) |
Freehand strokes ignore the magnet and bar snapping, so they follow the pointer exactly. Change the look with
style.color (use alpha for transparency) and style.width:
drawings.add({ type: "highlighter", points, style: { color: "rgba(56, 189, 248, 0.4)", width: 18 } });Tool defaults (like the highlighter's width and color) are merged into drawings created from the toolbar or with
add; drawings passed in defaultDrawings or load use exactly the style they carry.
Both tools are in the toolbar's Freehand group; show only them with
<DrawingToolbar tools={["brush", "highlighter"]} />.