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 itThe tools
| Group | id | Anchors |
|---|---|---|
| Lines | trend-line, ray, extended-line, arrow | 2 |
| Horizontal / vertical | horizontal-line, horizontal-ray, vertical-line, cross-line | 1 |
| Shapes | rectangle, ellipse | 2 |
| Channels | parallel-channel | 3 |
| Fibonacci | fib-retracement (2), fib-extension (3) | 2–3 |
| Positions | long-position, short-position | 3 |
| Measurement | measure | 2 |
| Annotation | text (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
| Member | Description |
|---|---|
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 key | Applies to | Notes |
|---|---|---|
text | text tool, shapes | \n starts a new line |
fontSize | both | media px |
fontFamily | both | defaults to the UI sans stack |
fontWeight | both | 'normal' | 'bold' |
fontStyle | both | 'normal' | 'italic' |
fontColor | both | falls back to color |
textAlign | both | 'left' | 'center' | 'right' |
textVAlign | both | 'top' | 'middle' | 'bottom' |
textPosition | shapes 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 stateSee 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();
});