Customization
The shell and layouts
The complete chart application: symbol search, panes, layouts and stored drawings.
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.
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.
@ortex-charts/uiChartShellOptions extends FinancialChartOptions, so every chart option — theme, time
zone, resolution, session, watermark, crosshair, branding, data — is accepted in the same
object.
The Shell Handle
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, Indicators,
Drawing tools or 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.
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 }>;
drawings?: Drawing[]; // only with layoutIncludesDrawings
}
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:
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.
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.
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.
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.
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.
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.
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 supplies it from resolveSymbol or searchSymbols, and an
adapter spec 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.
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
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
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.