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 puts everything together and saves layouts to your browser.
Minimal chart. The runtime files, one container and a datafeed that returns historical bars.
Loading chart… · Fixed sample period ending 15 October 2026.
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.