Saving and loading charts
What each kind of saved state holds, quick snapshots, and the save/load adapter behind the built-in Save and Load controls.
What you save depends on what you want users to get back. Layouts, indicator templates, drawing templates and chart settings templates each cover a different part of the chart.
| What's saved | What it contains | Usually handled by |
|---|---|---|
| Chart layout | How the chart is set up, plus saved analysis | A complete layout adapter |
| Study template | A set of indicator settings | A study template adapter |
| Drawing template | A reusable drawing style | A drawing template adapter |
| Chart settings template | Appearance and settings | A chart template adapter |
| Separate drawing state | Drawings and groups, stored on their own | A separate drawing adapter |
Quick snapshots
widget.save(state => {
localStorage.setItem('my-chart:layout:v1', JSON.stringify(state));
});
const saved = localStorage.getItem('my-chart:layout:v1');
if (saved) widget.load(JSON.parse(saved));Be ready for JSON parse errors, a full storage quota and saved data the runtime rejects. Putting a version in your own storage key doesn't change the runtime's own schema version.
Built-in Save and Load controls
Provide a complete adapter and the chart shows its Save and Load controls. If the layout adapter is missing or incomplete, those controls stay off. The chart never guesses a storage URL. The sample stores layouts in the browser and supports list, load, save and delete.
Save/load adapter
A layout adapter needs all four operations: list, save, load and delete. Handle sign-in in your backend requests. Don't put credentials in the chart options.
| Method | Returns |
|---|---|
getAllCharts() | A list of layout details |
saveChart(chartData) | The saved layout's ID |
getChartContent(id) | The content string saved earlier |
removeChart(id) | Completes once the layout is deleted |
Example: saving in the browser
// 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) }
}The legacy interface types list extra methods for other features. The runtime checks for the complete layout methods on their own. Optional template and separate drawing storage each need their own complete set of methods. Don't add empty methods that just report success to satisfy a broad type.
On your server
Check that the signed-in user is allowed to perform every list, read, write and delete. The client_id and user_id strings narrow down whose data you're working with, but they don't prove who the user is. Limit list sizes in the database, and enforce a size limit on content before you store it.
Return saved content exactly as you received it; don't rewrite what's inside. Give a clear error for missing records, denied access, invalid content and temporary failures. If a response arrives after another operation has started, or after the chart was removed, ignore it.
Loading chart… · Fixed sample period ending 15 October 2026.