DocumentationDrawing Tools

Drawing Tools

openalgo-charts/draw is a lazy tier (5.6 KB Brotli) with 18 tools and a headless controller. It ships no UI — no toolbar, no dialogs. It owns the model and the interactions; your app supplies the buttons.

import { createChart } from 'openalgo-charts';
import { DrawingController } from 'openalgo-charts/draw';
 
const chart = createChart(el);
chart.addSeries('candlestick').setData(bars);
 
const draw = new DrawingController(chart, { magnet: true });
draw.setTool('trend-line');   // the next two clicks place it

The tools

GroupidAnchors
Linestrend-line, ray, extended-line, arrow2
Horizontal / verticalhorizontal-line, horizontal-ray, vertical-line, cross-line1
Shapesrectangle, ellipse2
Channelsparallel-channel3
Fibonaccifib-retracement (2), fib-extension (3)2–3
Positionslong-position, short-position3
Measurementmeasure2
Annotationtext (1), path (free-form)1 / n

long-position and short-position report R:R and a risk-based position size from accountSize and risk in their style bag. measure reports price change, percent, and a bar count taken from logical indices — so it matches what the gapless axis actually shows rather than raw elapsed time.

Anchors live in data space

A drawing’s points are { time, price }, never pixels. The time axis is gapless — weekends, holidays, and session breaks collapse — so a pixel anchor would slide the moment the viewport changed. Anchors map through DataLayer.timeToIndexFloat, which also resolves positions between bars (inside a collapsed gap) and past the last bar, where trend projections and forecasts live.

Controller API

MemberDescription
setTool(id | null)Arm a tool for placement, or return to the cursor.
activeTool()The armed tool id, or null.
drawings() / get(id)The model.
add(drawing)Add one directly (import, or host-authored).
update(id, patch)Patch points, style, locked, or visible.
remove(id) / clear()Delete one, or all.
select(id | null) / selected()Selection.
undo() / redo() / canUndo() / canRedo()History.
toJSON() / fromJSON(data)Explicit serialisation.
setOptions(patch)magnet, stayInDrawingMode, historyLimit, defaultStyle.
destroy()Detach layers and listeners.

Events

draw:tool, draw:add, draw:update, draw:remove, draw:select — all on chart.on(...).

The controller listens on the event bus (click, crosshair:move, drag, drag:end), not the single-slot subscribeClick / subscribeDrag callbacks — so your app keeps using those for its own order lines without the two fighting over one channel.

Interaction

  • Place — click once per anchor, or press-drag-release to lay down a two-point shape in one gesture. A live preview follows the cursor between clicks.
  • Select — click a drawing; click empty space to deselect.
  • Move — drag the body to translate every anchor, or drag a handle to move one.
  • Magnet{ magnet: true } snaps new anchors to the nearest O/H/L/C of the hovered bar.
  • Undo — a whole drag is one undo step, not one per frame.
  • Lock — a locked drawing renders but cannot be selected or dragged.
💡

While a tool is armed the controller puts the chart in placement mode (chart.setPlacementMode(true)), so a press starts a shape instead of panning. The chart reports the gesture as two click events — the press point, then the release point tagged viaDrag. DrawingController manages this for you; you only need it if you are placing things (alerts, annotations) without the draw tier.

Text and labels

The text tool draws a standalone text box, but shapes carry labels too — set style.text on a rectangle, ellipse, or parallel channel and it renders inside the outline. That is one shape with two colours: color strokes the outline, fontColor paints the label.

Style keyApplies toNotes
texttext tool, shapes\n starts a new line
fontSizebothmedia px
fontFamilybothdefaults to the UI sans stack
fontWeightboth'normal' | 'bold'
fontStyleboth'normal' | 'italic'
fontColorbothfalls back to color
textAlignboth'left' | 'center' | 'right'
textVAlignboth'top' | 'middle' | 'bottom'
textPositionshapes only'inside' the outline or 'outside' just above it

| background, backgroundColor, backgroundOpacity | text tool | filled plate behind the text | | border, borderColor | text tool | stroked plate border | | wrap, wrapWidth | both | soft-wrap instead of running off the pane |

draw.add('rectangle', [a, b], {
  color: '#8b5cf6',
  text: 'Supply zone',
  fontColor: '#ffffff',
  fontSize: 13,
  fontWeight: 'bold',
  textPosition: 'inside',
  textVAlign: 'top',
  textAlign: 'center',
});
💡

With textPosition: 'inside', textVAlign x textAlign give the nine placements a TradingView shape-text panel exposes. 'outside' parks the block above the shape and ignores textVAlign. examples/yfinance/index.html wires all of it to a properties bar.

Persistence

Drawings round-trip through the chart state with no extra plumbing:

localStorage.setItem('layout', JSON.stringify(chart.getState()));   // includes drawings
 
// later
chart.restoreState(JSON.parse(localStorage.getItem('layout')));
const draw = new DrawingController(chart);   // picks them up from the state

See Chart State for the rest of the payload.

Building your own tool

A tool is a descriptor, like a chart type or an indicator. draw receives anchors already in device px; distance receives them in media px, the same space as the cursor.

import { registerDrawingTool, distToSegment } from 'openalgo-charts/draw';
 
registerDrawingTool({
  id: 'zigzag',
  name: 'Zig Zag',
  points: 3,
  defaultStyle: { color: '#f5a623', lineWidth: 2 },
  draw: ({ ctx, pts, style, rc }) => {
    ctx.strokeStyle = style.color;
    ctx.lineWidth = style.lineWidth * rc.dpr;
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (const p of pts.slice(1)) ctx.lineTo(p.x, p.y);
    ctx.stroke();
  },
  // Return a distance in media px, or null for a miss. 0 means "inside".
  distance: (x, y, { pts }) => Math.min(
    distToSegment(x, y, pts[0], pts[1]),
    distToSegment(x, y, pts[1], pts[2]),
  ),
});
 
draw.setTool('zigzag');

Geometry helpers are exported: distToSegment, distToLine, distToPolyline, distToRect, distToEllipse, rectOf, extendSegment.

Building a toolbar

The controller is deliberately headless, so a host toolbar is a handful of buttons. The yfinance example ships a full one: a vertical tool rail, a floating properties bar that appears on selection (colour grid, opacity, thickness, line style, lock, duplicate, delete) and can be dragged anywhere, plus Delete / Esc / Ctrl-Z key handling.

// tool rail
for (const tool of registeredDrawingTools()) {
  button(tool.name).onclick = () => draw.setTool(tool.id);
}
 
// properties bar on selection
chart.on('draw:select', ({ id }) => showPropertiesBar(id));
 
// keyboard
window.addEventListener('keydown', (e) => {
  if (e.key === 'Delete') { const id = draw.selected(); if (id) draw.remove(id); }
  if (e.key === 'Escape') draw.setTool(null);
  if ((e.ctrlKey || e.metaKey) && e.key === 'z') e.shiftKey ? draw.redo() : draw.undo();
});