Customization
The toolbar and your buttons
Hide what you do not want, add your own buttons and menus, and keep it accessible.
Read this page as Markdown, or take the whole documentation in one file.
The Toolbar Is a Set of Switches
createChartShell draws a toolbar above the chart and a drawing rail beside it. Every item
is independently switchable, and everything an item does is also a method on the returned
shell, so a host can hide the whole toolbar and still drive the chart from its own controls.
import { createChartShell } from "@ortex-charts/ui";
const shell = createChartShell(el, {
symbol: "AAPL",
datafeed,
toolbar: { theme: false, screenshot: false, download: true },
resolutions: ["5", "15", "60", "1D", "1W"],
indicators: ["sma", "ema", "rsi", "macd", "bb"],
drawingTools: ["trendline", "ray", "horizontalLine", "rectangle", "fibRetracement"],
});@ortex-charts/uiThe Built-In Items
ToolbarItem is the union of the switch names. Everything is on by default except
download.
| Item | What it is | Shell method |
|---|---|---|
symbol |
Symbol button opening a search popover; Enter loads typed text | setSymbol |
resolution |
Segmented buttons labeled 1m 5m 15m 1h D W M |
setResolution |
ranges |
Period buttons: 1D 1W 1M 1Q 1Y 5Y 10Y by default |
setRange |
seriesType |
Candles, bars, line, area, baseline, plus Heikin-Ashi and hollow toggles | setSeriesType |
indicators |
Searchable list of the registry, plus the active list with settings, hide and remove | addIndicator, indicators |
overlays |
Host data overlays, as a menu or the "Add data to chart" modal | setOverlay, overlays, openDataPicker |
scale |
Auto, logarithmic and percent scale modes | setScaleMode |
fit |
Fit the whole history into view | — |
realtime |
Scroll to the latest bar | — |
replay |
Bar replay; off by default | replay, startReplay, stopReplay |
screenshot |
PNG download, or onScreenshot |
— |
download |
CSV of the visible series; off by default | exportCsv |
theme |
Light and dark toggle | setTheme |
colorBlind |
Colour-blind safe palette, independent of light and dark | setTheme |
fullscreen |
Full screen and back | toggleFullscreen |
toolbar: { replay: true, download: true, theme: false }
Switching everything off leaves a bare chart that is still fully driven by the shell methods, which is the supported way to build a completely custom interface without giving up symbol binding, layouts and overlays.
drawings: false removes the drawing rail entirely. indicators: false removes the
indicator menu. indicators: ["sma", "ema"] limits which ones the menu offers without
affecting what addIndicator can do from code.
Configuration as Data
Every switch is plain JSON, so a company's chart configuration can live in a checked-in file with only the callbacks attached in code.
{
"resolutions": ["5", "15", "60", "1D", "1W"],
"toolbar": { "theme": true, "colorBlind": true, "screenshot": false, "download": true },
"indicators": ["sma", "ema", "rsi", "macd", "bb"],
"drawingTools": ["trendline", "ray", "horizontalLine", "rectangle", "fibRetracement"],
"dataPicker": "modal",
"compare": true
}
import config from "./chart-config.json";
createChartShell(el, {
...config,
datafeed,
overlays,
onLayoutChange: (layout) => api.saveChartLayout("stock-page", layout),
});
Period Buttons
A price chart is read in periods — the last month, the last year — so the toolbar carries them beside the resolutions. The two are different questions: a resolution is how coarse each bar is, a period is how much of the timeline is on screen.
createChartShell(el, {
datafeed,
ranges: [
{ label: "1D", ms: 86_400_000, resolution: "5", title: "The last day, in five-minute bars" },
{ label: "1W", ms: 7 * 86_400_000, resolution: "60" },
{ label: "1M", ms: 30 * 86_400_000 },
{ label: "1Q", ms: 91 * 86_400_000 },
{ label: "1Y", ms: 365 * 86_400_000 },
{ label: "5Y", ms: 5 * 365 * 86_400_000, resolution: "1W" },
{ label: "10Y", ms: 10 * 365 * 86_400_000, resolution: "1W" },
],
rangeDisplay: "auto",
});
ranges defaults to a day, a week, a month, a quarter, a year, five years and ten years,
with no resolution attached. DEFAULT_RANGES is exported if you want to extend that list
rather than replace it. Pass false, or toolbar: { ranges: false }, to leave the buttons
out.
Seven periods are seven clicks saved on a trading screen and seven buttons of clutter in a
sidebar, and the same chart is often both. rangeDisplay decides which it is:
| Value | What you get |
|---|---|
"auto" (default) |
Spelled out when the chart is wide enough, folded into one button when it is not, changing as the chart is resized |
"buttons" |
Always spelled out |
"menu" |
Always one button naming the current period |
Folded, the button says which period is showing, or Period when the reader has moved to a
view that is not one of them.
| Field | What it does |
|---|---|
label |
What the button says |
ms |
How much time it shows, ending at the last bar |
resolution |
Switch resolution with it, for spans where the two only make sense together |
title |
Tooltip and accessible name |
Two details worth knowing. Each period ends at the last bar rather than at the clock, so a market that closed on Friday still shows its last month rather than a month of empty axis. And the button matching the view is lit only while it matches: pan or zoom away and it stops claiming to be what you are looking at.
A period shorter than the chart can zoom to settles wider than asked, because bar spacing is
capped — a week is seven daily bars, and seven bars do not stretch across a wide pane. That is
what the resolution field is for.
shell.setRange(ms, resolution?) does the same thing from your own control.
One Row, Not Two
A toolbar that carries every control wraps onto a second row on anything narrower than a wide desktop. That row costs the chart about forty pixels of height, and it costs them from the part of the screen a reader is actually looking at. So by default it does not wrap: it gives things up instead, in order of how little they are missed.
| Step | What goes | What is left |
|---|---|---|
| 1 | The words on buttons that read as icons | Candles, Indicators, Data and Drawing tools as icons, with the words as tooltips |
| 2 | The period buttons | One button naming the period showing |
| 3 | The view and export controls | One More button holding them as a palette |
| 4 | The middle resolutions | The shortest, the longest and the selected one, with a chevron holding the rest |
Each step is applied only if the one before it was not enough, and the whole thing runs again whenever the chart is resized, so a chart that grows takes it all back. Everything given up stays one click away.
createChartShell(el, {
symbol: "AAPL",
datafeed,
toolbarFit: "auto", // the default; "wrap" takes a second row instead
extrasCollapsed: true, // start with the tail folded, at any width
drawingRailCollapsed: true, // the drawing tools folded to a thumb
});
toolbarFit is about what fits. extrasCollapsed and drawingRailCollapsed are about what
you want to start with, and they hold at every width. The two are kept apart on purpose:
folding because a chart is narrow is not the same as a reader choosing to fold, so a reader
who opens the tail keeps it open at every width that can hold it.
extrasCollapsed folds the right-hand end of the toolbar: the scale modes, fit, the legend
and drawing-rail toggles, realtime, replay, download, screenshot, theme, colour-blind and full
screen, behind a single More button. Opening it shows the same buttons as a small palette,
with an item that puts them back in the toolbar for good.
drawingRailCollapsed does the same for the drawing tools down the left edge, leaving a thumb
that opens them.
Both are starting positions, not locks. The reader's own choice is recorded in the layout and outranks the option on every later visit, and a layout that has never carried the field, which is every layout saved before you turned the option on, counts as no choice rather than as a vote for the old behaviour.
shell.setExtrasCollapsed(false); // and shell.isExtrasCollapsed()
shell.setDrawingRailCollapsed(true);
Your Own Buttons
ToolbarAction adds a button or a dropdown. It is data plus callbacks, so it fits the same
configuration-file pattern.
interface ToolbarAction {
id: string;
label?: string; // icon-only when omitted
icon?: IconName | string; // a built-in name, or your own <svg …> markup
title?: string; // tooltip and accessible name; defaults to label
placement?: "start" | "middle" | "end";
onClick?: (shell: ChartShell, event: MouseEvent) => void;
menu?: Array<{ label: string; icon?: IconName; hint?: string; onSelect: (shell: ChartShell) => void }>;
active?: boolean; // render as pressed
disabled?: boolean;
}
createChartShell(el, {
symbol: "AAPL",
datafeed,
actions: [
{
id: "api",
label: "Download with API",
icon: "api",
placement: "end",
onClick: (shell) => openApiDialog(shell.symbol(), shell.resolution()),
},
{
id: "share",
icon: "share",
title: "Share",
menu: [
{ label: "Copy link", onSelect: (s) => navigator.clipboard.writeText(linkFor(s.getLayout())) },
{ label: "Post to Slack", icon: "camera", onSelect: (s) => slack(s.chart.chart.takeScreenshot()) },
],
},
],
});
placement decides where the button lands: start puts it with the symbol and resolutions,
middle with the chart-type menus, and end at the right, which is the default.
At runtime, shell.addAction(action) adds one and returns a function that removes it again,
which is how a button appears only in a particular application state.
const removeAlertButton = shell.addAction({
id: "add-alert",
icon: "plus",
title: "Add alert at the last close",
onClick: (s) => {
const last = s.chart.main.lastIndex();
const v = s.chart.main.valueAt(last);
if (v && "close" in v) s.chart.alerts().add({ price: v.close, label: "New alert" });
},
});
removeAlertButton();
A Button That Carries State
active is read when the toolbar is built, so a button whose meaning changes — an
adjusted-prices toggle, a "follow the last price" switch — is updated with setAction
rather than by handing the shell a new definition. Rebuilding the options array, which is
what a React host does on every render, changes nothing: the shell is created once.
let adjusted = false;
createChartShell(el, {
datafeed,
actions: [
{
id: "adj",
label: "ADJ",
title: "Adjust prices for dividends",
active: adjusted,
onClick: (shell) => {
adjusted = !adjusted;
shell.setAction("adj", {
active: adjusted,
title: adjusted ? "Showing dividend-adjusted prices" : "Adjust prices for dividends",
});
void shell.binding?.reload(); // the feed reads the flag itself
},
},
],
});
setAction(id, patch) takes any of active, disabled, label and title, changes only
what the patch names, and does nothing for an id the toolbar does not have. active also
sets aria-pressed, so the state is announced rather than only drawn.
binding.reload() is the other half of that pattern: the same symbol fetched again because
something the feed reads for itself changed. It does not resolve the symbol a second time,
and it puts the reader back where they were looking instead of jumping to the right edge.
Icons
ICONS is a record of 52 inline SVG path fragments and icon(name) builds an <svg>
element from one. They are 20-by-20, single stroke, and use currentColor, so they inherit
the button color and follow the theme.
import { ICONS, icon } from "@ortex-charts/ui";
Object.keys(ICONS); // every available IconName
const el = icon("magnet");
The names, grouped by what they are for:
- Series types:
candles,bars,line,area,baseline,heikinAshi,hollow. - Tools:
cursor,crosshair,trendline,ray,horizontalLine,verticalLine,rectangle,parallelChannel,fibRetracement,measure,text,magnet. - Editing:
undo,redo,trash,clear,eye,eyeOff,settings,lock,plus,minus,check,close,chevron. - Navigation and scale:
fit,realtime,log,percent,auto,search. - Replay:
replay,play,pause,stepForward,stepBack,stop. - Actions:
camera,download,share,link,api,indicator,sun,moon.
Passing your own markup instead of a name works and is the right answer for a brand icon:
{ id: "brand", icon: '<path d="M4 16 L10 6 L16 16 Z" fill="none" stroke="currentColor" stroke-width="1.6"/>' }
Use a 20-by-20 or 24-by-24 coordinate space and currentColor strokes so the icon matches
the built-ins at every size and in both themes.
Menus and Dialogs
The same primitives the built-in toolbar uses are exported, so a host action can open a menu
or a form that looks exactly like the rest of the chrome rather than like a bolted-on
window.prompt.
import { openMenu, openDialog, openSymbolSearch } from "@ortex-charts/ui";
openMenu(shell.root, anchorButton, (menu) => {
menu.head("Export");
menu.item({ label: "CSV", icon: "download", onSelect: () => save(shell.exportCsv()) });
menu.item({ label: "PNG", icon: "camera", onSelect: () => savePng(shell.chart.chart.takeScreenshot()) });
menu.sep();
menu.item({ label: "Include hidden series", checked: includeHidden, onSelect: () => { includeHidden = !includeHidden; return false; } });
});
Returning false from onSelect keeps the menu open, which is what a toggle wants.
openDialog(shell.root, {
title: "Alert settings",
fields: [
{ key: "price", label: "Price", type: "number", value: 182.5, step: 0.01 },
{ key: "repeat", label: "Repeat", type: "boolean", value: false },
{ key: "channel", label: "Notify by", type: "select", value: "email", options: ["email", "push", "none"] },
],
submitLabel: "Create",
onSubmit: (values) => createAlert(values),
});
openDialog returns a function that closes it. Fields are number, select or boolean;
this is the same generator the indicator settings dialog uses, which is why an indicator you
register yourself gets a proper form without writing one.
Keyboard and Accessibility
What is implemented:
- The chart is focusable. Arrow keys scroll, with Shift for a larger step;
+and-zoom about the center; Home resets the time scale. - Drawings respond to keys on a focused chart: Escape cancels a tool or clears the selection, Delete and Backspace remove the selected drawing, and Ctrl or Cmd with Z and Y undo and redo. See Drawing tools.
- Toolbar buttons carry
aria-pressedso a screen reader announces the active chart type, scale mode and tool. - Menus are
role="menu"witharia-haspopupandaria-expandedon the button that opens them, arrow-key navigation between items,aria-checkedon toggles, Escape to close, and focus returned to the anchor. - Dialogs are
role="dialog"witharia-modal, an accessible name from the title, a focus trap, Enter to submit and Escape to cancel. - Icon-only buttons carry a
titleand an accessible name, which is whyToolbarActionfalls back fromtitletolabel. - Reduced motion is respected in the chrome CSS.
What is not implemented, stated plainly:
- There is no screen-reader description of the data. The canvas is not annotated, and there is no textual summary of the series, the visible range or the values at the crosshair.
- There is no focusable data table as an alternative representation, which is the approach the Highcharts accessibility module takes.
- There is no high-contrast theme shipped, though one is a theme object away.
- Keyboard support does not extend to creating or editing drawings, only to canceling and deleting them.
If accessibility conformance is a procurement requirement for you, these are the gaps to weigh, and the CSV export plus your own table is the honest interim answer.
Mobile
Below 640 pixels the toolbar drops its labels, the resolution strip scrolls horizontally,
and the drawing rail becomes a horizontal bar under the chart. Coarse pointers get 32-pixel
targets. Pinch zoom, drag panning and long-press crosshair are in the engine rather than in
the chrome, so they work on a bare createFinancialChart too.
Full screen uses the Fullscreen API on the shell root, and falls back to a fixed-position layer where the API is missing, which is the case on iOS Safari. Escape leaves either.
Driving the Chart Without the Toolbar
Every method behind the buttons is public, so switching the toolbar off is not a loss of functionality.
const BARE = {
symbol: false, resolution: false, seriesType: false, indicators: false, overlays: false,
scale: false, fit: false, realtime: false, screenshot: false, theme: false, fullscreen: false,
} as const;
const shell = createChartShell(el, { symbol: "AAPL", datafeed, toolbar: BARE, drawings: false });
await shell.setSymbol("MSFT");
await shell.setResolution("60");
shell.setSeriesType("line");
shell.setScaleMode("logarithmic");
shell.addIndicator("rsi", { length: 21 });
await shell.setOverlay("si", true);
shell.setTheme("light");
await shell.toggleFullscreen();
const csv = shell.exportCsv();
const layout = shell.getLayout();
See the shell and layouts for the full surface, including overlays, layouts and per-series placement.