Charts
Drawing tools
Trend lines, Fibonacci levels, shapes and notes, with magnet, undo and JSON.
Attaching the Tools
Drawing tools are a primitive on a pane. Create them once and the returned object is the whole API: starting a tool, adding drawings from code, selecting, deleting, undo, redo and serialization.
import { createDrawingTools, createFinancialChart } from "@ortex-charts/financial";
const chart = createFinancialChart(el, { data: bars, resolution: "1D" });
const tools = createDrawingTools(chart.chart, {
magnetPx: 8,
onChange: (change) => {
if (change.type !== "selected" && change.type !== "toolChanged") {
saveDrawings(tools.getDrawings());
}
},
});
Using the shell instead gives you the same primitive with a tool rail already
wired to it, reachable as shell.drawings.
The Nine Tools
| Kind | Points needed | Notes |
|---|---|---|
trendline |
2 | Extends left, right, both or neither through style.extend. |
ray |
2 | A trend line that always extends right. |
horizontalLine |
1 | Price line across the pane with a value label. |
verticalLine |
1 | Time line across the pane. |
rectangle |
2 | Filled at style.fillOpacity, optional label from style.text. |
parallelChannel |
3 | Two points define the base line, the third the width. |
fibRetracement |
2 | Levels from style.levels, each labeled with price and ratio. |
measure |
2 | Shows the price change, percent change and bar count between two points. |
text |
1 | A text note anchored to a bar and a price. |
POINTS_REQUIRED is exported, so a custom tool rail can show progress while a drawing is
being placed.
TradingView ships roughly 90 drawing tools. These nine cover what most charts actually use, and the missing ones — the rest of the Fibonacci family, Gann fans, pitchforks, arrows and callouts, ellipses and triangles, polylines, brush, the long and short position tools with a risk-and-reward readout, and regression trends — are variations on the same geometry rather than new machinery. They are not here today.
Drawing From the User Interface
tools.startTool("trendline"); // next clicks place the points
tools.startTool("fibRetracement", { color: "#F5A524", extend: "right" });
tools.cancelTool();
While a tool is active, each click places a point and the drawing completes when it has the points it needs. Escape cancels. Once a drawing exists, clicking selects it, dragging moves it, and dragging a handle moves one point.
Keyboard behavior on a focused chart:
| Key | Effect |
|---|---|
| Escape | Cancel the active tool, or clear the selection. |
| Delete or Backspace | Remove the selected drawing. |
| Ctrl or Cmd + Z | Undo. |
| Ctrl or Cmd + Shift + Z, Ctrl + Y | Redo. |
Magnet
magnetPx snaps a placed or dragged price to the nearest open, high, low or close of the
bar under the cursor, when one is within that many pixels. It defaults to eight. Setting it
to zero disables snapping entirely, which is what a chart of a continuous value rather than
of bars wants.
Drawing From Code
tools.add({
kind: "trendline",
points: [
{ time: bars[120].time, price: bars[120].low },
{ time: bars[300].time, price: bars[300].high },
],
style: { color: "#22C08A", extend: "right", text: "Support" },
meta: { source: "auto-detected", confidence: 0.8 },
});
Only kind and points are required; the id, style, locked and visible are filled in.
meta is carried through serialization untouched, which is where you keep the identifier
that links a drawing back to whatever produced it.
tools.updateDrawing(id, { style: { color: "#EF4E5A" }, locked: true });
tools.remove(id);
tools.clear();
tools.select(id); // or null to clear the selection
tools.selected; // a copy of the selected drawing, or null
tools.undo();
tools.redo();
A locked drawing is drawn and hit-tested but cannot be moved, which is the right state for levels your application computed rather than the user drew.
The Drawing Shape
Drawings are plain JSON. Points are anchored to data, not to pixels, so they survive zooming, panning, a resolution change and a window resize.
interface Drawing {
id: string;
kind: DrawingKind;
points: Array<{ time: number; price: number }>;
style: DrawingStyle;
locked: boolean;
visible: boolean;
meta?: unknown;
}
interface DrawingStyle {
color: string; // empty string means the theme line color
lineWidth: number; // 1.5
lineStyle: "solid" | "dashed" | "dotted";
fillOpacity: number; // 0.12
extend: "none" | "right" | "left" | "both";
text?: string;
fontSize: number; // 12
levels?: number[]; // Fibonacci ratios
showLabels: boolean; // true
}
The Fibonacci defaults are [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]. Replace the array to
change which levels are drawn, including adding extension levels above one:
tools.add({
kind: "fibRetracement",
points: [low, high],
style: { levels: [0, 0.382, 0.5, 0.618, 1, 1.618, 2.618] },
});
DRAWING_STYLE_DEFAULTS is exported if you want to start from the defaults and change one
field.
Saving and Restoring
const json = JSON.stringify(tools.getDrawings());
tools.setDrawings(JSON.parse(json));
That is the whole persistence story. The chart never writes to storage. Where drawings live is your decision, and the two common answers are different:
- Per symbol. A trend line drawn on AAPL should be there on every chart of AAPL. This is
the most-requested behavior in the category and it is a storage question, so
the shell takes a
drawingsStorewithload(symbol)andsave(symbol, drawings)and does the rest. - Per layout. A drawing that belongs to one saved view. Pass
layoutIncludesDrawings: trueto the shell and drawings ride along inside the layout JSON.
Use one or the other, not both.
Change Events
createDrawingTools(chart.chart, {
onChange: (c) => {
switch (c.type) {
case "created": console.log("added", c.drawing.kind); break;
case "updated": console.log("moved", c.drawing.id); break;
case "deleted": console.log("removed", c.id); break;
case "selected": setSelected(c.id); break;
case "toolChanged": setActiveTool(c.kind); break;
}
},
});
selected and toolChanged fire for interface state and do not change the data, so
persistence should ignore them, which is why the example at the top of this page filters
them out.
Other Hooks
createDrawingTools(chart.chart, {
paneId: rsiPane.id, // tools on a pane other than the first
priceScaleId: "left", // anchor prices to another scale
defaultStyle: { color: "#4C8DFF", lineWidth: 2 },
requestText: async () => window.prompt("Note") ?? null,
onEdit: (drawing) => openMyStyleDialog(drawing),
priceFormatter: (price) => `$${price.toFixed(2)}`,
});
requestTextis called when a text drawing is placed. It defaults towindow.prompt; replace it with your own modal to keep the interface consistent.onEditfires on a double-click, which is where a style dialog belongs.priceFormattercontrols the labels on measures, Fibonacci levels and horizontal lines; it defaults to the main series price format.- One instance covers one pane. Drawing on two panes means two instances.
Hit Testing and Tooltips
Drawings take part in the chart hit test, so a hovered drawing reports through
chart.subscribeCrosshairMove in the hit field of the event, and a click reports through
chart.subscribeClick.
chart.chart.subscribeClick((e) => {
if (!e.hit) return;
const payload = e.hit.data as { drawing: Drawing; handle: number } | undefined;
if (payload) openDetailPanel(payload.drawing.id, payload.drawing.meta);
});
hit.id is the drawing id, hit.data.drawing is a copy of the drawing including your
meta, and hit.data.handle is the index of the handle under the pointer or −1 for the
body. That is enough to open your own context menu or detail panel.