Drawings
Letting the user draw
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 endedWhile 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 anchor | Effect |
|---|---|
Shift | Keeps 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:
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:
| Mode | Snaps |
|---|---|
off | Never (default) |
weak | Within 16 pixels of one of the four values |
strong | Always, 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.
| Type | Anchors | Handles and labels |
|---|---|---|
trend-line | 2 | Segment |
ray | 2 | Extends past the second anchor |
extended-line | 2 | Extends both ways |
arrow | 2 | Filled arrowhead at the second anchor |
horizontal-line | 1 | Full width; its value stays labelled on the price axis |
horizontal-ray | 1 | From the anchor to the right; its value stays labelled on the price axis |
vertical-line | 1 | Full height; its time stays labelled on the time axis |
rectangle | 2 | Eight handles: corners resize both ways, edge middles move one edge |
parallel-channel | 3 | Base line through anchors 1–2, parallel through anchor 3; the parallel's ends set the width |
fib-retracement | 2 | Levels 0, 0.236, 0.382, 0.5, 0.618, 0.786 and 1, each in its own color with a tinted band |
price-range | 2 | Change, percent change and bars between the anchors, with an arrow in the up or down color |
date-range | 2 | Bars and time between the anchors, e.g. 24 bars · 6h |
date-price-range | 2 | Change, percent change, bars and time |
long-position | 3 (2 placed) | Entry, target and stop; see positions |
short-position | 3 (2 placed) | The same for a short trade |
path | 2 or more | Polyline, finished with a double click or Enter |
text | 1 | One 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.
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.
| Action | Effect |
|---|---|
Alt + drag a drawing | Drags a copy and leaves the original in place |
Ctrl/⌘ + C, Ctrl/⌘ + V | Copies the selected drawing and pastes it 5 bars further |
| Double-click a text | Edits it in place |
Delete / Backspace | Removes the selected drawing |
Escape during a drag | Restores 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:
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
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 });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.
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.
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:
| Member | Default |
|---|---|
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 |
editableText | false; true edits the text in place on placement and double-click |
textAnchor | 0: 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:
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:
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:
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
};