Lumen ChartsLumen Charts

Examples

Download Markdown

Every example runs on the real chart runtime with sample data. You don't need to sign in or connect a data feed. Pick an example to run it in the chart; its code is further down this page. The full demo puts everything together and saves layouts to your browser.

Minimal chart

The smallest working setup: the runtime files, one container and a datafeed. The sample datafeed returns historical bars in order, within the requested time range. Your first chart · Historical bars

export const symbols = ['AAPL', 'MSFT', 'BTCUSD'] as const
export const resolutions = ['15', '60', '1D'] as ResolutionString[]
const day = 86_400_000
export const sampleEnd = Date.UTC(2026, 9, 15)
const sampleStart = sampleEnd - 730 * day

export function resolutionMs(resolution: string): number {
  if (resolution === '1D' || resolution === 'D') return day
  const minutes = Number(resolution)
  if (!Number.isFinite(minutes) || minutes <= 0) throw new Error('Unsupported sample interval')
  return minutes * 60_000
}
export function sampleBar(symbol: string, time: number, step: number): Bar {
  const index = (time - sampleStart) / step
  const base = symbol === 'BTCUSD' ? 62000 : symbol === 'MSFT' ? 400 : 180
  const wave = Math.sin(index * .17) * base * .025 + Math.cos(index * .043) * base * .04
  const open = base + wave + index * base * .00002
  const close = open + Math.sin(index * 1.71) * base * .008
  return { time, open, close, high: Math.max(open, close) + base * .003, low: Math.min(open, close) - base * .003, volume: Math.round(100000 + Math.abs(Math.sin(index)) * 400000) }
}
export function historicalBars(symbol: string, resolution: string, from: number, to: number, countBack: number): Bar[] {
  const step = resolutionMs(resolution)
  const end = Math.min(Math.ceil(to * 1000 / step) * step, sampleEnd)
  const requestedStart = Math.min(from * 1000, end - Math.min(Math.max(0, countBack), 5000) * step)
  const start = Math.max(sampleStart, Math.ceil(requestedStart / step) * step, end - 5000 * step)
  const bars: Bar[] = []
  for (let time = start; time < end; time += step) bars.push(sampleBar(symbol, time, step))
  return bars
}
export function createDemoDatafeed(options: { realtime?: boolean; empty?: boolean } = {}) {
  const timers = new Map<string, ReturnType<typeof setInterval>>()
  let disposed = false
  const later = (callback: () => void) => setTimeout(() => { if (!disposed) callback() }, 0)
  const datafeed: IBasicDataFeed = {
    onReady(callback) { later(() => callback({ supported_resolutions: resolutions, supports_marks: false, supports_timescale_marks: false, supports_time: true })) },
    getServerTime(callback) { later(() => callback(sampleEnd / 1000)) },
    searchSymbols(input, _exchange, _type, callback) {
      later(() => callback(symbols.filter(symbol => symbol.includes(input.toUpperCase())).map(symbol => ({ symbol, full_name: symbol, ticker: symbol, description: `${symbol} · synthetic sample`, exchange: 'DEMO', type: 'stock' }))))
    },
    resolveSymbol(name, success, failure) {
      if (!symbols.includes(name as typeof symbols[number])) { later(() => failure('unknown_symbol')); return }
      const info: LibrarySymbolInfo = { name, ticker: name, description: `${name} · sample data`, type: 'stock', exchange: 'DEMO', listed_exchange: 'DEMO', session: '24x7', timezone: 'Etc/UTC', minmov: 1, pricescale: 100, has_intraday: true, has_daily: true, supported_resolutions: resolutions, volume_precision: 0, format: 'price', data_status: 'streaming' }
      later(() => success(info))
    },
    getBars(info, resolution, period, success, failure) {
      try {
        const bars = options.empty ? [] : historicalBars(info.ticker ?? info.name, resolution, period.from, period.to, period.countBack)
        later(() => success(bars, { noData: bars.length === 0 }))
      } catch (error) { later(() => failure(error instanceof Error ? error.message : 'Sample data unavailable')) }
    },

    subscribeBars(info, resolution, callback, id) {
      datafeed.unsubscribeBars(id)
      if (!options.realtime || options.empty || disposed) return
      const step = resolutionMs(resolution)
      let sequence = 0
      const bar = sampleBar(info.ticker ?? info.name, sampleEnd - step, step)
      timers.set(id, setInterval(() => {
        sequence += 1
        const close = bar.close + Math.sin(sequence * .4) * bar.close * .0005
        callback({ ...bar, close, high: Math.max(bar.high, close), low: Math.min(bar.low, close), volume: (bar.volume ?? 0) + sequence * 100 })
      }, 1000))
    },
    unsubscribeBars(id) { const timer = timers.get(id); if (timer) clearInterval(timer); timers.delete(id) },

  }
  return { datafeed, activeSubscriptions: () => timers.size, dispose() { disposed = true; for (const id of timers.keys()) datafeed.unsubscribeBars(id) } }
}

Simulated realtime

The latest bar updates once per second. Each subscription owns one timer, and unsubscribing clears it. Realtime updates

subscribeBars(info, resolution, callback, id) {
  datafeed.unsubscribeBars(id)
  if (!options.realtime || options.empty || disposed) return
  const step = resolutionMs(resolution)
  let sequence = 0
  const bar = sampleBar(info.ticker ?? info.name, sampleEnd - step, step)
  timers.set(id, setInterval(() => {
    sequence += 1
    const close = bar.close + Math.sin(sequence * .4) * bar.close * .0005
    callback({ ...bar, close, high: Math.max(bar.high, close), low: Math.min(bar.low, close), volume: (bar.volume ?? 0) + sequence * 100 })
  }, 1000))
},
unsubscribeBars(id) { const timer = timers.get(id); if (timer) clearInterval(timer); timers.delete(id) },

Themes

Switch between light and dark without recreating the chart. Use this site's theme toggle; the chart follows it with the call below. Themes and overrides

export function applyTheme(widget: ILumenChartingLibraryWidget, theme: 'light' | 'dark') {
  // Restyles the existing chart; drawings, indicators and the visible range stay as they are.
  return widget.changeTheme(theme)
}

Create a drawing

Add a trend line with the drawing API. Drawings

export function addTrendLine(widget: ILumenChartingLibraryWidget) {
  const chart = widget.activeChart()
  const range = chart.getVisibleRange()
  const to = range.to || sampleEnd / 1000
  const from = range.from || to - 40 * 86400
  const id = chart.createMultipointShape([
    { time: from + (to - from) * .25, price: 180 },
    { time: from + (to - from) * .75, price: 195 },
  ], { shape: 'trend_line' })
  if (!id) throw new Error('The drawing could not be created')
  return id
}

Add an indicator

Add a 20-period Moving Average with the built-in indicator API. Built-in indicators

export async function addMovingAverage(widget: ILumenChartingLibraryWidget) {
  const id = await widget.activeChart().createStudy('Moving Average', false, false, { length: 20 })
  if (!id) throw new Error('The study could not be created')
  return id
}

Save a layout

Save, list, load and delete layouts with the controls under the chart. This adapter keeps them in your browser; yours would call your own service. Save/load adapter

// Import these public types from the declarations in your authorized distribution.
export const storageKey = 'lumencharts-docs:layouts:v1'
type RecordEntry = ChartData & { id: string; timestamp: number }
type StoragePort = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
export function createDemoStorage(storage: StoragePort) {
  function read(): RecordEntry[] {
    const raw = storage.getItem(storageKey)
    if (!raw) return []
    let parsed: unknown
    try { parsed = JSON.parse(raw) } catch { throw new Error('Saved sample layouts are invalid. Reset saved layouts to start again.') }
    if (!Array.isArray(parsed) || parsed.length > 30 || !parsed.every(item => item && typeof item === 'object' && typeof item.id === 'string' && /^\d+$/.test(item.id) && typeof item.name === 'string' && typeof item.content === 'string' && typeof item.symbol === 'string' && typeof item.resolution === 'string' && Number.isFinite(item.timestamp))) throw new Error('Saved sample layouts are invalid. Reset saved layouts to start again.')
    return parsed as RecordEntry[]
  }
  function write(records: RecordEntry[]) {
    const value = JSON.stringify(records)
    if (value.length > 2_000_000) throw new Error('Sample storage is full. Delete a saved layout first.')
    storage.setItem(storageKey, value)
  }
  const adapter: Pick<IExternalSaveLoadAdapter, 'getAllCharts' | 'saveChart' | 'getChartContent' | 'removeChart'> = {
    async getAllCharts() { return read().map(({ id, name, symbol, resolution, timestamp }) => ({ id: Number(id), name, symbol, resolution: resolution as ResolutionString, timestamp })) },
    async saveChart(chart) {
      const records = read()
      const id = String(chart.id ?? Math.max(Date.now(), ...records.map(item => Number(item.id) + 1)))
      const next = records.filter(item => item.id !== id)
      if (next.length >= 30) throw new Error('Save up to 30 sample layouts. Delete one first.')
      next.push({ ...chart, id, timestamp: Math.floor(Date.now() / 1000) })
      write(next)
      return id
    },
    async getChartContent(id) { const item = read().find(item => item.id === String(id)); if (!item) throw new Error('Saved layout was not found'); return item.content },
    async removeChart(id) { write(read().filter(item => item.id !== String(id))) },
  }
  return { adapter, reset: () => storage.removeItem(storageKey) }
}

Mount and cleanup

Every switch between examples removes the chart and stops its datafeed before the next one starts. Do the same when the view that hosts your chart closes. Lifecycle and cleanup

export function removeChart(widget: ILumenChartingLibraryWidget | undefined, feed: { dispose(): void } | undefined) {
  // Remove the widget before the view goes away, then stop the datafeed's timers and sockets.
  widget?.remove()
  feed?.dispose()
}

Multiple charts

Two charts in one widget, each working on its own. Workspaces and comparisons

export function splitWorkspace(widget: ILumenChartingLibraryWidget) {
  widget.setLayout('2h')
}

Pine moving average

Load the optional Pine worker and run a short script. Optional Pine

export async function addPineAverage(widget: ILumenChartingLibraryWidget) {
  return widget.activeChart().createPineStudy(
    '//@version=5\nindicator("Sample average", overlay=true)\nplot(ta.sma(close, 20))',
  )
}

About these previews

The chart loads the compiled runtime so it can run here, but this site doesn't distribute the library for reuse. See runtime access and hosting. If a chart fails to load, you'll see a retry button. Anything you save stays in your browser and is only used by these docs. The code uses public integration types that come with your authorized distribution.

On this page