Custom drawing tool
Build your own drawing tool with defineTool.
"use client";
import { ChartCanvas, Pane, withAlpha } from "@reincharts/core";
import { LineSeries } from "@reincharts/series";
import { Grid, XAxis, YAxis } from "@reincharts/axes";
import { Crosshair } from "@reincharts/coordinates";
import {
defineTool,
type Drawing,
Drawings,
DrawingStyleToolbar,
DrawingToolbar,
useDrawings,
} from "@reincharts/interactive";
import { weather, type WeatherDay } from "@/lib/data-general";
import { useDocsChartTheme } from "@/lib/use-docs-theme";
/** A custom tool: highlights a time range over the full pane height, with its length in days. */
const timeBand = defineTool({
type: "timeBand",
label: "Time Band",
group: "custom",
icon: (
<svg viewBox="0 0 18 18" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.2">
<rect x="5" y="2.5" width="8" height="13" rx="1" fill="currentColor" fillOpacity="0.2" />
<path d="M5 2v14M13 2v14" />
</svg>
),
points: 2,
defaults: { color: "#f59e0b" },
styleControls: ["color"],
draw(ctx, g, _drawing, state) {
const [a, b] = g.points;
if (!a || !b) {
return;
}
const left = Math.min(a.x, b.x);
const width = Math.abs(b.x - a.x);
ctx.fillStyle = withAlpha(g.color, state.selected || state.hovered ? 0.2 : 0.12);
ctx.fillRect(left, 0, width, g.height);
ctx.fillStyle = g.color;
ctx.fillRect(Math.round(left), 0, 1, g.height);
ctx.fillRect(Math.round(left + width), 0, 1, g.height);
const bars = Math.round(Math.abs(g.logical(g.time(b.x)) - g.logical(g.time(a.x))));
ctx.font = "12px sans-serif";
ctx.textAlign = "center";
ctx.fillText(`${bars} days`, left + width / 2, 16);
},
hitTest(g, x) {
const [a, b] = g.points;
return !!a && !!b && x >= Math.min(a.x, b.x) && x <= Math.max(a.x, b.x);
},
// drag handles at mid-height instead of the clicked prices
handles: (g) => g.points.map((p) => ({ x: p.x, y: g.height / 2 })),
});
const last = (back: number) => weather[weather.length - 1 - back].date.getTime();
const defaultDrawings: Drawing[] = [
{
id: "band",
type: "timeBand",
pane: "main",
points: [
{ time: last(131), price: 0 },
{ time: last(86), price: 0 },
],
style: {},
},
];
export default function CustomTool() {
const theme = useDocsChartTheme();
// custom tools are registered next to (or instead of) the built-in ones
const drawings = useDrawings({ tools: [timeBand], defaultDrawings });
return (
<div className="flex size-full flex-col">
<DrawingToolbar
controller={drawings}
tools={["timeBand", "trendLine", "rectangle"]}
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>
);
}A tool is a plain object: how many points the user places, how to draw the drawing and how to hit-test it. Register it
with useDrawings({ tools: [myTool] }) and it gets everything the built-in tools have: toolbar button, placement with
magnet, selection, drag handles, undo / redo, copy / paste, style toolbar and JSON persistence.
import { withAlpha } from "@reincharts/core";
import { defineTool, useDrawings } from "@reincharts/interactive";
const timeBand = defineTool({
type: "timeBand", // stored in Drawing.type
label: "Time Band", // toolbar tooltip
group: "custom", // toolbar group (custom groups are appended)
icon: <svg viewBox="0 0 18 18">{/* 18×18 icon */}</svg>,
points: 2,
defaults: { color: "#f59e0b" },
styleControls: ["color"],
draw(ctx, g, drawing, state) {
const [a, b] = g.points; // pane plot pixels
if (!a || !b) return; // still being placed
ctx.fillStyle = withAlpha(g.color, state.selected ? 0.2 : 0.12);
ctx.fillRect(Math.min(a.x, b.x), 0, Math.abs(b.x - a.x), g.height);
},
hitTest: (g, x) => x >= Math.min(g.points[0].x, g.points[1].x) && x <= Math.max(g.points[0].x, g.points[1].x),
});
const drawings = useDrawings({ tools: [timeBand] });The geometry
draw and hitTest receive a Geometry: the drawing's points converted to pane pixels, the pane size, the resolved
color, the theme, and converters (x(time), y(price), time(x), price(y), logical(time)), formatters
(formatPrice, formatTime) and extend(a, b, mode) for rays. The canvas is translated to the pane plot origin and
clipped.
Optional hooks
handles(g)/dragHandle(g, index, p): custom edit handles (a position's target / stop handles).build(points): turn the placed points into the stored ones (positions add the stop).crossValue(drawing, logical, logicalOf): the drawing's price at a bar, to supportonCrossalerts.editableText/textAnchor: inline text editing.axisLabels(drawing): prices / times always labelled on the axes.points: "multi"+freehand: paths and brushes.
Helpers
The helpers used by the built-in tools are exported too:
| Helpers | Purpose |
|---|---|
strokeOf, fillOf, segment, polyline, fillPolygon, arrowHead | Draw with the drawing's color, width and dash |
label, multiLabel, text, textBox, handle | Labels, stats boxes, text and edit handles |
dist, distToSegment, nearSegment, nearPolyline, inRect, inPolygon, rectOf, TOLERANCE | Hit testing |
extendSegment, constrainAngle, linearRegression, regression, lineCross | Rays, 45° snapping, regressions, crossValue |
createGeometry, createEditor, renderOrder, drawingId | The editing engine behind <Drawings> |
icons | The 18 × 18 icons of the built-in tools |