Skip to main content

Drawing tools

vroom ships interactive drawing tools for annotating the chart. Drawings are anchored in data space (time + price), so they stay glued to the candles as the user pans and zooms.

Drawing is web only today (React Native support is planned). Three tools are available: the line (a two-point trendline), the box (an axis-aligned rectangle), and the pencil (a freehand stroke tool).

There are two ways to manage the drawings themselves:

  • Managed persistence (recommended) — inject a storage adapter into VroomChart that gives vroom access to a store (e.g. local storage, MMKV).
  • Controlled — you own the Drawings array and persist them however you'd like.

The controlled drawings model

Drawings are a controlled prop: the chart renders what you pass and hands you every change through callbacks so you can manage/store. Unless you really need full control of drawings managed persistence is probably what you want.

import { useState } from "react";
import {
VroomChart,
type ChartMode,
type DrawTool,
type Drawing,
} from "@vroomchart/react";

function Chart({ candles }) {
const [mode, setMode] = useState<ChartMode>("pan");
const [tool, setTool] = useState<DrawTool>(null);
const [drawings, setDrawings] = useState<Drawing[]>([]);

return (
<VroomChart
candles={candles}
mode={mode}
tool={tool}
drawings={drawings}
onDrawingComplete={(d) => setDrawings((p) => [...p, d])} // add
onDrawingChange={(d) =>
setDrawings((p) => p.map((x) => (x.id === d.id ? d : x)))
} // move
onDrawingDelete={(id) => setDrawings((p) => p.filter((x) => x.id !== id))} // remove
onModeChange={setMode}
/>
);
}

Persisting drawings

Owning the array yourself is often more than you want. Instead, pass a drawingStore adapter and it manages and persists the drawings for you. The chart keys storage by seriesKey (the market identity), passed to your adapter as marketId.

import { useMemo } from "react";
import { VroomChart, type DrawingStore } from "@vroomchart/react";

function Chart({ candles, asset }) {
const drawingStore = useMemo<DrawingStore>(
() => ({
load: (marketId) => localStorage.getItem(`drawings:${marketId}`), // string | null
save: (marketId, data) =>
localStorage.setItem(`drawings:${marketId}`, data), // opaque string
}),
[],
);

return (
<VroomChart
candles={candles}
seriesKey={asset} // the market — also the storage key
drawingStore={drawingStore}
// no `drawings` / onDrawing* — the chart owns them
/>
);
}

Async & other backends

load and save may return promises, so the same adapter shape works for React Native (AsyncStorage / MMKV) or a remote backend:

const drawingStore: DrawingStore = {
load: (marketId) => AsyncStorage.getItem(`drawings:${marketId}`), // Promise<string | null>
save: (marketId, data) => AsyncStorage.setItem(`drawings:${marketId}`, data),
};

Saves are debounced by the chart (so dragging a handle doesn't thrash storage) and flushed on market switch and unmount. seriesKey is required for persistence — without it, drawings work in-session but aren't saved.

Activating a drawing tool

To start drawing, put the chart in draw mode and pick a tool — 'line', 'box' or 'pencil':

setMode("draw");
setTool("line"); // or 'box' | 'pencil'

How you trigger that is up to you: a toolbar button, a menu, or a keyboard shortcut. vroom deliberately does not bind a global hotkey, so it won't collide with your app's shortcuts.

useEffect(() => {
const onKey = (e: KeyboardEvent) => {
// "l" for "line"...get it?
if (e.key.toLowerCase() !== "l" || e.metaKey || e.ctrlKey) return;
const drawing = mode === "draw";
setMode(drawing ? "pan" : "draw");
setTool(drawing ? null : "line");
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [mode]);

Undo & redo

Every committed drawing action — a finished stroke or shape, an endpoint drag, a delete — is one undo step. Draw two pencil strokes and press ⌘Z / Ctrl+Z: only the second disappears; again, and the first goes too. ⇧⌘Z / Ctrl+Shift+Z / Ctrl+Y redoes in order. In-progress gestures never enter history (Escape or ⌘Z cancels a pending point instead).

History is in-memory and session-only — the drawings persist (via drawingStore or your own state), the undo stack deliberately doesn't, matching TradingView, Excalidraw and Figma. In managed mode it's also scoped per seriesKey: switching markets resets it, so undo can never edit a market you're no longer looking at. Depth is capped at 100 steps (historyLimit to change).

Managed mode

With a drawingStore the shortcuts just work. For toolbar buttons, bind availability via onHistoryChange and trigger via historyRef:

import { useRef, useState } from "react";
import {
VroomChart,
type UndoRedoControls,
type UndoRedoState,
} from "@vroomchart/react";

function Chart({ candles, asset, drawingStore }) {
const [history, setHistory] = useState<UndoRedoState>({
canUndo: false,
canRedo: false,
});
const historyRef = useRef<UndoRedoControls | null>(null);

return (
<>
<button disabled={!history.canUndo} onClick={() => historyRef.current?.undo()}>
Undo
</button>
<button disabled={!history.canRedo} onClick={() => historyRef.current?.redo()}>
Redo
</button>
<VroomChart
candles={candles}
seriesKey={asset}
drawingStore={drawingStore}
onHistoryChange={setHistory}
historyRef={historyRef}
/>
</>
);
}

Controlled mode

When you own the array, own the history too — useDrawingHistory bundles both. Wire its handlers to the chart and its undo/redo to the onUndo/onRedo props (which fire on the keyboard shortcuts):

import { VroomChart, useDrawingHistory } from "@vroomchart/react";

function Chart({ candles }) {
const h = useDrawingHistory();

return (
<VroomChart
candles={candles}
drawings={h.drawings}
onDrawingComplete={h.onDrawingComplete}
onDrawingChange={h.onDrawingChange}
onDrawingDelete={h.onDrawingDelete}
onUndo={h.undo}
onRedo={h.redo}
/>
);
}

h.canUndo / h.canRedo drive buttons; h.replaceAll(list) swaps in a loaded document without recording an undo step. For fully custom state management, the bare DrawingHistory stack is exported too.

Platform support

Drawing tools are web only for now — the drawings prop and its callbacks type-check on React Native but don't render there yet. See Platform differences.