# The shell and layouts

> The complete chart application: symbol search, panes, layouts and stored drawings.

Section: Customization. Source: https://ortexcharts.com/docs/shell

## One Call for the Whole Application

`createChartShell` wraps a financial chart in the chrome a user expects: a toolbar, a
drawing rail, symbol search, an indicator menu, a data picker, scale controls, screenshot,
theme and full screen. It is plain DOM with no framework dependency, styled through CSS
custom properties derived from the chart theme.

```ts
import { createChartShell } from "@ortex-charts/ui";

const shell = createChartShell(container, {
  symbol: "AAPL",
  datafeed,                                    // any Datafeed; search comes from searchSymbols
  overlays: myOverlays,                        // host-defined data series
  layout: savedLayout,                         // what the user chose last time, or null
  onLayoutChange: (layout) => api.saveChartLayout("stock-page", layout),
  drawingsStore: myDrawingsStore,              // drawings that follow the symbol
});
```

In React it is `<ChartShell symbol="AAPL" layout={saved} options={{ datafeed, overlays, onLayoutChange }} />`
from [`@ortex-charts/react`](https://ortexcharts.com/docs/react).

_Live example: [chart-shell](https://ortexcharts.com/examples/chart-shell)_

`ChartShellOptions` extends `FinancialChartOptions`, so every chart option — theme, time
zone, resolution, session, watermark, crosshair, branding, `data` — is accepted in the same
object.

## The Shell Handle

```ts
interface ChartShell {
  readonly root: HTMLElement;
  readonly chart: FinancialChart;             // .chart.chart is the core Chart
  readonly drawings: DrawingsPrimitive | null;
  readonly binding: DatafeedBinding | null;

  symbol(): string;
  setSymbol(symbol: string): Promise<void>;
  resolution(): string;
  setResolution(resolution: string): Promise<void>;
  setSeriesType(type: MainSeriesType): void;
  setScaleMode(mode: ScaleMode): void;

  addIndicator(id: string, inputs?: Record<string, number | string | boolean>): IndicatorInstance;
  indicators(): readonly IndicatorInstance[];

  overlays(): Array<{ id: string; symbol: string; key: string; label: string; on: boolean }>;
  setOverlay(id: string, on: boolean, symbol?: string): Promise<void>;
  openDataPicker(): void;

  placeSeries(seriesId: string, placement: Partial<SeriesPlacement>): void;
  placementOf(seriesId: string): SeriesPlacement | null;

  addAction(action: ToolbarAction): () => void;
  exportCsv(): string;

  getLayout(): ChartLayout;
  setLayout(layout: ChartLayout | null): Promise<void>;

  setTheme(theme: Theme | "dark" | "light"): void;
  toggleFullscreen(): Promise<void>;
  isFullscreen(): boolean;

  replay(): ReplayController;
  startReplay(index?: number): void;
  stopReplay(): void;

  remove(): void;
}
```

Nothing is hidden behind the toolbar. `shell.chart` is the `FinancialChart`,
`shell.chart.chart` is the core `Chart`, and `shell.drawings` is the drawing primitive, so
anything on [Series types](https://ortexcharts.com/docs/series), [Indicators](https://ortexcharts.com/docs/indicators),
[Drawing tools](https://ortexcharts.com/docs/drawings) or [Order flow](https://ortexcharts.com/docs/order-flow) applies unchanged.

## Layouts: The Chart Never Persists, the Host Does

A **layout** is the user's choice of *how* the chart looks, independent of *which* symbol is
shown. It is plain JSON with a version.

```ts
interface ChartLayout {
  version: 1;
  seriesType: "candlestick" | "bar" | "line" | "area" | "baseline";
  scaleMode: "normal" | "logarithmic" | "percentage" | "indexedTo100";
  resolution?: string;
  indicators: Array<{ id: string; inputs?: Record<string, number | string | boolean>; visible?: boolean }>;
  overlays: string[];                                   // ids that are on; `id@SYMBOL` for a compared symbol
  scales?: Record<string, { scale: "right" | "left" | "own"; pane: "main" | "separate"; visible?: boolean }>;
  colors?: Record<string, string | { up: string; down: string }>;   // chosen from the legend menu
  legendCollapsed?: boolean;                            // with the "legend" toolbar item
  drawings?: Drawing[];                                 // only with layoutIncludesDrawings
}
```

```ts
const layout = shell.getLayout();
await shell.setLayout(layout);        // null resets to the shell defaults, including defaultOn overlays
```

`onLayoutChange` fires at most once per frame after any user action that changes the layout:
chart type, scale mode, resolution, indicator added, removed, reconfigured or hidden,
overlay toggled, series moved to another scale or pane, and drawings when they are included.

The shell never touches storage. Where the layout lives — per user, per page kind, per
symbol — is a host decision. The usual arrangement is one row per user and page kind:

```ts
const saved = await api.get(`/chart-layout/stock/`);

const shell = createChartShell(el, {
  symbol,
  datafeed,
  overlays,
  layout: saved,
  onLayoutChange: (l) => api.put(`/chart-layout/stock/`, l),
});
```

Unknown overlay and indicator ids in a stored layout are ignored rather than throwing, so
renaming or retiring an overlay does not break every saved layout in your database.

## Colors the Reader Chooses

Every legend row has a menu, and in it a row of swatches. One click repaints that series;
a candlestick or bar series gets two rows, up and down, because a reader who wants green
and red will want to say which green and which red.

The choice is keyed by the series **title**, not by its id, so it survives a symbol change
and a reload: "Price" stays the color the reader picked, whatever is being priced. When a
second series of the same title arrives — the same overlay for a compared symbol, say — it
takes a contrasting color instead, so the two can be told apart:

```ts
import { contrastingColor } from "@ortex-charts/ui";

shell.setSeriesColor(series.id, "#7FA3F0");                     // a line
shell.setSeriesColor(series.id, { up: "#22C08A", down: "#EF4E5A" });  // candles or bars
shell.seriesColorOf(series.id);                                 // → what it is now, or null

contrastingColor(["#7FA3F0", "#22C08A"], palette);              // the furthest hue left
```

The colors travel in the layout as `colors`, so storing the layout stores them, and
`setLayout(null)` puts every series back on the theme.

## Overlays Are Host-Defined

An overlay is a named thing your application knows how to put on the chart. The shell shows
the label and group in the Data menu, keeps the check marks in sync, stores only the id in
the layout, and calls your disposer when it is switched off.

```ts
import type { OverlayDef } from "@ortex-charts/ui";

const overlays: OverlayDef[] = [
  {
    id: "si",
    label: "Short interest",
    group: "Securities lending",
    description: "Estimated short interest as a percentage of free float",
    defaultOn: true,
    add: async ({ chart, symbol, resolution }) => {
      const rows = await api.shortInterest(symbol, resolution);
      const s = chart.addSeries("line", {
        title: "Short interest",
        priceScaleId: "left",
        align: "forwardFill",
        priceFormat: { type: "percent", precision: 2, minMove: 0.01 },
      });
      s.setData(rows);
      return () => chart.removeSeries(s.id);
    },
  },
];
```

`add` receives `{ chart, fc, shell, symbol, isCurrentSymbol, resolution }`, may be
asynchronous, and returns the function that removes the overlay again. It is called again
after **every symbol or resolution change**, with the previous disposer run first, so the
host fetches for `ctx.symbol` inside `add` and never has to watch the chart itself.

`otherSymbolOnly: true` marks an overlay that only makes sense for a compared symbol and
hides it for the chart's own.

### Overlays That Load as the Reader Scrolls

An overlay with twenty years of daily history behind it does not have to fetch twenty years
to draw the last six months. `ctx.windowed` gives the series the visible span plus a margin,
and fetches more only when the view moves past what it holds:

```ts
add: async (ctx) => {
  const s = ctx.chart.addSeries("line", { title: "Short interest", priceScaleId: "left" });
  const loader = ctx.windowed(s, (from, to) => api.shortInterest(symbol, from, to));
  return () => { loader.dispose(); ctx.chart.removeSeries(s.id); };
},
```

`overlayLoading` on the shell sets the defaults for every overlay that asks for one, and the
third argument overrides them per overlay. The options are the loader's own, described under
[loading history in windows](https://ortexcharts.com/docs/data#loading-history-in-windows).

```ts
createChartShell(el, {
  overlayLoading: { margin: 0.5, earliest: Date.UTC(2005, 0, 1) },
});
```

## Comparing Symbols

When a datafeed is bound, the shell adds a built-in **Price** overlay for other symbols: the
closes of another instrument as a line, with the scale switched to percent so the two are
comparable. Comparing AAPL with MSFT therefore needs no host code beyond `searchSymbols`.

Overlays for another symbol are stored as `id@SYMBOL` in the layout, and `overlayKey` and
`parseOverlayKey` are exported for hosts that need to read or build those keys.

```ts
await shell.setOverlay("price", true, "MSFT");
shell.overlays();   // [{ id, symbol, key, label, on }, …] including compared symbols
```

Set `compare: false` to remove the feature.

## The "Add Data to Chart" Modal

With more than eight overlays, or whenever symbol search is available, the Data button opens
a modal instead of a dropdown. `dataPicker: "menu" | "modal" | "auto"` overrides the
decision.

The modal has a series search, a category list with counts on the left, grouped rows that
toggle on click, and a symbol search that points the picker at another instrument so its
series can be compared. `shell.openDataPicker()` opens it from your own control, and
`openDataPicker` is exported from `@ortex-charts/ui` for hosts that want the same component
somewhere else.

## Moving a Series to Another Scale or Pane

Every legend row carries a `⋯` button. For an overlay or an indicator output it offers
**Right scale**, **Left scale**, **Own scale** (a hidden scale, so the series autoscales
alone), **Main pane**, **New pane**, **Hide** and **Remove**, plus **Collapse pane**,
**Expand pane**, **Maximise pane** and **Restore panes** for the pane the series sits on.

The choice is stored in `layout.scales`, keyed by series title, so it survives an overlay
being re-added after a symbol change and comes back with the user's saved layout.

```ts
shell.placeSeries(series.id, { scale: "own", pane: "separate" });
shell.placementOf(series.id);   // { scale, pane, visible } or null
```

The underlying core methods are `chart.setPaneCollapsed`, `chart.isPaneCollapsed`,
`chart.maximizePane`, `chart.restorePanes`, `chart.maximizedPane` and
`chart.subscribePaneChange`, and `chart.subscribeLegendMenu` is the hook a host uses to
build its own menu on that button.

A collapsed pane is a 24-pixel legend strip. Its series stay attached and keep updating, so
collapsing is a display state rather than a teardown.

## Drawings That Follow the Symbol

The most-requested behavior in this category — a trend line drawn on AAPL being there on
every chart of AAPL — is a storage question, and the host owns storage. Give the shell a
`drawingsStore` and it does the rest.

```ts
import { localStorageDrawingsStore } from "@ortex-charts/ui";

createChartShell(el, { symbol: "AAPL", datafeed, drawingsStore: localStorageDrawingsStore() });

// or your own, server-backed:
createChartShell(el, {
  symbol: "AAPL",
  datafeed,
  drawingsStore: {
    load: (symbol) => api.get(`/drawings/${symbol}`),
    save: (symbol, drawings) => api.put(`/drawings/${symbol}`, drawings),
  },
});
```

Both methods may be asynchronous. Drawings are saved whenever they change and on every
symbol switch, and loaded for the new symbol.

With a store in use, leave `layoutIncludesDrawings` off: a layout then describes the chart
and drawings describe the symbol, which is the separation users expect. Turn
`layoutIncludesDrawings` on instead only when the page is inherently single-symbol and you
want one blob rather than two.

## Symbol Logos

`SymbolInfo.logo` — a URL or a data URI — is shown in the symbol button and before the main
series in the legend. A missing or failing image falls back to the color dot silently. The
[datafeed](https://ortexcharts.com/docs/data) supplies it from `resolveSymbol` or `searchSymbols`, and an
[adapter spec](https://ortexcharts.com/docs/data) maps it with `symbol.logo`, which accepts a template such as
`https://logos.example.com/{symbol}.png` or `{info.logo_url}`.

## CSV Export

The `download` toolbar item is off by default. Turning it on exports every visible series on
the main timeline as CSV.

```ts
createChartShell(el, {
  symbol: "AAPL",
  datafeed,
  toolbar: { download: true },
  download: {
    fileName: "aapl-daily.csv",
    onDownload: (csv, shell) => api.saveExport(shell.symbol(), csv),   // intercept it
  },
});

const csv = shell.exportCsv();   // the same text, on demand
```

With `onDownload` set, the browser does not save a file; the text goes to you instead.

## Screenshots

```ts
createChartShell(el, {
  screenshotFileName: "chart.png",
  onScreenshot: (canvas) => canvas.toBlob((blob) => uploadToSlack(blob!)),
});

const canvas = shell.chart.chart.takeScreenshot();   // from code, at any time
```

`takeScreenshot` composes every pane canvas into one and is on the core chart, so it works
without the shell.

## Full Screen

`toggleFullscreen()` uses the Fullscreen API on the shell root and falls back to a
fixed-position layer where the API is unavailable, which is the case on iOS Safari. Escape
leaves either. `isFullscreen()` reports the current state, and the toolbar button reflects
it.

## Events

```ts
createChartShell(el, {
  onLayoutChange: (layout) => save(layout),
  onSymbolChange: (symbol, info) => setPageTitle(symbol, info?.name),
  onResolutionChange: (resolution) => track("resolution", resolution),
  onThemeChange: (theme) => persistThemePreference(theme),
  onScreenshot: (canvas) => upload(canvas),
  onError: (error) => report(error),
});
```

`onError` catches datafeed and overlay failures that would otherwise be silent; wiring it to
your error reporter is worth the two lines.

## What the Host Has to Provide

The chart side is finished; the persistence side is yours. For a complete integration you
need four things, none of which the library can decide for you:

- **A per-user layout store keyed by page kind**, one JSON body per user and page, read on
  mount and written from `onLayoutChange`.
- **An overlay registry**, one `OverlayDef[]` per page kind, wrapping your own series
  endpoints.
- **A drawings store per symbol**, if you want drawings to follow the symbol.
- **Defaults for anonymous users**, which is simply a layout object in your bundle.

Nothing in the chart depends on those existing. Without any of them the shell still works;
it just forgets everything on reload.
