Skip to content

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 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.