Live examples

Every chart on this page is the real library running in your browser - the code shown is the code that ran. Try it, copy it, ship it.

Jump to depth of market, drawing tools, market profiles, chart controls, or trading.

Branding and optional watermarks

The corner logo stays visible by default. The background symbol or custom text watermark is opt-in. Try the controls, both themes and phone width, then open Appearance in Chart settings. See Branding and Watermarks.

live
Rendering live chart…
Simulated NIFTY bars. The corner logo starts on and the background watermark starts off. Toggle watermark to see the symbol and interval, then try custom text, phone width and both themes. Chart settings offers the same watermark controls under Appearance.
View example code
el.style.display = 'flex';
el.style.flexDirection = 'column';
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;padding:8px;flex-shrink:0';
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;justify-content:center;flex:1;min-height:0;overflow:hidden';
const stage = document.createElement('div');
stage.style.cssText = 'width:100%;max-width:100%;height:100%;min-height:0';
wrap.appendChild(stage);
el.append(controls, wrap);

const bars = Array.from({ length: 150 }, (_, index) => {
  const close = 23800 + Math.sin(index / 9) * 28;
  return { time: 1700000000 + index * 300, open: close - 3,
    high: close + 7, low: close - 6, close, volume: 650 + index * 65 };
});
const widget = lib.createWidget(stage, {
  symbol: 'NIFTY SIM', exchange: 'NSE', interval: '5m',
  intervals: ['5m', '15m', '1h'], persist: false,
  feed: { getBars: async () => bars, subscribeBars: () => () => {} },
  navigation: { defaultVisibleBars: 80 },
  rail: { tools: ['trend-line', 'horizontal-line', 'rectangle'] },
});
const listeners = new AbortController();
function button(label, action) {
  const node = document.createElement('button');
  node.type = 'button';
  node.textContent = label;
  node.style.cssText = 'min-height:44px;padding:5px 9px;border:1px solid var(--oac-card-border);border-radius:5px;background:var(--oac-card);color:inherit;font:12px system-ui;cursor:pointer';
  node.addEventListener('click', action, { signal: listeners.signal });
  controls.appendChild(node);
  return node;
}
button('Toggle watermark', () => {
  widget.chart.setWatermarkOptions({ visible: !widget.chart.watermarkOptions().visible });
});
button('Automatic or custom text', () => {
  widget.chart.setWatermarkOptions({ text: widget.chart.watermarkOptions().text ? '' : 'Research' });
});
let brand = true;
const logo = button('Logo: on', () => {
  brand = !brand;
  widget.chart.setBranding(brand);
  logo.textContent = 'Logo: ' + (brand ? 'on' : 'off');
  logo.setAttribute('aria-pressed', String(brand));
});
logo.setAttribute('aria-pressed', 'true');
let narrow = false;
const width = button('Width: desktop', () => {
  narrow = !narrow;
  stage.style.width = narrow ? '360px' : '100%';
  width.textContent = narrow ? 'Width: phone' : 'Width: desktop';
  width.setAttribute('aria-pressed', String(narrow));
});
width.setAttribute('aria-pressed', 'false');
button('Switch theme', () => widget.setTheme(widget.theme() === 'dark' ? 'light' : 'dark'));
button('Chart settings', () => widget.openSettings());
return { destroy() { listeners.abort(); widget.destroy(); } };

Objects and compact dialogs

Find drawings, indicators and registered profiles in one panel. Hide, lock, edit or remove supported objects, or focus a drawing beyond the latest candle. Change the host width to try compact dialogs. See the Objects guide.

live
Rendering live chart…
Simulated stock candles with irregular moves, pullbacks and changing volume. Open Objects to manage drawings and indicators, or focus the rectangle beyond the newest bar. Try the 350 px width, or hide RSI, save the layout, show it, and restore. The profile offers only show/hide and removal; its host-owned state is separate from the saved chart layout.
View example code
el.style.display = 'flex';
el.style.flexDirection = 'column';
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;padding:8px;flex-shrink:0';
const stage = document.createElement('div');
stage.style.cssText = 'flex:1;min-height:0;max-width:100%;width:100%';
el.append(controls, stage);
const bars = lib.generateBars(1700000000, 160, 60);
const widget = lib.createWidget(stage, {
  symbol: 'OBJECTS SIM', interval: '1m', intervals: ['1m'],
  rail: false, statusline: false, topbar: false,
  navigation: { defaultVisibleBars: 100 },
});
widget.series.setData(bars);
function addDrawing() {
  widget.draw.add({ tool: 'trend-line', paneIndex: 0,
    points: [{ time: bars[100].time, price: bars[100].low },
      { time: bars[140].time, price: bars[140].low }],
    style: { color: '#f0a020', lineWidth: 3 },
  });
}
addDrawing();
widget.draw.add({ tool: 'rectangle', paneIndex: 0,
  points: [{ time: bars[159].time + 20 * 60, price: bars[159].high + 2 },
    { time: bars[159].time + 35 * 60, price: bars[159].high + 5 }],
  style: { color: '#4da3ff', lineWidth: 2 },
});
widget.chart.addIndicator('rsi');
const profile = new lib.VolumeProfile(
  lib.computeVolumeProfileSessions(bars, { tickSize: 0.5, session: 'composite' }),
  { width: 60, showValueArea: false, showPocLabel: false },
);
widget.chart.addPrimitive(profile);
let exists = true;
let visible = true;
const unregister = widget.objects.register({
  id: 'session-profile',
  get: () => exists ? { kind: 'profile', name: 'Session profile', paneIndex: 0, visible } : null,
  setVisible(on) {
    if (visible === on) return;
    visible = on;
    if (on) widget.chart.addPrimitive(profile); else widget.chart.removePrimitive(profile);
  },
  remove() { widget.chart.removePrimitive(profile); exists = false; },
});
function button(label, action) {
  const node = document.createElement('button');
  node.textContent = label;
  node.type = 'button';
  node.style.cssText = 'padding:5px 9px;border:1px solid var(--oac-card-border);border-radius:4px;background:var(--oac-card);color:inherit;font:12px system-ui';
  node.addEventListener('click', action);
  controls.appendChild(node);
  return node;
}
button('Open Objects', () => widget.openObjects());
button('Add drawing', addDrawing);
button('Add RSI', () => widget.chart.addIndicator('rsi'));
button('Undo drawing action', () => widget.draw.undo());
let compact = false;
const widthButton = button('Width: fit', () => {
  compact = !compact;
  stage.style.width = compact ? '350px' : '100%';
  widthButton.textContent = compact ? 'Width: 350 px' : 'Width: fit';
  widthButton.setAttribute('aria-pressed', String(compact));
});
widthButton.setAttribute('aria-pressed', 'false');
let saved;
const restoreButton = button('Restore layout', () => {
  if (saved) widget.restoreState(saved);
});
restoreButton.disabled = true;
button('Save layout', () => {
  saved = JSON.parse(JSON.stringify(widget.getState()));
  restoreButton.disabled = false;
});
return { destroy() { unregister(); widget.destroy(); } };

Managed data loading

Load older history, simulate a failed refresh and retry, or hold the display while live updates continue. The data-loading guide explains the shared controller for widgets and custom terminals.

live
Rendering live chart…
Simulated NIFTY around 23,800 with irregular candles, changing volume and live price ticks. Fail a refresh, then use Retry on the chart. Pause holds the display while live data continues; it does not start historical replay.
View example code
el.style.display = 'flex';
el.style.flexDirection = 'column';
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;padding:8px;min-height:48px;flex-shrink:0';
const stage = document.createElement('div');
stage.style.cssText = 'flex:1;min-height:0';
el.append(controls, stage);
const now = 1789093800;
const price = value => Math.round((23800 + (value - 100) * 4) * 20) / 20;
const bars = lib.generateBars(now - 359 * 60, 360, 60).map(bar => ({
  ...bar, open: price(bar.open), high: price(bar.high),
  low: price(bar.low), close: price(bar.close),
  volume: Math.round(bar.volume / 65) * 65,
}));
let failNext = false;
let empty = false;
let requests = 0;
let reconnect;
const counter = document.createElement('span');
counter.style.cssText = 'font:12px system-ui;padding:7px';
const source = {
  async getBars(request) {
    counter.textContent = 'History requests: ' + ++requests;
    await new Promise((resolve, reject) => {
      const stop = () => { clearTimeout(timer); reject(new Error('Cancelled')); };
      const timer = setTimeout(() => {
        request.signal?.removeEventListener('abort', stop);
        resolve();
      }, 700);
      request.signal?.addEventListener('abort', stop, { once: true });
      if (request.signal?.aborted) stop();
    });
    if (failNext) { failNext = false; throw new Error('Simulated connection failure'); }
    if (empty) return [];
    return bars.filter(bar => bar.time >= request.from && bar.time <= request.to);
  },
  async getBarsPage(request) {
    const older = await this.getBars({ ...request, from: bars[0].time });
    return { bars: older.slice(-60), hasMore: older.length > 60 };
  },
  subscribeBars(request, onBar, options) {
    reconnect = options.onResync;
    let seed = 0x31f2c7;
    const random = () => {
      seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
      return seed / 4294967296;
    };
    const timer = setInterval(() => {
      if (empty) return;
      const last = bars[bars.length - 1];
      const close = Math.round((last.close + (random() - 0.5) * 2) * 20) / 20;
      const live = { ...last, close, high: Math.max(last.high, close),
        low: Math.min(last.low, close), volume: last.volume + (1 + Math.floor(random() * 6)) * 65 };
      bars[bars.length - 1] = live;
      onBar({ ...live });
    }, 800);
    return () => clearInterval(timer);
  },
};
const feed = lib.withBarCache(source, { now: () => now * 1000 });
const widget = lib.createWidget(stage, {
  feed, symbol: 'NIFTY SIM', exchange: 'NFO', interval: '1m', intervals: ['1m'],
  rail: false, indicators: false, lookbackBars: 120, loading: { now: () => now, pageSize: 60 },
  navigation: { defaultVisibleBars: 100 },
});
function button(label, action) {
  const button = document.createElement('button');
  button.textContent = label;
  button.type = 'button';
  button.style.cssText = 'padding:5px 9px;border:1px solid #64748b;border-radius:4px;font:12px system-ui';
  button.addEventListener('click', action);
  controls.appendChild(button);
}
button('Load older', () => void widget.dataController.loadMore());
button('Fail refresh', () => { failNext = true; void widget.reload(); });
button('Reconnect', () => reconnect?.());
button('Pause / resume display', () => {
  widget.dataController.setPaused(!widget.dataController.getState().paused);
});
button('Empty / restore', () => {
  empty = !empty;
  widget.setSymbol(empty ? 'EMPTY SIM' : 'NIFTY SIM', 'NFO');
});
controls.appendChild(counter);
return widget;

Depth of market

Watch a simulated live order book, pause updates, and group multiple ticks into one price row. The depth-of-market guide explains feed integration and price aggregation. All data and interactions here are simulated.

Depth of marketSimulated · updating
Ladder NIFTY ATM CEBest bid 99.95Best ask 100.05Source tick 0.05Snapshot 0
NIFTY ATM CE · candlesticks

Loading depth chart…

ATM call ladder · 0 rows · independent scale
Bid qtyPriceAsk qty

Drawing tools

Choose a tool, draw on the chart, then select it to move or delete it. Try undo and redo with the visible controls. See the drawing tools guide for the full tool catalogue and integration API. Open the full-size drawing gallery to inspect editable samples of all 85 tools, including pitchforks, harmonic patterns and advanced geometry.

live
Rendering live chart…
Synthetic hourly bars. Pick a tool, then click to place anchors. Cursor selects and moves drawings; Delete removes a selection, Esc cancels placement, and Ctrl/Cmd+Z undoes. Reset restores the three examples.
View the running example source
// In your app: import { createChart, generateBars } from 'openalgo-charts';
// import { DrawingController, getDrawingTool } from 'openalgo-charts/draw';
// The website supplies these exports as lib and a sized container as el.
const root = document.createElement('div');
root.className = 'oac-draw-playground';
root.innerHTML = `
  <div class="oac-draw-playground__rail" role="group" aria-label="Drawing tools"></div>
  <div class="oac-draw-playground__main">
    <div class="oac-draw-playground__actions" role="group" aria-label="Drawing actions">
      <select aria-label="Select a drawing"></select>
    </div>
    <div class="oac-draw-playground__plot" tabindex="0" aria-label="Interactive drawing chart"></div>
    <div class="oac-draw-playground__status" role="status" aria-live="polite"></div>
  </div>`;
el.appendChild(root);
const rail = root.querySelector('.oac-draw-playground__rail');
const actions = root.querySelector('.oac-draw-playground__actions');
const plot = root.querySelector('.oac-draw-playground__plot');
const status = root.querySelector('[role="status"]');
const selection = root.querySelector('select');
const listeners = new AbortController();
const listen = (node, event, handler) => node.addEventListener(event, handler, { signal: listeners.signal });

const chart = lib.createChart(plot);
const bars = lib.generateBars(1700000000, 160, 3600);
chart.addSeries('candlestick').setData(bars);
chart.timeScale.fitContent(bars.length);
const draw = new lib.DrawingController(chart, { magnet: 'weak', stayInDrawingMode: false });
const point = (index, price) => ({ time: bars[index].time, price });

// Seed three editable drawings, then start with an empty undo history.
draw.add({ id: 'demo-trend', tool: 'trend-line', paneIndex: 0,
  points: [point(15, bars[15].low), point(75, bars[75].low)],
  style: { color: '#4f8cff', lineWidth: 2 } });
draw.add({ id: 'demo-zone', tool: 'rectangle', paneIndex: 0,
  points: [point(93, bars[93].high), point(137, bars[137].low)],
  style: { color: '#14b8a6', fill: true, fillOpacity: 0.14, lineWidth: 2 },
  text: { value: 'Supply zone', color: '#14b8a6', position: 'inside', valign: 'top' } });
draw.add({ id: 'demo-level', tool: 'horizontal-line', paneIndex: 0,
  points: [point(0, bars[45].low)],
  style: { color: '#f5a623', lineStyle: 'dashed', lineWidth: 2 } });
const initial = draw.toJSON();
draw.fromJSON(initial);

const quickTools = [
  [null, 'Cursor', 'Click a drawing to select it; drag its body or handles.'],
  ['trend-line', 'Trend line', 'Click a start and end point, or drag across the chart.'],
  ['horizontal-line', 'Horizontal line', 'Click once to mark a price level.'],
  ['ray', 'Ray', 'Click two points to project a line to the right.'],
  ['rectangle', 'Rectangle', 'Click two opposite corners, or drag to mark a zone.'],
  ['fib-retracement', 'Fibonacci', 'Click the swing low and high to place a retracement.'],
  ['long-position', 'Long position', 'Click entry, then target; drag the stop or target to adjust.'],
  ['measure', 'Measure', 'Click two points to measure price, time and volume.'],
  ['brush', 'Brush', 'Press, draw a stroke, and release.'],
];
const tools = [quickTools[0], ...lib.registeredDrawingTools().map(tool => [
  tool.id, tool.name, tool.freehand ? 'Press, draw, and release.' :
    tool.points === 0 ? 'Click each point, then double-click to finish.' :
    'Click ' + tool.points + ' point(s) to place the drawing.',
])];
const catalog = document.createElement('select');
catalog.setAttribute('aria-label', 'All drawing tools');
catalog.add(new Option('All drawing tools...', ''));
for (const [id, name] of tools.slice(1)) catalog.add(new Option(name, id));
actions.prepend(catalog);
listen(catalog, 'change', () => {
  draw.setTool(catalog.value || null);
  plot.focus({ preventScroll: true });
  refresh();
});
const toolButtons = new Map();
function button(parent, label, action) {
  const node = document.createElement('button');
  node.type = 'button';
  node.textContent = label;
  listen(node, 'click', () => { action(); refresh(); });
  parent.appendChild(node);
  return node;
}
for (const [id, label, instruction] of quickTools) {
  const node = button(rail, label, () => { draw.setTool(id); plot.focus({ preventScroll: true }); });
  node.title = instruction;
  toolButtons.set(id, node);
}
const undo = button(actions, 'Undo', () => draw.undo());
const redo = button(actions, 'Redo', () => draw.redo());
const remove = button(actions, 'Delete', () => draw.removeMany(draw.selection()));
button(actions, 'Reset demo', () => {
  draw.setTool(null);
  draw.fromJSON(initial);
  chart.timeScale.fitContent(bars.length);
});

function refresh() {
  const active = draw.activeTool();
  catalog.value = active || '';
  const picked = draw.get(draw.selected());
  const label = drawing => drawing.text?.value || lib.getDrawingTool(drawing.tool).name;
  const rows = draw.drawings();
  selection.replaceChildren(new Option('Select a drawing...', ''));
  rows.forEach(drawing => selection.add(new Option(label(drawing), drawing.id)));
  selection.value = picked?.id || '';
  toolButtons.forEach((node, id) => node.setAttribute('aria-pressed', String(id === active)));
  undo.disabled = !draw.canUndo();
  redo.disabled = !draw.canRedo();
  remove.disabled = draw.selection().length === 0;
  const tool = tools.find(([id]) => id === active);
  status.textContent = rows.length + ' drawings · ' +
    (active ? tool[1] + ': ' + tool[2] : picked ? 'Selected: ' + label(picked) + '. Drag its body or handles.' : tool[2]);
}
listen(selection, 'change', () => {
  const id = selection.value || null;
  draw.setTool(null);
  draw.select(id);
  refresh();
});

// Shortcuts belong to this playground, so another chart or a text field keeps its keys.
listen(root, 'keydown', event => {
  if (event.target.closest('input, textarea, select, [contenteditable="true"]')) return;
  if (event.key === 'Escape') draw.setTool(null);
  else if (event.key === 'Delete' || event.key === 'Backspace') draw.removeMany(draw.selection());
  else if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
    event.shiftKey ? draw.redo() : draw.undo();
  } else return;
  event.preventDefault();
  event.stopPropagation();
  refresh();
});
const off = ['draw:tool', 'drawing:select', 'drawing:change'].map(event => chart.on(event, refresh));
refresh();

// Unmounts and site-theme changes release the controller and every host listener.
const destroy = chart.destroy.bind(chart);
chart.destroy = () => {
  listeners.abort();
  off.forEach(unsubscribe => unsubscribe());
  draw.destroy();
  destroy();
  root.remove();
};
return chart;

Compact market profiles

2.1.0: inspect small TPO letters and volume values, right-click to split or unsplit one day, and compare Dark, Blue, Graphite, Emerald and Ivory themes. Lowercase o marks each day’s open; # appears only on the newest session. The six sessions are synthetic data. The full guide is Profile Demo & Themes. Use the full-size profiles selector to switch between Volume Profile, TPO letters and the current order-flow demo with its themes, text colors, lot display and optional statistics table.

Six synthetic sessions · 2-point rowsOpen full-size demo ↗

Interactive

Switch tabs to reconfigure each chart live.

Chart type

Custom theme

Data tooltip

Event markers

Range switcher

Legend

Series compare

Indicators & markers

Price scale

Custom chart types

Scales formatting

Scales config

Data

Custom plugins

Trading

Data-driven positions, orders, TP/SL brackets, and fill markers via chart.trading. Drag the order lines; the pill ✕ fires cancel/close.

Positions & orders

Trade markers

The terminal in one call

createWidget from openalgo-charts/widget builds the chart with its chrome: a top bar (symbol, interval, chart type, indicators, capture, settings, theme), the drawing rail, a status line, dialogs generated from the settings schemas the engine already ships, a right-click menu and a keymap with a ? panel. The engine underneath still ships no DOM; the widget is a packaged host driving the same public API your own would. Pick a tool from the rail, open Indicators, change the chart type, press ?.

live
Rendering live chart…
No feed is wired here, so the bars go straight on widget.series; with a feed the interval pills and the symbol box reload through it. Persistence is off unless asked for.
View example code
const widget = lib.createWidget(el, {
symbol: 'DEMO',
exchange: 'NSE',
interval: '1h',
intervals: ['5m', '15m', '1h', '1d'],
});

// Without a feed the host puts bars on the primary series itself. A feed
// would make setSymbol, setInterval and reload() fetch through it.
const bars = lib.generateBars(1700000000, 240, 3600);
widget.series.setData(bars);
widget.chart.timeScale.fitContent(160);

// The chrome's own facts arrive as events; the chart's bus is still there on widget.chart.
widget.on('interval', function (e) {
widget.context.status('Interval ' + e.interval + ' (no feed wired, so the bars stay)');
});
return widget;

Every option, event and theming token is on The Widget Tier.

Custom indicators

The 102 built-ins are not special-cased. Each is a descriptor - inputs, plots and a calc - and anything you register with registerIndicator uses the same contract, so it gets generated settings, a legend entry and state persistence for free.

This one computes a trend regime, then uses four descriptor hooks on the result: colorBy for the line, barColors to repaint the candles, background to shade the pane, and markers to flag each flip. No fork, no build step.

live
Rendering live chart…
View example code
lib.registerIndicator({
id: 'demo-regime',
name: 'Trend Regime',
category: 'Custom',
placement: 'onchart',
inputs: [
  { key: 'length', type: 'number', label: 'Length', default: 20, min: 2, max: 200, step: 1 },
  { key: 'up', type: 'color', label: 'Bull', default: '#26a69a' },
  { key: 'down', type: 'color', label: 'Bear', default: '#ef5350' },
],
plots: [{
  key: 'basis', type: 'line', title: 'Basis', style: { lineWidth: 2 },
  // colorBy is a function, not the name of a column. It runs per bar and
  // returns a colour, or undefined to fall back to the plot's own.
  colorBy: ({ index, values, settings }) => {
    const t = values.trend[index];
    return t == null ? undefined : t > 0 ? settings.up : settings.down;
  },
}],
calc(bars, s) {
  const close = lib.sourceValues(bars, 'close');
  const basis = lib.nulls(lib.sma(close, Math.max(2, s.length | 0)));
  const trend = basis.map((b, i) => (b == null ? null : close[i] >= b ? 1 : -1));
  return { basis, trend };
},
// Repaint the price candles by regime.
barColors({ values, settings }) {
  return values.trend.map((t) => (t == null ? null : t > 0 ? settings.up : settings.down));
},
// Shade the whole pane behind them.
background({ values, settings }) {
  return values.trend.map((t) =>
    t == null ? null : lib.withAlpha(t > 0 ? settings.up : settings.down, 0.07));
},
// Flag every flip, anchored to the candle rather than to the indicator line.
markers({ bars, values, settings }) {
  const out = [];
  for (let i = 1; i < bars.length; i++) {
    const a = values.trend[i - 1], b = values.trend[i];
    if (a == null || b == null || a === b) continue;
    out.push({
      time: bars[i].time,
      position: 'atPrice',
      price: b > 0 ? bars[i].low : bars[i].high,
      shape: b > 0 ? 'arrowUp' : 'arrowDown',
      color: b > 0 ? settings.up : settings.down,
      text: b > 0 ? 'BULL' : 'BEAR',
    });
  }
  return out;
},
});

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 200, 3600);
chart.addSeries('candlestick').setData(bars);
chart.addIndicator('demo-regime');
chart.timeScale.fitContent(bars.length);
return chart;

A descriptor can also supply draws for free-standing lines, boxes and labels, levels for data-derived horizontals, fills between plots, table for an on-chart grid, and alerts the chart evaluates for you. See Indicators for the full contract.

Render backends and vector export

The series pass goes through a render backend port. renderer: 'auto' takes the WebGL2 backend from openalgo-charts/webgl where the device has WebGL2 and the 2D path otherwise, decided once at construction; chart.rendererKind reports which one is painting. Pan and zoom: the candles are batched into one GPU draw call per frame and composited into the pane’s own canvas, so screenshots, drawings, axes and text are unchanged.

live
Rendering live chart…
View example code
const chart = lib.createChart(el, { renderer: 'auto' });
const bars = lib.generateBars(1700000000, 600, 3600);
chart.addSeries('candlestick').setData(bars);
chart.timeScale.fitContent(240);

// Decided once, at construction: 'webgl2' where WebGL2 works, else 'canvas2d'.
el.style.position = 'relative';
const tag = document.createElement('div');
tag.style.cssText = 'position:absolute;right:64px;bottom:30px;z-index:5;font:12px ui-monospace,monospace;background:rgba(0,0,0,.55);color:#e6edf3;padding:4px 8px;border-radius:6px;pointer-events:none';
function show(note) { tag.textContent = 'rendererKind: ' + chart.rendererKind + (note ? '  ' + note : ''); }
show(lib.isWebGL2Supported() ? '' : '(no WebGL2 on this device)');
// A lost context moves the chart to the 2D path for the session and says so once.
chart.on('renderer:fallback', function (e) { show('(fell back: ' + e.reason + ')'); });
el.appendChild(tag);
return chart;

chart.exportSVG() writes the same frame as a standalone vector document: axis labels and tags stay text, lines stay lines, and nothing transient (crosshair, hover, drag) is in it. The right half below is the string the left half returned, inlined as markup and re-exported as you pan or zoom the live chart.

live
Rendering live chart…
View example code
el.style.display = 'flex';
el.style.gap = '10px';
const live = document.createElement('div');
live.style.cssText = 'flex:1 1 0;min-width:0;height:100%;position:relative';
const out = document.createElement('div');
out.style.cssText = 'flex:1 1 0;min-width:0;height:100%;overflow:hidden;border:1px dashed #5b6478;border-radius:6px;line-height:0';
el.appendChild(live);
el.appendChild(out);

const chart = lib.createChart(live);
const bars = lib.generateBars(1700000000, 120, 3600);
chart.addSeries('candlestick').setData(bars);
chart.addPriceLine({ price: bars[bars.length - 1].close, color: '#f5a623', lineWidth: 1, dashed: true, id: 'last' });
chart.timeScale.fitContent(120);

// One export per frame at most: the string is the whole paint, so exporting
// on every zoom event during a wheel glide would repeat the work.
let queued = false;
function exportNow() {
// Sized to the target panel rather than the live chart: the chart lays
// itself out for the export and puts the live layout back before returning.
// queued stays set until the export is done, so nothing the export itself
// does can schedule the next one: an export that re-queued on its own
// relayout would run once per frame for as long as the page is open.
try { out.innerHTML = chart.exportSVG({ width: out.clientWidth, height: out.clientHeight }); }
finally { queued = false; }
}
function queueExport() { if (!queued) { queued = true; requestAnimationFrame(exportNow); } }
chart.on('resize', queueExport);   // the first measured layout, and any later one
chart.on('zoom', queueExport);
chart.on('pan', queueExport);
requestAnimationFrame(function () { requestAnimationFrame(queueExport); });
return chart;

The options, the fallback rules and the serialising context behind the export are on Render Backends & SVG Export.

Copy-paste snippets

Want the full walkthrough? Head to the documentation.