Skip to content

Drawings ​

Letting the user draw ​

ts
chart.drawings.startTool('trend-line');

chart.on('drawingsChange', ({ reason, ids }) => console.log(reason, ids)); // 'create', ['trend-line-1']
chart.on('toolChange', () => toolbar.reset()); // the tool ended

While a tool is active, the chart previews the next anchor under the pointer and the drawing in progress, and labels its anchors on the price and time axes. With a mouse, each anchor is a click: click, move to the next point, click again; holding the button never draws. On a touch screen, which has no hover, dragging places the next anchor where the finger lifts, and a loupe above the finger shows where it lands (interaction.touchMagnifier). path takes any number of anchors and finishes with a double click or Enter. The tool ends when the drawing is complete, on Escape, or with cancelTool(); the new drawing is selected.

While drawing or dragging an anchorEffect
ShiftKeeps the line horizontal, vertical or at 45° from the neighbouring anchor
Ctrl / ⌘Inverts the magnet: off becomes strong, weak or strong becomes off

Pressing or releasing a modifier updates the preview at once, without moving the pointer.

startTool() takes the style and text of the new drawing, and continuous keeps the tool active for drawing several in a row:

ts
chart.drawings.startTool('horizontal-line', { continuous: true, style: { color: '#e5484d' } });

Magnet ​

Anchors always snap to bar times, also beyond the latest bar. options.drawings.magnet snaps values on the main pane to the open, high, low or close of the bar under the pointer:

ModeSnaps
offNever (default)
weakWithin 16 pixels of one of the four values
strongAlways, to the nearest of them

Built-in types ​

fib-extension, pitchfork, date-range, date-price-range, long-position, short-position and callout are extras: register them with registerExtras(chart) (or one by one) before using them. The others are always there.

TypeAnchorsHandles and labels
trend-line2Segment
ray2Extends past the second anchor
extended-line2Extends both ways
arrow2Filled arrowhead at the second anchor
horizontal-line1Full width; its value stays labelled on the price axis
horizontal-ray1From the anchor to the right; its value stays labelled on the price axis
vertical-line1Full height; its time stays labelled on the time axis
rectangle2Eight handles: corners resize both ways, edge middles move one edge
parallel-channel3Base line through anchors 1–2, parallel through anchor 3; the parallel's ends set the width
fib-retracement2Levels 0, 0.236, 0.382, 0.5, 0.618, 0.786 and 1, each in its own color with a tinted band
price-range2Change, percent change and bars between the anchors, with an arrow in the up or down color
date-range2Bars and time between the anchors, e.g. 24 bars · 6h
date-price-range2Change, percent change, bars and time
long-position3 (2 placed)Entry, target and stop; see positions
short-position3 (2 placed)The same for a short trade
path2 or morePolyline, finished with a double click or Enter
text1One or more lines, written and edited in place

Positions ​

long-position and short-position plan a trade. The first click sets the entry, the second the end and the distance to the target; the stop starts mirrored, so the risk/reward ratio starts at 1. The entry line and the target and stop edges can be grabbed anywhere and dragged up and down; the markers on the sides stretch the position left and right. The entry stays between the stop and the target, and they stay on their sides of it.

The zones show the reward and the risk. Walking the bars after the entry, the position closes at the first bar reaching the target or the stop (the stop first when a bar reaches both); until then it shows the open profit or loss at the last close. Together with replay this makes a practice tool: place a position, step forward and see how it plays out.

ts
chart.drawings.add({
  type: 'long-position',
  points: [
    { time: entryTime, price: 100 }, // entry
    { time: endTime, price: 110 }, // target
    { time: endTime, price: 95 } // stop
  ]
});

Editing ​

Hovering a drawing shows its handles; hovering a handle enlarges it and sets a cursor that tells what dragging it does. Users select a drawing by clicking it, move it by dragging, and reshape it by dragging a handle, on a hovered or a selected drawing. The selected drawing is labelled on the price and time axes, with the range it covers.

ActionEffect
Alt + drag a drawingDrags a copy and leaves the original in place
Ctrl/⌘ + C, Ctrl/⌘ + VCopies the selected drawing and pastes it 5 bars further
Double-click a textEdits it in place
Delete / BackspaceRemoves the selected drawing
Escape during a dragRestores the drawing

Locked drawings can be selected but not moved or edited.

Text ​

Placing a text drawing opens an editor where it is painted: Enter keeps the text, Shift+Enter starts a new line, Escape or an empty text places nothing, and clicking elsewhere keeps it. Double-clicking a text, or chart.drawings.editText(id), edits it again.

A toolbar for the selected drawing ​

The chart leaves the toolbar to you, so it matches your product. chart.drawings.bounds(id) returns the area a drawing covers in chart coordinates, clipped to its pane, or null while it is hidden or off screen. The area moves with the view, the data and edits, so read it in an animation frame while a drawing is selected:

ts
let selected: string | null = null;

chart.on('selectionChange', ({ selection }) => {
  selected = selection?.kind === 'drawing' ? selection.id : null;
});

function follow() {
  const box = selected ? chart.drawings.bounds(selected) : null;
  toolbar.hidden = !box;

  if (box) {
    toolbar.style.left = `${box.x + box.width / 2}px`;
    toolbar.style.top = `${box.y - 48}px`;
  }

  requestAnimationFrame(follow);
}

requestAnimationFrame(follow);
colorPicker.onchange = (color) => chart.drawings.update(selected!, { style: { color } });

Programmatic changes ​

ts
const id = chart.drawings.add({
  type: 'horizontal-line',
  points: [{ time: Date.now(), price: 104.5 }],
  style: { color: '#e5484d', lineStyle: 'dashed' },
  locked: true
});

chart.drawings.update(id, { visible: false });
const copy = chart.drawings.duplicate(id);
chart.select({ kind: 'drawing', id: copy });
ts
interface DrawingInput {
  id?: string;
  type: string;
  pane?: string; // default 'main'
  points: Array<{ time: TimeInput; price: number }>;
  style?: Partial<{
    color: string;
    lineWidth: number;
    lineStyle: 'solid' | 'dashed' | 'dotted';
    fillColor: string;
    textColor: string;
    fontSize: number;
  }>;
  text?: string;
  locked?: boolean;
  visible?: boolean;
}

Undo and redo ​

User actions (creating, moving, copying, editing text, deleting) and add(), duplicate(), update(), remove() and clear() are undoable steps. set() replaces all drawings, for example when loading a document, and clears the history.

ts
chart.drawings.undo();
chart.drawings.redo();
chart.drawings.canUndo();

Ctrl/⌘ + Z, Ctrl/⌘ + Shift + Z and Ctrl + Y work while the chart has focus.

Persisting drawings ​

Drawings are part of chart.saveState(). To store them separately, listen to drawingsChange and save chart.drawings.list(); restore them with chart.drawings.set(saved).

Custom drawing types ​

A drawing type paints its shape and reports hits; the chart handles placement, the magnet and Shift, selection, handles, axis labels, dragging, copies, undo and persistence. All coordinates are relative to the pane's plot area.

ts
import type { DrawingTypeDefinition } from '@kumochart-sdk/core';

const cross: DrawingTypeDefinition = {
  points: 1,
  defaultStyle: (theme) => ({ color: theme.alert }),
  paint: ({ ctx, geometry }) => {
    const [anchor] = geometry.points;

    if (!anchor) {
      return;
    }

    ctx.strokeStyle = geometry.style.color;
    ctx.lineWidth = geometry.style.lineWidth;
    ctx.beginPath();
    ctx.moveTo(anchor.x - 12, anchor.y);
    ctx.lineTo(anchor.x + 12, anchor.y);
    ctx.moveTo(anchor.x, anchor.y - 12);
    ctx.lineTo(anchor.x, anchor.y + 12);
    ctx.stroke();
  },
  hitTest: (geometry, point, tolerance) => {
    const [anchor] = geometry.points;
    const distance = anchor ? Math.hypot(point.x - anchor.x, point.y - anchor.y) : Infinity;
    return distance <= 12 + tolerance ? { part: 'body', distance } : null;
  }
};

chart.drawings.registerType('cross', cross);
chart.drawings.startTool('cross');

geometry provides the anchors in pixels (points) and in data (anchors), the plot size, the resolved style, text, the theme, the data (bars, the displayed bars, and source, the source bars a replay has revealed, both BarsView), and helpers to convert values (valueToY), count bars (barIndex), format values (formatValue) and measure text (measureText). Use points: 0 for tools with any number of anchors.

The optional members shape the editing experience:

MemberDefault
handles(geometry)One handle per anchor, with ids '0', '1', …
moveHandle(anchors, handle, to)Replaces the anchor of the handle; required for other handle ids
axisMarks(anchors)Every anchor's value and time, while selected
bounds(geometry)The box around the handles
editableTextfalse; true edits the text in place on placement and double-click
textAnchor0: the anchor the text starts at, where the editor opens
place and complete(placed)The user places every anchor
levels(anchors, scale)No lines for alerts

A handle is { id, x, y, cursor?, line?, marker? }: line lets it be grabbed anywhere along a segment, such as a whole edge, and marker: false hides its circle; moveHandle receives the anchor under the pointer (already snapped by the magnet) and returns the new anchors. This rectangle-like type lets users drag only its top edge:

ts
const band: DrawingTypeDefinition = {
  points: 2,
  paint, // as above
  hitTest,
  handles: ({ points: [a, b] }) =>
    a && b ? [{ id: 'top', x: (a.x + b.x) / 2, y: Math.min(a.y, b.y), cursor: 'ns-resize' }] : [],
  moveHandle: ([a, b], _handle, to) =>
    a && b
      ? a.price > b.price
        ? [{ ...a, price: to.price }, b]
        : [a, { ...b, price: to.price }]
      : [],
  axisMarks: (anchors) => ({ prices: anchors.map((anchor) => anchor.price), times: [] })
};

Set always: true in axisMarks() to keep the labels while the drawing is not selected, as the horizontal line does.

levels() describes the lines alerts can watch, each through two points: { key, from, to, extend? }, where extend is none (between the points), end (on past to) or both; two points at the same time make a horizontal level. The chart interpolates between the points along the pane's scale. Use scale.toScale() and scale.fromScale() for lines offset as drawn, like the parallel of a channel:

ts
levels: ([start, end, through], scale) => {
  const offset = scale.toScale(through.price) - scale.toScale(start.price);
  const shift = (point) => ({
    ...point,
    price: scale.fromScale(scale.toScale(point.price) + offset)
  });
  return [
    { key: 'base', from: start, to: end },
    { key: 'parallel', from: shift(start), to: shift(end) }
  ];
};

When the user should place fewer anchors than the drawing keeps, set place and derive the others in complete(). It also runs for the preview, so the whole drawing follows the pointer. The position tools keep three anchors from two clicks:

ts
const position: DrawingTypeDefinition = {
  points: 3,
  place: 2,
  complete: ([entry, target]) =>
    entry && target
      ? [entry, target, { time: target.time, price: 2 * entry.price - target.price }]
      : [],
  paint,
  hitTest
};

Released under the MIT license.