Primitives & plugins
Everything drawn on the chart that is not a series is a primitive: markers, event badges, price lines, profiles, and the whole trading layer. They all use one small interface, and so can you.
Markers & event badges
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 120, 3600);
const series = chart.addSeries('candlestick');
series.setData(bars);
const markers = series.createMarkers();
markers.setMarkers([
{ time: bars[30].time, position: 'belowBar', shape: 'arrowUp', size: 'medium', color: '#26a69a', text: 'BUY' },
{ time: bars[82].time, position: 'aboveBar', shape: 'arrowDown', size: 'medium', color: '#ef5350', text: 'SELL' },
]);
chart.timeScale.fitContent(bars.length);
return chart;Corporate-action badges (earnings / dividend / split) attach at the chart level:
chart.addEventMarkers().setEvents([{ time, type: 'earnings', label: 'E' }]);EventMarkers
EventMarkers renders circular badges near the bottom of the plot. Each element in the array
passed to setEvents has this shape:
interface ChartEvent {
time: number; // UTCSeconds bar time
type: 'earnings' | 'dividend' | 'split' | 'news' | string;
label: string; // text inside the badge (first two characters are shown)
color?: string; // overrides the built-in type color
id?: string; // returned as externalId by hitTest for click routing
}Built-in type colors: earnings -> #f0a020, dividend -> #26a69a, split -> #4f8cff,
news -> #9aa0b4. An unknown type string falls back to #9aa0b4. Providing color
overrides the default for that individual event.
When id is set on an event, hitTest returns that value as externalId, letting a click
listener identify the specific event without inspecting canvas coordinates:
const em = chart.addEventMarkers();
em.setEvents([
{ time: bars[20].time, type: 'earnings', label: 'E', id: 'q2-earnings' },
{ time: bars[50].time, type: 'dividend', label: 'D', id: 'div-jun' },
]);SeriesMarkers
series.createMarkers() is the standard entry point; it wires the internal series identifier
automatically. SeriesMarkers is also exported for direct import, which is useful for unit
testing primitives in isolation or for subclassing:
import { SeriesMarkers } from 'openalgo-charts';The key method on any SeriesMarkers instance is setMarkers, which replaces the full
marker list and sorts it by time internally:
// signature
setMarkers(markers: readonly SeriesMarker[]): voidCall it again with an updated array whenever signal state changes; it calls requestUpdate()
on the host automatically.
Price lines
chart.addPriceLine({ price: 101.5, color: '#3b82f6', lineWidth: 2, dashed: false, id: 'vwap' });PriceLine direct construction
chart.addPriceLine(opts) is a convenience wrapper. For live updates (such as a moving
stop-loss or a real-time VWAP level), construct PriceLine directly, keep a reference, and
mutate it without going through the chart API:
import { PriceLine } from 'openalgo-charts';
const sl = new PriceLine({
id: 'stop-loss', price: 19750, color: '#ef5350',
lineWidth: 1, dashed: true,
badge: 'SL', qty: 100, cursor: 'ns-resize',
});
chart.addPrimitive(sl);
// move the line and trigger a repaint
sl.setPrice(19600);
// update the info segment, e.g. live P&L
sl.setLeftLabel('-150');setPrice(price: number) moves the horizontal line and calls requestUpdate() on the host.
setLeftLabel(text: string) updates the info segment of the pill group and repaints.
setDragGhost(price | null) shows/clears a dimmed reference line while dragging.
See PriceLineOptions for the full option set including badge, qty,
extentFromRight, closeButton, and label.
Logo / brand watermark
LogoWatermark stamps a small image in a corner of the plot. It draws on the canvas (so it
is captured by takeScreenshot()), is source-agnostic (pass a src URL/data-URI or a
preloaded image), and an optional tint recolors the opaque pixels so a single-color logo
reads on any theme.
const chart = lib.createChart(el);
chart.addSeries('candlestick').setData(lib.generateBars(1700000000, 140, 3600));
// colored badge, bottom-right (the default corner)
chart.addPrimitive(new lib.LogoWatermark({
src: '/openalgo-charts/openalgo-logo.svg', height: 34, opacity: 0.9,
}));
// same logo tinted to a faint gray, top-left
chart.addPrimitive(new lib.LogoWatermark({
src: '/openalgo-charts/openalgo-logo.svg', position: 'top-left',
height: 26, opacity: 0.5, tint: '#8b91a7',
}));
chart.timeScale.fitContent(140);
return chart;| Option | Default | Description |
|---|---|---|
src / image | - | image URL / data URI, or a preloaded image / bitmap |
position | bottom-right | top-left / top-right / bottom-left / bottom-right / center |
height | 28 | logo height in px (width follows the source aspect) |
margin | 12 | gap from the plot edges in px |
opacity | 0.7 | 0..1 |
tint | - | recolor opaque pixels (e.g. a faint theme gray) |
zOrder | top | bottom / normal / top |
Swap or restyle it live with watermark.setOptions({ ... }).
watermarkRect placement helper
watermarkRect is the pure placement function that LogoWatermark uses internally to compute
the logo’s top-left pixel offset. Call it directly inside a custom primitive that needs
corner-aligned or centered layout at the same snap grid:
import { watermarkRect } from 'openalgo-charts';
// inside a primitive's draw():
const { x, y, w, h } = watermarkRect(
'bottom-left', 12, logoW, logoH, rc.plotWidth, rc.plotHeight,
);
ctx.drawImage(
img,
Math.round(x * rc.dpr), Math.round(y * rc.dpr),
Math.round(w * rc.dpr), Math.round(h * rc.dpr),
);Signature (all values in media/CSS pixels):
function watermarkRect(
position: WatermarkPosition,
margin: number,
w: number,
h: number,
plotW: number,
plotH: number,
): { x: number; y: number; w: number; h: number }The returned x/y are unscaled media pixels. Multiply by rc.dpr before drawing to a
device-pixel canvas.
A custom chart type
Register a renderer once and use it like any built-in type:
import { registerChartType } from 'openalgo-charts';
registerChartType('my-style', {
defaultStyle: { color: '#fff' },
isPriceSeries: true,
draw: (ctx, items, toY, barSpacing, dpr, style) => { /* paint to the canvas */ },
extents: (bar) => ({ min: bar.low, max: bar.high }),
});
chart.addSeries('my-style').setData(bars);A custom primitive
Implement IPrimitive and attach it - the same contract markers, profiles, and the trade
layer are built on:
import type { IPrimitive } from 'openalgo-charts';
const watermark: IPrimitive = {
zOrder: 'bottom',
draw(ctx, host) {
ctx.fillStyle = 'rgba(255,255,255,0.05)';
ctx.font = '700 48px system-ui';
ctx.fillText('OPENALGO', 24, host.plotHeight - 24);
},
// optional: hitTest(point), autoscaleInfo(), attached(host), detached()
};
chart.addPrimitive(watermark, 0);Primitives receive a host with pixel/price/time conversions and the plot rectangle, so they can draw anything and participate in hit-testing and autoscale.
bestHit helper
When a pane contains several independent primitives that each implement hitTest, the chart
collects all their results and calls bestHit to pick the winner. The rule is: smallest
distance wins; among equal distances the highest zOrder wins (top > normal > bottom).
Use it directly inside a composite primitive that delegates hit-testing to multiple sub-objects:
import { bestHit } from 'openalgo-charts';
import type { PrimitiveHit, PrimitiveRenderContext } from 'openalgo-charts';
const composite = {
zOrder: () => 'normal' as const,
draw(ctx: CanvasRenderingContext2D, rc: PrimitiveRenderContext) { /* ... */ },
hitTest(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null {
return bestHit([
lineA.hitTest(x, y, rc),
lineB.hitTest(x, y, rc),
markerLayer.hitTest(x, y, rc),
]);
},
};Signature:
function bestHit(hits: readonly (PrimitiveHit | null)[]): PrimitiveHit | nullnull entries in the input array are skipped. Returns null if every entry is null.
InvalidationLevel
InvalidationLevel is a const object (not a TypeScript enum) that describes how much of a
pane needs repainting. The chart uses it inside InvalidateMask to coalesce multiple
invalidation requests per animation frame into the minimum necessary work:
const InvalidationLevel = {
None: 0, // nothing to do
Cursor: 1, // repaint only the overlay canvas (crosshair, hover, dragging primitives)
Light: 2, // repaint the base canvas at the current scales
Full: 3, // recompute scales and ticks, then repaint everything
} as const;host.requestUpdate() takes no arguments. Calling it signals the chart to schedule the next
animation frame; the chart decides the repaint scope internally via its InvalidateMask.
InvalidationLevel values are exported for advanced use cases such as constructing a custom
InvalidateMask, reading PaneInvalidation.level to inspect what a prior frame coalesced, or
passing constants to internal helpers that accept a level directly.
A single named import gives you both the value object and the companion type:
import { InvalidationLevel } from 'openalgo-charts';
import type { InvalidationLevel as Level } from 'openalgo-charts';
// compare numeric levels returned by PaneInvalidation.level
if (paneInval.level >= InvalidationLevel.Full) {
console.log('full repaint scheduled');
}
// type annotation for helpers that receive a level
function logLevel(level: Level): void {
const names: Record<Level, string> = { 0: 'None', 1: 'Cursor', 2: 'Light', 3: 'Full' };
console.log(names[level]);
}