DocumentationPrimitives & Plugins

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;
live
Rendering live 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[]): void

Call 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;
live
Rendering live chart…
LogoWatermark: a colored badge and a tinted monochrome mark.
OptionDefaultDescription
src / image-image URL / data URI, or a preloaded image / bitmap
positionbottom-righttop-left / top-right / bottom-left / bottom-right / center
height28logo height in px (width follows the source aspect)
margin12gap from the plot edges in px
opacity0.70..1
tint-recolor opaque pixels (e.g. a faint theme gray)
zOrdertopbottom / 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 | null

null 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]);
}