Turning on the drawing tools
Read this guide if you want to mount drawing tools — trend lines, horizontal lines — onto the chart, or blow a pane up to full screen with a double-click.
The drawing tools and paneMaximize are not core; they are extensions mounted on top of core — both use the same plugin contract as Building your own indicator. This page shows what interaction vocabulary those extensions hand your users.
Drawing tools
import { drawingTools } from "@finchart/tools";
// Mount onto a pane — a drawing lives by that pane's value axis. `plot` comes
// separately, for x and the input stack.
const tools = plot.mainPane.use(drawingTools({ plot }));
tools.add({ type: "horizontal", price: 105 });
tools.select(handle); // select programmatically
tools.select(null); // clear the selection
tools.clear(); // erase everything
tools.undo(); // one committed add/remove/update/drag
tools.redo();canUndo() / canRedo() answer button state, including an in-flight draft or drag, and historyChanges announces when to read them again. A drag becomes one command on release. clear() and a successful load() replace a document and empty both stacks.
A full running example — down to the clear-selection, delete and erase-all buttons — is in Drawing tools. Saving drawings across sessions — serialize() / load(), keyed by symbol and interval — is in the @finchart/tools README.
paneMaximize — one pane fills the chart
import { paneMaximize } from "@finchart/core";
const maximize = plot.use(paneMaximize({ gestures: true }));gestures: true is opt-in — double-clicking a pane is already taken by doubleClickReset and used to reset to the full view (on by default — for the table see the interaction options table in the Plot contract). If maximize ate the same gesture by default, that reset would die quietly on every pane click — which is why you turn it on explicitly.
The plugin is the toggle and the gestures; the state is the chart's (plot.maximizedPane, set with plot.maximizePane). A maximize writes no pane's flex — the split the user arranged is still there when it ends — and every change rings panesChange once, even for a lone pane whose height doesn't move, so a pressed-state toolbar follows that one event.
Alongside the drawing tools, the drawing answers first
paneMaximize with gestures: true and the drawing tools both watch dblclick and Esc. The maximize gestures sit below the default input priority, so they only get what nothing on the chart claimed: a double-click on a shape selects the shape, and Esc cancels the line being drawn (or clears the selection) before it restores from maximize — whatever order the two were installed in. A toolbox given a negative priority drops below them on purpose.
The keyboard contract
The container element (the one you passed to build()) always takes focus (tabIndex 0) — not the canvas. keyboard: false does not turn that focus off either; what that option turns off is the two "core" rows in the table below. To pull it out of the tab order entirely, give that element tabindex="-1" yourself.
And the name is the caller's to give. Core makes that container a tab stop but hands it neither a name nor a role — because core does not know what your chart draws. Leave it off and what a screen reader user arrives at is an unnamed container.
<div id="chart" role="img" aria-label="AAPL daily candles, January to June 2024"></div>The full contract is in plot-contract's keyboard controls (accessibility) — this side is a summary of that section. keyboard: false does not reach as far as tabIndex — because pointer.ts attaches tabIndex outside the option branch.
| Key | What it does | Owner |
|---|---|---|
← → | pan by 5% of the visible width | core |
+ − | zoom about the center | core |
Esc | cancel the drawing → undo the drag → clear the selection (in that order) | drawing tools |
Esc | (when the drawing tools have nothing to drop) restore the maximized pane | paneMaximize |
Delete Backspace | delete the selected drawing | drawing tools |
] [ | cycle the drawing selection (next/previous, wraps at the ends) | drawing tools |
| double-click a pane | toggle maximize on that pane | paneMaximize({ gestures: true }) |
↑ ↓ (with Shift, 40px) | move a focused pane divider 8px | pane divider |
Home End | move a focused pane divider to its limit | pane divider |
The divider rows belong to the handle between panes, which is a tab stop of its own; the keys it handles stop there and never reach the container.
Ctrl/⌘+Z does not pass through this table yet: the DOM host deliberately keeps modified keys outside the normalized input stack. Bind it on the chart element and call tools.undo() / tools.redo() directly. That recipe is unambiguous with one toolbox; with one toolbox per pane, the application must choose its active pane because the toolbox's cursor-based keyboard owner is private.
The owner is whoever contends under the cursor. Mount one drawing-tools instance per pane and Delete, ] and [ belong to the pane the cursor last crossed. Over a spot where nobody contends — an axis, a margin, an indicator pane with no toolbox — the last owner stays the owner: going to the axis to read a price label and then hitting Delete is a normal path. Only Esc skips this verdict (it is the escape key — press it anywhere and whatever you were doing ends).
Right-click picks but does not eat — on a line it selects that line, on empty space it clears the selection (same rule as the left button). And it returns false, so the contextmenu event still fires. Your app reads tools.selection() inside it and builds the menu:
plot.on("contextmenu", ({ position }) => {
const target = tools.selection(); // the drawing under the cursor, null if none
openMenu(position, target);
});We do not block the native menu. @finchart/dom calls preventDefaultonly when a consumer ate the event, and the toolbox deliberately does not eat it — so your app has to attach a contextmenu listener on its own container and block it there. Leave it unblocked and the browser menu comes up on top of your app's menu.
A key passes through the input stack once — if a consumer does not eat it, it moves on to the next. Who owns Esc is decided by the same rule as the warning above (mounted later, asked first). A key nobody ate goes to core's pan/zoom. Nobody takes Tab — moving focus belongs to the browser.
To do the same thing programmatically, mimic the real key input — routeInput goes through the input stack unchanged, so it is the same path a genuine keyboard event takes.
plot.routeInput({ type: "keydown", key: "Delete" });Why the two vocabularies are separate extensions
The drawing tools and paneMaximize need not know about each other — the two negotiate over a single Esc, and only through the chain. That they landed with no core commit is itself evidence that this negotiation needs nothing but extension-side wiring. The design case for narrowing a requirement with capability interfaces is in Building your own indicator.