Skip to content
EliteChart

Writing a drawing tool

Implement a custom drawing tool — anchors, hit testing, render hook, serialization.

A drawing tool is anchored geometry plus hit-test plus render. This guide walks through a complete custom tool — a diagonal "trade-zone" rectangle with two anchors and a tinted fill.

Quick example

code
import type { DrawingTool, Drawing } from '@elitechart/core';

interface TradeZoneDrawing extends Drawing {
  readonly kind: 'trade-zone';
  readonly anchors: readonly [
    { time: number; price: number },
    { time: number; price: number },
  ];
  readonly color: string;
}

export const tradeZone: DrawingTool<TradeZoneDrawing> = {
  kind: 'trade-zone',
  name: 'Trade Zone',
  anchorsRequired: 2,

  create(anchors): TradeZoneDrawing {
    return {
      id: `tz-${Date.now()}`,
      kind: 'trade-zone',
      anchors: [anchors[0], anchors[1]],
      color: '#22d3ee',
      lineWidth: 1,
      visible: true,
      locked: false,
    };
  },

  hitTest(d, p): boolean {
    const [a, b] = d.anchors;
    return p.time >= Math.min(a.time, b.time)
        && p.time <= Math.max(a.time, b.time)
        && p.price >= Math.min(a.price, b.price)
        && p.price <= Math.max(a.price, b.price);
  },

  render(ctx, d, project) {
    const [a, b] = d.anchors;
    const p1 = project(a);
    const p2 = project(b);
    ctx.fillStyle = `${d.color}22`;
    ctx.strokeStyle = d.color;
    ctx.lineWidth = d.lineWidth;
    ctx.fillRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
    ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
  },
};

Register the tool with the chart:

code
import { useChartStore } from '@elitechart/elitechart';
import { tradeZone } from './trade-zone';

useChartStore.getState().registerDrawingTool(tradeZone);

How it works

The contract is four fields and three methods.

kind — string id, used in serialized JSON and as the value passed to setActiveTool(kind).

name — display name in the left toolbar.

anchorsRequired — how many click points the drawing needs before it's committed. The drawing tool runs in "active" mode until this many anchors have been placed.

create(anchors) — initial drawing factory. Receives the clicked anchors, returns the persisted drawing record.

hitTest(drawing, point) — return true when point (in data-space) is inside the drawing's selectable area. Called on every mouse move while the chart isn't being dragged.

render(ctx, drawing, project) — the canvas paint hook. ctx is a Canvas 2D context already translated to the price-pane origin; project({ time, price }) gives you { x, y } pixel coordinates.

Serialization

Drawings serialize to plain JSON. Your custom drawing's record must be self-contained — no closures, no DOM nodes — because the same JSON is what gets persisted to localStorage and round-tripped through cloud sync (Phase 2).

If your drawing has state that's expensive to recompute (a path cache, for example), recompute it inside render. Don't pin it on the drawing record.

Variations

Drawing with N anchors (polyline)

code
{
  anchorsRequired: -1,  // -1 means "user double-clicks to commit"
  create(anchors) { return { id, kind: 'my-poly', anchors, ... }; },
  hitTest(d, p) { /* point-vs-line-segment for each segment */ },
  render(ctx, d, project) {
    ctx.beginPath();
    d.anchors.forEach((a, i) => {
      const p = project(a);
      i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y);
    });
    ctx.stroke();
  },
}

Snap to OHLC with magnet

The point passed to hitTest and render is already magnet-snapped if the magnet is enabled in the toolbar. No special handling needed.

API

Field / methodRequired
kindyes
nameyes
anchorsRequiredyes
create(anchors)yes
hitTest(d, p)yes
render(ctx, d, project)yes
defaultStyle?no
keybinding?no