Events
OpenAlgo Charts gives you one small, predictable event surface. A single
chart.on(name, cb) bus covers interaction, viewport, data, and lifecycle
events, and it returns an unsubscribe function so cleanup is one call. The
trading layer routes its trading:* events through the same bus, so you never
juggle two APIs.
Subscribing
// Subscribe. The return value unsubscribes.
const off = chart.on('crosshair:move', (e) => {
// handle event
});
// Later: stop listening.
off();
// Or drop one listener / every listener for a name.
chart.off('crosshair:move', handler);
chart.off('crosshair:move');
// Fire once, then auto-unsubscribe.
chart.once('ready', () => console.log('chart is live'));Every on(...) returns its own unsubscribe. Keep the returned function and call
it on teardown (component unmount, route change) so listeners never leak.
What the engine emits
| Event | Payload | Fires when |
|---|---|---|
ready | {} | Once, on the microtask after createChart returns. |
crosshair:move | { time, index, price, bar, point } | The pointer moves over the plot (all-null when it leaves). |
click | { id } | A hit-testable primitive (marker, event, price line) is clicked. id is its externalId. |
hover | { id } | The pointer enters (id = externalId) or leaves (id = null) a hit-testable primitive. |
pan | { from, to, logicalFrom, logicalTo } | The user pans (drag, two-finger, kinetic glide). |
zoom | { from, to, logicalFrom, logicalTo } | The user zooms (wheel or pinch). |
resize | { width, height } | The container size changes (CSS px). |
lazy-load | { from, to, direction } | The viewport nears the oldest bar and history paging should run. |
trading:* | see Trading events | The user interacts with a position, order, or bracket. |
from / to on the viewport and data events are UTC seconds (or null
when the edge falls outside loaded data). logicalFrom / logicalTo are the raw
logical bar indices, handy when you drive your own paging math.
crosshair:move and pan / zoom fire at pointer/gesture rate. Do only light
work in the handler (update a legend, a readout). Defer heavy work to
requestAnimationFrame or a debounce.
Live demo
Move the crosshair, then drag and wheel-zoom. The readout is driven entirely by
chart.on('crosshair:move') and chart.on('zoom' | 'pan').
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 200, 3600);
chart.addSeries('candlestick').setData(bars);
chart.timeScale.fitContent(bars.length);
el.style.position = 'relative';
const out = document.createElement('div');
out.style.cssText = 'position:absolute;left:8px;top:8px;font:12px ui-monospace,monospace;background:rgba(0,0,0,.55);color:#e6edf3;padding:6px 9px;border-radius:6px;pointer-events:none;line-height:1.5';
el.appendChild(out);
let hover = 'move the crosshair';
let view = '';
const render = () => { out.innerHTML = hover + (view ? '<br>' + view : ''); };
chart.on('crosshair:move', (e) => {
hover = e.bar
? 'O ' + e.bar.open.toFixed(2) + ' H ' + e.bar.high.toFixed(2) + ' L ' + e.bar.low.toFixed(2) + ' C ' + e.bar.close.toFixed(2)
: 'off chart';
render();
});
const fmt = (t) => t == null ? '-' : new Date(t * 1000).toISOString().slice(0, 16).replace('T', ' ');
chart.on('zoom', (r) => { view = 'zoom ' + fmt(r.from) + ' -> ' + fmt(r.to); render(); });
chart.on('pan', (r) => { view = 'pan ' + fmt(r.from) + ' -> ' + fmt(r.to); render(); });
render();
return chart;Interaction events
crosshair:move
Fires on every pointer move across the plot, and once with all-null fields when
the pointer leaves. bar is the hovered bar of the primary price series (or
null off the data); point is the crosshair position in container CSS px.
chart.on('crosshair:move', ({ time, price, bar, point }) => {
if (!bar) { legend.hidden = true; return; }
legend.hidden = false;
legend.textContent = `O ${bar.open} H ${bar.high} L ${bar.low} C ${bar.close}`;
});The typed helper chart.subscribeCrosshairMove(cb) delivers the same
CrosshairMoveEvent if you prefer a single dedicated callback.
click
Fires when a hit-testable primitive is clicked. The payload id is the
externalId you set on the marker, event marker, or price line, so you can route
the click without a lookup.
chart.on('click', ({ id }) => {
if (id === 'entry-line') openOrderPanel();
});chart.subscribeClick(cb) is the typed equivalent (cb(externalId)).
hover
Fires when the pointer enters or leaves a hit-testable primitive (id is the
primitive’s externalId, or null on leave) — at state-change rate, not
pointer rate. The chart already renders hover states (thicker line, brighter
pill, cursor hint) by itself; use this event for app-side affordances such as
an order tooltip or a details panel.
chart.on('hover', ({ id }) => {
tooltip.hidden = id === null || !id.startsWith('ord:');
});Viewport events
pan and zoom report the new visible window after the gesture. resize fires
whenever the container changes size (the built-in ResizeObserver drives it, and
an explicit chart.applySize(w, h) also emits it).
chart.on('pan', ({ from, to }) => syncOtherChart(from, to));
chart.on('zoom', ({ from, to }) => syncOtherChart(from, to));
chart.on('resize', ({ width, height }) => layoutOverlays(width, height));Use pan / zoom to keep a second chart’s time window in lockstep, or to save
the user’s camera. The window is not persisted for you by design.
Data events
lazy-load
Fires when the viewport nears the oldest loaded bar. Pair it with a history
loader and series.prependData(...), then call chart.historyLoadComplete() to
re-arm the trigger. See Data Loading for the full pattern.
chart.setHistoryLoader(() => {
chart.on('lazy-load', async ({ to }) => {
const older = await fetchOlderBars(before(to));
series.prependData(older);
chart.historyLoadComplete();
});
});setHistoryLoader is the direct hook — it fires the loader with no payload.
lazy-load is the observable form of the same trigger, carrying the time window
so a single handler can serve multiple series.
Lifecycle events
ready
Fires once, on the microtask after createChart returns. Because it is deferred,
a subscription registered on the very next line still receives it.
const chart = createChart(el);
chart.on('ready', () => console.log('ready')); // still firesTrading events
Every trading event is mirrored onto the main bus, so
chart.on('trading:order_modify', cb) and
chart.trading.on('trading:order_modify', cb) are equivalent (both use the full
trading: prefixed name). Use whichever reads better in your code.
| Event | Payload |
|---|---|
trading:order_modify | { orderId, newPrice, previousPrice } |
trading:order_cancel | { orderId } |
trading:order_click | { order } |
trading:position_close | { positionId } |
trading:position_click | { position } |
trading:bracket_modify | { parentId, bracketRole, newPrice } |
chart.on('trading:order_modify', async ({ orderId, newPrice }) => {
await broker.modifyOrder(orderId, { price: newPrice });
});
chart.on('trading:position_close', async ({ positionId }) => {
await broker.closePosition(positionId);
});See the Trading API page for the full data model.
Keyboard shortcut events
Shortcut triggers come off their own manager rather than the value bus, because
the payload is a rich ShortcutTriggerEvent:
chart.shortcuts?.on(({ command, combo }) => {
analytics.track('shortcut', { command, combo });
});See Keyboard Shortcuts.
Design notes and scope
OpenAlgo Charts is a lean rendering engine, not a full white-label shell, so the event surface stays deliberately small and stable. Concerns that belong to your application layer are composed from the primitives above rather than shipped as built-in events:
- Drawing tools, annotations, context menus. Build them as
primitives / plugins and hit-test with
click. - Indicator lifecycle. Indicators here are plain series and transforms you add and remove yourself, so there is no separate add/remove/settings event stream to subscribe to.
- Data-feed reconnection, gap-fill, timezone, theme families, undo/redo,
state persistence. These live in your app or your feed. The engine exposes
the hooks (
setData,update,prependData,setHistoryLoader,setTheme-style options) so you own the policy. See Data Loading and Live Data.
This keeps the base bundle small and the API easy to reason about: a handful of events you will actually use, each with a plain payload and a one-call unsubscribe.