Charts
Drawing tools
Trend lines, Fibonacci levels, shapes and notes, with magnet, undo and JSON.
Read this page as Markdown, or take the whole documentation in one file.
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 32 Tools
Every kind below is placed by clicking, moved by dragging, edited by its handles, hit-tested
for tooltips and serialized to the same JSON shape. POINTS_REQUIRED is exported, so a
custom tool rail can show progress while a drawing is being placed.
Lines and Freehand
| Tool | kind |
Points | Notes |
|---|---|---|---|
| Trend line | trendline |
2 | Extends left, right, both or neither through style.extend. |
| Ray | ray |
2 | A trend line that always extends right. |
| Horizontal line | horizontalLine |
1 | Price line across the pane with a value label. |
| Horizontal ray | horizontalRay |
1 | A price line from its anchor rightward. |
| Vertical line | verticalLine |
1 | Time line across the pane. |
| Arrow | arrow |
2 | A line with a head at the second point, labeled from style.text. |
| Curved arrow | curvedArrow |
3 | The third point bends the curve. |
| Polyline | polyline |
Open ended | A click per vertex, ended by a double-click or Enter. |
| Brush | brush |
Open ended | Samples the pointer while the button is held, smoothed. |
Shapes and Channels
| Tool | kind |
Points | Notes |
|---|---|---|---|
| Rectangle | rectangle |
2 | Filled at style.fillOpacity, optional label from style.text. |
| Ellipse | ellipse |
2 | The two points are opposite corners of its bounding box. |
| Triangle | triangle |
3 | Filled at style.fillOpacity. |
| Parallel channel | parallelChannel |
3 | Two points define the base line, the third the width. |
| Regression trend channel | regressionChannel |
2 | A least-squares fit between the two times, with a band at style.deviation standard deviations either side. |
Fibonacci, Gann and Pitchfork
| Tool | kind |
Points | Default levels |
|---|---|---|---|
| Fibonacci retracement | fibRetracement |
2 | 0, 0.236, 0.382, 0.5, 0.618, 0.786, 1 |
| Fibonacci extension | fibExtension |
3 | 0, 0.618, 1, 1.618, 2.618, 3.618, 4.236 |
| Fibonacci fan | fibFan |
2 | 0.236, 0.382, 0.5, 0.618, 0.786 |
| Fibonacci arcs | fibArcs |
2 | 0.382, 0.5, 0.618, 1 |
| Fibonacci time zones | fibTimeZone |
2 | 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, counted in bars |
| Fibonacci channel | fibChannel |
3 | 0, 0.236, 0.382, 0.5, 0.618, 1, 1.618 |
| Gann fan | gannFan |
2 | 1/8, 1/4, 1/3, 1/2, 1, 2, 3, 4, 8, as price per bar |
| Pitchfork | pitchfork |
3 | 1, the median line and its two tines; add 0.5 for the half lines |
All eight read style.levels, so replacing the array changes which lines are drawn.
LEVELS_BY_KIND holds the sets that differ from the retracement default, and levelsFor
returns the one a kind starts with.
Measurement and Position
| Tool | kind |
Points | Notes |
|---|---|---|---|
| Measure | measure |
2 | Price change, percent change and bar count between two points. |
| Price range | priceRange |
2 | The vertical move alone, colored up or down. |
| Date range | dateRange |
2 | The horizontal span alone, in bars and in elapsed time. |
| Date and price range | datePriceRange |
2 | Both readouts in one box. |
| Long position | longPosition |
3 | Entry, target and stop, shaded profit and loss, with the reward-to-risk ratio. |
| Short position | shortPosition |
3 | The same, inverted. |
Annotation
| Tool | kind |
Points | Notes |
|---|---|---|---|
| Text | text |
1 | A text note anchored to a bar and a price. |
| Callout | callout |
2 | Text in a rounded box with a tail to the anchor. |
| Note with a leader line | note |
2 | A dot at the anchor, an underlined note at the second point. |
| Price label | priceLabel |
1 | The price at a bar as a filled tag, prefixed by style.text. |
TEXT_KINDS names the three that ask the host for their text as they are placed, through
requestText.
What the Rail Shows
The shell's tool rail shows nine of these by default, because 32 icons crowd a rail: trend
line, ray, horizontal line, vertical line, rectangle, parallel channel, Fibonacci
retracement, measure and text. Its drawingTools option names the set you want instead, in
the order you want it. See The toolbar.
TradingView ships roughly 90 drawing tools, counting the variations within each family. The 32 here are the ones a chart reaches for: the whole Fibonacci set, the channels, the ranges, the position tools and the annotations. Pattern tools, Elliott wave and XABCD among them, are the family that is not here.
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
deviation: number; // 2, the regression channel half-width in standard deviations
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.