Skip to content

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 drawingsStore with load(symbol) and save(symbol, drawings) and does the rest.
  • Per layout. A drawing that belongs to one saved view. Pass layoutIncludesDrawings: true to 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)}`,
});
  • requestText is called when a text drawing is placed. It defaults to window.prompt; replace it with your own modal to keep the interface consistent.
  • onEdit fires on a double-click, which is where a style dialog belongs.
  • priceFormatter controls 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.