Skip to content

Interaction ​

Built in, no setup needed. Each input path can be turned off with feature flags.

Pointer ​

GestureWhereEffect
DragPlotPan (with momentum when inertia is on); with plotDragScale, moving up or down also scales prices
Wheel / scrollPlotZoom time axis around the pointer (see wheel and zoomAnchor below)
Horizontal scroll (also with Cmd), Shift + wheelPlotPan
Ctrl/Cmd + wheelPlotZoom
PinchPlot (touch or trackpad)Zoom
Long-press, then dragPlot (touch)Inspect: the crosshair and legend follow the finger and the chart does not pan
DragPrice axisScale prices (turns off autoscale for that pane)
Double-clickPrice axisRestore autoscale
DragTime axisZoom time axis around the plot's right edge (see zoomAnchor below)
Double-clickTime axisJump to the newest bar
DragPane separatorResize panes
Alt + clickPlotLock the crosshair on that bar, or release it (see locking the crosshair)
Shift + clickDrawingAdd it to the drawing selection, or take it out (see selecting several drawings)
Shift + dragPlot, on a pane with drawingsSelection box: select the drawings inside it

When the newest bar is out of view, a floating button scrolls back to it (scrollButton option).

On touch, a press that stays still for about half a second starts inspecting. The touchInspect option sets how it ends: 'release' (default) when the finger lifts, 'tap' keeps the crosshair until the next tap, and 'off' turns the long-press off so the press pans as usual. Moving before the long-press is a normal pan. While the crosshair is locked, a hold is a normal pan too, and a tap on the plot releases the lock. With the sketch feature flag, a long-press held still for another half second becomes a sketch.

A plain touch moves the crosshair too while it pans. chart.inspecting tells the two apart: it is true while a long-press drives the crosshair, and while 'tap' keeps it afterwards. crosshair:move fires when inspecting starts, so read it there; the UI's pointer tooltip does.

Wheel and plot drag options ​

The wheel option sets what the plain wheel does: 'zoom' (default) zooms the time axis, 'pan' scrolls it (down moves to newer bars), and 'page' leaves it to the page, which suits charts inside a scrolling page. Ctrl/Cmd + wheel zooms and Shift + wheel pans in every mode, so a chart in 'page' mode stays usable. Shift + wheel pans along whichever axis the wheel moved most.

With plotDragScale: true, the vertical part of a plot drag scales the pane's main price scale and turns its autoscale off, as dragging the price axis does; the horizontal part still pans. A press that stays within the click distance does not scale, and on touch the chart keeps vertical swipes from scrolling the page. It needs the axisDragScale feature, and the cursor shows ns-resize while it scales. Double-click the price axis to restore autoscale.

ts
chart.applyOptions({ wheel: 'page', plotDragScale: true });

Zoom anchor ​

The zoomAnchor option sets the point a time zoom keeps fixed on screen:

AnchorKeeps fixed
'pointer'The bar under the pointer, or under the middle of the two fingers for a pinch. A time-axis drag uses the point where it was pressed. The + / - keys have no pointer, so they use the plot's right edge
'newest'The newest bar, while it is on screen, so live prices never slide away. When it is off screen, the zoom falls back to 'pointer'
'right-edge'The plot's right edge

It has one value per input: plot for the wheel, pinch (touch or trackpad) and the + / - keys, and timeAxis for dragging the time axis. The default is { plot: 'pointer', timeAxis: 'right-edge' }. A single string sets both, and applyOptions merges a partial object. A view limit such as fixRightEdge still wins over the anchor.

ts
// Keep the newest bar pinned for every zoom input.
chart.applyOptions({ zoomAnchor: 'newest' });
// Only change the time-axis drag.
chart.applyOptions({ zoomAnchor: { timeAxis: 'pointer' } });

chart.setBarSpacing(px, anchorX) zooms from code around any point, whatever this option says.

Keyboard ​

The chart is focusable. The keyboard.scope option sets when it takes keys: 'focus' (default) while it has focus, 'hover' also while the pointer is over it, and 'global' anywhere on the page. The drawing keys (Escape, Delete, the arrows while drawings are selected, copy, paste, undo and redo) always need the chart focused or hovered, so a 'global' chart does not take the page's own.

Keys are always left alone when they are typed into an input, a text area, a select or an editable element, or inside a dialog or a widget with an interactive role (a list box, tabs, a slider, a menu, a grid, a text box or a combo box), and while a modal dialog without the chart is open. So are keys the page already handled (preventDefault), browser shortcuts with Cmd, or with Ctrl on anything but the arrows, and a key a focused chart used, which 'hover' and 'global' charts do not use again.

Page-scroll keys (arrows, PageUp/PageDown, Home/End) are taken only when they move the view: at the oldest or newest edge they scroll the page as usual.

ts
chart.applyOptions({ keyboard: { scope: 'hover' } });

These are the default keys; actions and shortcuts rebinds them.

KeyEffect
← / →Pan
PageUp / PageDownMove one screen to older / newer bars
Home / EndJump to the oldest / newest bar
Ctrl + ← / →Move the crosshair one bar (it starts on the newest visible bar and scrolls into view)
+ / =Zoom in (around the right edge by default; see zoom anchor)
-Zoom out
Alt + RReset view
EscapeCancel drawing, exit drawing tool, deselect, or release a locked crosshair
Delete / BackspaceRemove the selected drawings
← / → / ↑ / ↓ with drawings selectedMove them one bar or one pixel (ten with Shift) instead of panning; see nudge
Ctrl/Cmd + C / VCopy the selected drawings / paste them
Ctrl/Cmd + ZUndo the last drawing or indicator change
Ctrl/Cmd + Shift + Z, Ctrl + YRedo

All keys need the keyboard feature. Beyond that:

KeysAlso need
← / →, PageUp / PageDown, Home / Endscroll
+ / =, -zoom
Shift + Endscroll and zoom
Ctrl + ← / →Nothing; the view follows the crosshair only when scroll is on
Drawing keysNothing

Any input stops running momentum, eased zoom and view animations.

To move the view from code, see View and price scale.

Actions and shortcuts ​

Every built-in command is an action on chart.actions, with an id, a label (from the localization messages, keys action…), an enabled state and, for toggles, a checked state. Run one by id, for example from your own toolbar or menu; run returns false when the action is disabled or changed nothing.

ts
chart.actions.run('zoom-in');
const undo = chart.actions.get('undo'); // { id, label, enabled, checked: null, keys: ['Mod+Z'], defaultKeys }
for (const action of chart.actions.list()) addMenuItem(action);
IdDefault keys
scroll-older, scroll-newerArrowLeft, ArrowRightPan
page-older, page-newerPageUp, PageDownOne screen
oldest-bar, newest-barHome, EndJump to an end
zoom-in, zoom-out+ and =, -Zoom
reset-viewShift+EndReset view
crosshair-prev, crosshair-nextCtrl+ArrowLeft, Ctrl+ArrowRightStep the crosshair
fit-contentFit all bars
auto-scaleToggle the main price scale's auto scale (checked)
remove-selectedDelete, BackspaceRemove the selected drawings or the selected indicator
delete-drawingsRemove the selected drawings
copy, cut, pasteMod+C, Mod+X, Mod+VCopy, cut and paste the selected drawings or the selected indicator
copy-drawings, cut-drawings, paste-drawingsCopy, cut and paste drawings
undo, redoMod+Z; Mod+Shift+Z, Ctrl+YThe chart's undo history (chart.history), drawings and indicators
lock-drawings, hide-drawingsToggle lock all and hide all (checked)
group-drawings, ungroup-drawingsMod+G, Mod+Shift+GGroup the selected drawings, dissolve their groups
draw-<tool>Pick a drawing tool, or go back to the cursor when it is active (checked while active); one per tool the chart has, built-in or registered, e.g. draw-rectangle

remove-selected, copy, cut, paste, undo and redo come with the drawings or indicators plugin (drawingTools(), drawings(), indicators() or builtins()), the drawing commands with a drawings plugin. Without them the chart has nothing to select, so it leaves those keys to the page.

The UI shell adds go-to-date (no default key), focus-next-region and focus-previous-region (F6, Shift+F6; see the key map), with a command palette, command-palette (Mod+K), indicator-picker (/, see Indicator picker) and, with a symbol search, symbol-search (/, plus Mod+K without the palette), which then keeps / and leaves indicator-picker without a key. Escape and Enter stay gesture keys: they finish or cancel what is in progress and cannot be rebound.

How the defaults are chosen: none uses Alt, because Alt (Option) with a letter types a character on Apple and several other layouts, and on Windows it reaches the browser menu. None is a letter or digit either, because a symbol can start with any of them: typing on a focused chart goes to the symbol search. That leaves the named keys (arrows, Page Up and Down, Home, End), +, - and =, alone or with Shift or Ctrl, and the view uses them: Shift+End resets the view, since End goes to the newest bar and Shift also brings back the default zoom and autoscale. Drawing tools and go-to-date have no default keys, since no such key is left that says what they do; bind the ones you want with chart.actions.setKeys('draw-trendline', ['F9']) (a function key keeps the letters for the search), the shortcuts dialog or the command palette.

The view keys act where keyboard.scope lets the chart take keys: while it has focus with the default 'focus', also while hovered with 'hover', and anywhere on the page with 'global', which is that scope's purpose. Actions marked local (the drawing keys, and go-to-date once bound) need the chart focused or hovered even then.

Writing shortcuts ​

A shortcut is modifiers and a key joined with +: Ctrl, Alt, Shift, Meta (Cmd on a Mac) or Mod (Ctrl or Cmd, whichever is pressed), then a letter, a digit, a character such as +, or a key name such as ArrowLeft, PageUp, Delete or F2. Case and order do not matter; normalizeShortcut('shift+ctrl+z') returns the canonical 'Ctrl+Shift+Z'.

Shortcuts match the same way on any keyboard layout. A letter or digit is taken from the character typed when it is a Latin letter or a digit, else from the physical key, so Mod+Z works on a Cyrillic layout, Alt+R where Alt types a symbol, and Ctrl+1 where the digit row types other characters. Modifiers must match exactly, except that Shift is ignored when no shortcut names it for that key (so + works whether or not your layout needs Shift for it); for letters, Shift+X and X are always different.

Rebinding ​

ts
chart.actions.setKeys('zoom-in', ['Shift+ArrowUp']); // replace the defaults
chart.actions.setKeys('reset-view', []); // turn its shortcut off
chart.actions.setKeys('zoom-in', null); // back to the defaults

localStorage.keymap = JSON.stringify(chart.actions.keymap); // only the changes
const errors = chart.actions.setKeymap(JSON.parse(localStorage.keymap)); // invalid entries are left out and returned

A keymap may name actions registered later, such as your own. To let users rebind shortcuts themselves, @chart-on/ui has a keyboard shortcuts dialog.

Your own actions ​

ts
const unregister = chart.actions.register({
  id: 'app:save-layout',
  label: 'Save layout',
  keys: ['Mod+S'],
  local: true, // only while this chart is hovered or focused, whatever `keyboard.scope` says
  enabled: () => hasChanges(),
  run: () => saveLayout(),
});

run returning false leaves the key to the page. Your shortcuts follow keyboard.scope and the keyboard feature like the built-in ones.

Conflicts ​

When several actions share a shortcut, the first enabled one that does something runs, with shortcuts set by setKeys ahead of defaults. conflicts() lists every shared shortcut, and actionsFor(keys) the actions on one, for example to warn before binding it:

ts
chart.actions.actionsFor('Mod+K'); // ['app:search']; Mod checks both Ctrl and Cmd
chart.actions.conflicts(); // [{ keys: 'Ctrl+K', ids: ['app:search', 'app:link'] }]
chart.actions.on('actions:change', refreshMenus); // actions or shortcuts changed

Typing a letter or digit on a focused chart opens the UI shell's symbol search.

Screen readers ​

The chart root is an application with the role description "chart", so screen readers announce it as a chart and pass the keys above through to it. Its accessible name is '<symbol name> chart' (or 'Chart'), and its description is a summary of what is in view, read when the chart gets focus:

ABC Corp, 1h. 50 bars from 2024-05-06 09:00 to 2024-05-08 10:00. Close 110.00 to 159.00, up 49.00 (44.55%). High 159.50, low 109.00.

chart.describe() returns the same text for your own UI. The summary follows panning, zooming and new data, rebuilt at most once per throttleMs (default 1000).

With crosshairReadout: true, a polite live region also reads the values under the crosshair as it moves (the time, then each series as in the legend), at the same rate.

ts
chart.applyOptions({ accessibility: { crosshairReadout: true, label: 'Price chart' } });
OptionDefaultEffect
summarytrueThe description above
crosshairReadoutfalseRead the crosshair values as they change
labelsymbol nameThe accessible name
throttleMs1000Shortest time between updates of the summary and the readout
announceBarsfalseAnnounce each new bar of the main series
announceIntervalMs5000Shortest time between two announcements

applyOptions({ accessibility }) merges into the current settings. The built-in themes meet WCAG AA contrast for text and the up and down colours (see theming).

Announcements ​

With announceBars: true, a second polite live region reads each new bar of the main series, with the close and change of the bar it completed:

New bar 2024-05-09 11:00. Previous close 159.00, +1.00 (+0.63%).

Updates of the forming bar, a new symbol or interval, and history pages are not announced. Call chart.announce(text) to read your own messages in the same region, for example when chart.annotations.alertsCrossed() reports a crossed alert. At most one announcement is read per announceIntervalMs: your messages go first, in order, and only the newest three wait their turn; bars that arrive faster (a fast replay) are skipped for the newest one.

ts
chart.applyOptions({ accessibility: { announceBars: true, announceIntervalMs: 10_000 } });
for (const { line, direction } of chart.annotations.alertsCrossed(prevClose, close)) chart.announce(`Alert at ${line.options.price} crossed ${direction}`);

Data table ​

chart.describeTable({ from?, to?, maxRows? }) returns the rows of getDataTable as a <table>: a caption with the name and time span, a header row, then one row per time with the time (in the chart timezone and formatTime) as its row header and every value in its series' own format. It defaults to the visible range; maxRows keeps the newest rows and the caption says how many were left out. The table is not attached, so place and style it yourself, and build a new one when the view changes.

ts
panel.replaceChildren(chart.describeTable({ maxRows: 200 }));

The UI shell has a ready-made panel for it that follows the view: see table view.

Cues beyond colour ​

Up and down need not rest on colour alone: the candle option hollowDown: true draws falling bodies as outlines (in candles, heikinAshi and volumeCandles), the hollow style does the same for rising bodies, and the legend option changeArrows: true puts ▲ or ▼ before each change value.