# Lumen Charts documentation

Public integration documentation for a proprietary library. Runtime access is separately authorized. Exported API signatures do not imply that all legacy capabilities are supported; preserve compatibility notes when using these documents.

<!-- Public documentation export; not library implementation source. -->
# Examples

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](https://docs.lumencharts.com/demo/) puts everything together and saves layouts to your browser.

[Interactive chart example](https://docs.lumencharts.com/examples/) — runs in a browser with synthetic data.

## 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](https://docs.lumencharts.com/charting-library/getting-started/quickstart) · [Historical bars](https://docs.lumencharts.com/charting-library/datafeed/history)

```ts
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](https://docs.lumencharts.com/charting-library/datafeed/realtime)

```ts
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](https://docs.lumencharts.com/charting-library/configuration/themes)

```ts
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](https://docs.lumencharts.com/charting-library/drawings)

```ts
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](https://docs.lumencharts.com/charting-library/studies)

```ts
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](https://docs.lumencharts.com/charting-library/persistence#saveload-adapter)

```ts
// 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](https://docs.lumencharts.com/charting-library/getting-started/lifecycle)

```ts
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](https://docs.lumencharts.com/charting-library/configuration/workspaces)

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

## Pine moving average

Load the optional Pine worker and run a short script. [Optional Pine](https://docs.lumencharts.com/charting-library/studies/pine)

```ts
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](https://docs.lumencharts.com/charting-library/getting-started/installation). 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.

---
Documentation page: https://docs.lumencharts.com/examples/
