DocumentationChart State

Chart State

chart.getState() returns a JSON-safe snapshot of everything the chart owns; chart.restoreState(state) puts it back. Saved layouts, chart templates, an objects panel, and drawing persistence all ride on this one pair.

localStorage.setItem('layout', JSON.stringify(chart.getState()));
 
// later, after your data has loaded
const report = chart.restoreState(JSON.parse(localStorage.getItem('layout')));

What is captured - and what is not

The rule: the chart serialises what the chart owns.

CapturedRestoredNotes
Visible logical range, bar spacingYesApplied only when the chart has data - see below.
Navigation optionsYesOptional navigation block: mousePan and defaultVisibleBars. The saved viewport takes precedence over the default view.
Grid (visibility, colours, styles, width, spacing)Yes
Canvas options (crosshair, scale text and lines, plot margins)YesThe Appearance tab’s block. See Theming and chart options.
Status-line switchesYesApplied to every pane legend.
Trading coloursYesHeld on the chart, so reading them never instantiates the trade layer.
Event-type visibilityYesWhich of earnings / dividends / splits / news the strip draws.
Crosshair modeYes
TimezoneYesA saved zone the runtime no longer recognises is skipped, not thrown: one stale name must not cost the whole layout.
Pane weightsYesPanes are created as needed.
Price scales (margins, minMove, mode, inverted, auto-scale, pinned range)YesPer pane.
Indicator instances (descriptor id, settings, pane, visibility)YesSince 2.1.7, visible: false survives restoration. Older layouts default to visible. Runtime instance IDs are recreated.
DrawingsRound-trippedAn opaque slot the drawing tier owns; the base engine only carries it.
Series descriptors (type, style, pane, price scale)NoReported back to you - see below.
Series dataNoThe host supplies the bars; they are not embedded in the saved layout.
Host-registered Objects providersNoRegister them again for a replacement chart and persist their state through the host.

The settings blocks are restored before the panes, deliberately: the chart-wide plot margins land first so a pane’s own saved marginTop / marginBottom is the more specific answer and wins. A layout saved by a host that built its dialog from the settings schema therefore brings the dialog’s settings back with it, with no second store.

Navigation settings are restored before the saved viewport. A saved navigation.defaultVisibleBars therefore remains the default for future resets without replacing the exact range the user saved. Older snapshots may omit navigation; the chart keeps its current navigation options. The count controls the initial/reset view of loaded data, not the amount of history your feed requests or retains.

⚠️

restoreState never recreates series. The chart has no way to know what data belongs in them. It restores everything else and hands you the series descriptors it saw, so you rebuild them from your own feed and re-apply the saved styling.

const report = chart.restoreState(saved);
 
for (const s of report.series) {
  const series = chart.addSeries(s.type, { paneIndex: s.paneIndex, style: s.style, priceScaleId: s.priceScaleId });
  series.setData(await loadBars(mySymbol, myInterval));   // your data
}
 
// Logical ranges index bars, so re-apply the viewport once the data has landed.
chart.restoreState(saved);

RestoreReport

FieldTypeDescription
appliedbooleanFalse when the payload was not a usable state object.
seriesSeriesState[]Series descriptors found in the state - rebuild these yourself.
indicatorsnumberIndicator instances recreated.
reasonstring?Why the payload was rejected.

Ordering

Logical ranges index bars, so a viewport restored onto an empty chart means nothing. The reliable sequence is:

  1. restoreState(saved) - grid, panes, price scales, indicators.
  2. Rebuild your series from report.series and feed them.
  3. restoreState(saved) again (or setVisibleLogicalRange(saved.viewport)) for the viewport.

Calling restoreState twice is safe: indicators are replaced, not appended, so restore is idempotent.

Versioning and forward compatibility

Every snapshot carries a version (CHART_STATE_VERSION). A state from a newer version is rejected rather than half-applied - applied: false with a reason - so an older build reading a newer layout degrades visibly instead of silently dropping settings.

An indicator whose tier is not loaded is skipped, not thrown: restoring a layout that uses openalgo-charts/indicators in an app that has not imported the tier leaves that indicator out and restores the rest.

The drawings slot

ChartState.drawings is an opaque passthrough. The base engine stores and returns it untouched; the drawing tier reads and writes it. That means an app which already persists chart state keeps drawings for free once the tier is loaded, with no new storage plumbing.

chart.setDrawingState(payload);   // what the drawing tier does
chart.drawingState();             // read it back

Coordinate helpers

State restore and drawing anchors both need to convert between time and screen x. Because the time axis is gapless - weekends, holidays, and session breaks collapse - many x positions have no bar behind them, so the whole-index helpers are not enough.

MethodDescription
chart.timeToCoordinate(time)UTC seconds → container-relative x (media px).
chart.coordinateToTime(x)x → UTC seconds, interpolated between bars and extrapolated past the right edge.
chart.priceToCoordinate(price, paneIndex?)Price → y.
chart.coordinateToPrice(y, paneIndex?)y → price.

coordinateToTime is what makes a two-axis drag work: chart.subscribeDrag passes the time under the cursor as a third argument alongside the price, so a trendline endpoint or a forward projection has a usable time even where no bar exists.

chart.subscribeDrag(
  (id, price, time) => moveAnchor(id, { time, price }),
  (id, price, time) => commitAnchor(id, { time, price }),
);