Customization
Colors, fonts and formatting
Theme tokens, per-series colors, number and date formatting, and localization.
Read this page as Markdown, or take the whole documentation in one file.
Three Levels of Color
Colors are resolved in one direction, from general to specific, and knowing the order saves a lot of guessing.
- The theme is a flat token object. Every color the engine paints, when nothing more specific is set, comes from here.
- Series options override the theme for one series:
color,upColor,topColorand so on. - Per-value coloring overrides both, where the type supports it: a histogram in
colorMode: "sign", candles in up and down colors, a baseline series above and below its base.
The rule to internalize is that a series left with unset colors follows the theme. That
is what makes chart.setTheme("light") repaint an entire chart, indicators included,
without touching a single series. Setting explicit colors opts that series out.
@ortex-charts/financialThroughout this page chart is the core Chart — setTheme, applyOptions, priceScale
and theme live there. If you started from createFinancialChart or createChartShell,
reach it as fc.chart or shell.chart.chart.
Theme Tokens
darkTheme and lightTheme are exported objects, and a theme is just such an object. Every
token is required, so start from one of the built-ins rather than from an empty object.
| Token | Type | What it colors |
|---|---|---|
name |
string |
An identifier for your own bookkeeping. |
background |
color | The plot area of every pane. |
axisBackground |
color | The price and time axis strips. |
axisBorder |
color | The line between the axes and the plot. |
grid |
color | Grid lines. |
paneSeparator |
color | The divider between panes. |
paneSeparatorHover |
color | The same divider while it is being hovered or dragged. |
text |
color | Axis labels and general text. |
textMuted |
color | Secondary text. |
fontFamily |
CSS font stack | Every string the canvas draws. |
fontSize |
number | Base size in pixels; labels derive from it. |
crosshair |
color | The crosshair lines. |
crosshairLabelBackground |
color | The axis tags at the crosshair. |
crosshairLabelText |
color | Text inside those tags. |
up |
color | Rising candle bodies and bars. |
down |
color | Falling candle bodies and bars. |
upWick |
color | Rising candle wicks. |
downWick |
color | Falling candle wicks. |
volumeUp |
color | Volume columns on a rising bar. |
volumeDown |
color | Volume columns on a falling bar. |
line |
color | A line or area series with no color of its own. |
areaTop |
color | Top of the area gradient. |
areaBottom |
color | Bottom of the area gradient. |
palette |
string[] |
Handed to successive overlays and indicator outputs in order. |
watermark |
color | The watermark text. |
lastValueText |
color | Text inside the last-value axis label. |
legendBackground |
color | Behind the legend; transparent in both built-in themes. |
legendText |
color | Legend text. |
selection |
color | Selected drawings and selection rectangles. |
tooltipBackground |
color | Hover tooltips. |
tooltipText |
color | Tooltip text. |
tooltipBorder |
color | Tooltip border. |
Colors accept #rgb, #rrggbb, #rrggbbaa, rgb() and rgba().
The Palette
palette is the one token that is not a single color, and it does more work than it looks.
Every series added without an explicit color takes the next entry, cycling when it runs
out. So does every indicator output whose definition asks for theme.palette[n]. Setting a
brand palette therefore recolors indicators and host overlays at once:
import { darkTheme } from "@ortex-charts/financial";
chart.setTheme({
...darkTheme,
name: "brand-dark",
palette: ["#FF7A00", "#7C5CE6", "#00B8D9", "#36B37E", "#FFAB00", "#FF5630"],
});
Colour-Blind Safe Themes
About one man in twelve cannot separate the red and green a candle chart has used since the 1980s. Two themes ship for that, and the toolbar has a switch:
import { createFinancialChart } from "@ortex-charts/financial";
createFinancialChart(el, { theme: "dark-colorblind", data: bars });
| Theme | Up | Down |
|---|---|---|
dark-colorblind |
#56B4E9 |
#D55E00 |
light-colorblind |
#0072B2 |
#E69F00 |
Both pairs come from the Okabe-Ito palette, which is chosen to stay apart under deuteranopia,
protanopia and tritanopia, and both differ in luminance as well as hue, so direction
survives a greyscale print, a monochrome display and total colour blindness. Overlay series
use the rest of that palette, exported as OKABE_ITO.
Only the colours that carry meaning change: direction, volume, the overlay palette and the single-series line. The background, grid and text keep the theme they came from, so a colour-blind chart still looks like the rest of your application.
Your own theme can have the same treatment:
import { colorBlindSafe } from "@ortex-charts/core";
const acmeSafe = colorBlindSafe(acmeTheme); // name becomes "acme-colorblind"
In the shell, the colorBlind toolbar item is a switch beside the light and
dark one, and the two are independent: turning the palette on keeps you in dark, and switching
to light keeps the palette.
A Complete Custom Theme
import { darkTheme, type Theme } from "@ortex-charts/financial";
export const brandDark: Theme = {
...darkTheme,
name: "brand-dark",
background: "#0B0E14",
axisBackground: "#0B0E14",
axisBorder: "#1E2531",
grid: "#141A23",
text: "#8A96A6",
textMuted: "#5A6675",
fontFamily: '"Inter", -apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 11,
up: "#00C08B",
down: "#FF4D5E",
upWick: "#00C08B",
downWick: "#FF4D5E",
volumeUp: "rgba(0, 192, 139, 0.4)",
volumeDown: "rgba(255, 77, 94, 0.4)",
line: "#4C8DFF",
areaTop: "rgba(76, 141, 255, 0.28)",
areaBottom: "rgba(76, 141, 255, 0.02)",
palette: ["#FFAB00", "#A78BFA", "#F472B6", "#34D399", "#FB923C", "#60A5FA"],
};
createFinancialChart(el, { theme: brandDark, data: bars });
Switching at Runtime
chart.setTheme("light");
chart.setTheme(brandDark);
chart.theme(); // the resolved Theme object
A theme change is a repaint, never a rebuild. Series, indicators, drawings, primitives, the visible range and the crosshair position all survive it. Indicator colors are functions of the theme, so they follow too.
Color Helpers
import { parseColor, withAlpha, contrastText } from "@ortex-charts/financial";
parseColor("#22C08A"); // [34, 192, 138, 1]
withAlpha(theme.up, 0.15); // "rgba(34, 192, 138, 0.15)"
contrastText("#22C08A"); // "#111418" or "#FFFFFF", whichever reads
contrastText is what the engine uses for text on colored badges, and it is the right tool
for a host drawing its own labels on a theme color.
Chart Chrome
Everything below is a chart option, applied at creation or with applyOptions, which is
deeply partial so you write only the keys you are changing.
Grid, Crosshair and Watermark
chart.applyOptions({
grid: { vertical: true, horizontal: true },
crosshair: {
mode: "magnet", // "magnet" | "free" | "hidden"
vertLine: { visible: true, width: 1, style: "dashed", labelVisible: true, color: "#6F7D91" },
horzLine: { visible: true, width: 1, style: "dashed", labelVisible: true },
},
watermark: { text: "AAPL", visible: true, fontSize: 44 },
});
magnet snaps the crosshair to the nearest bar value; free follows the pointer exactly;
hidden removes it while leaving hover events intact.
The watermark draws your own artwork instead of the text when you give it one, centred behind the series in the first pane:
chart.applyOptions({
watermark: {
visible: true,
logo: "https://cdn.example.com/mark.svg", // or "ortex" for the ORTEX wordmark
logoScale: 0.25, // height as a fraction of the pane, default 0.25
logoHeight: 120, // exact height in pixels, wins over logoScale
opacity: 0.08, // default 0.08
},
});
Keep the opacity low enough that a candle wick still reads over it; the width is capped at 80% of the plot so a wide mark cannot run under the price scale. This is your mark on your chart and is unrelated to the ORTEX mark, which a license removes.
The Legend
chart.applyOptions({
legend: {
visible: true,
showValues: true, // OHLC or value at the crosshair, not just titles
logo: "https://logos.example.com/aapl.png",
symbol: "NASDAQ:AAPL", // printed before the first series title
paneControls: true, // the collapse toggle on pane legends
collapsed: false, // show only the first row of every pane legend
collapsible: true, // the reader's own toggle on that first row
},
});
logo is drawn before the first series title and is set automatically by the
datafeed binding from SymbolInfo.logo. A missing image falls back to the
color dot silently. symbol sits between the logo and the title, which is where an
exchange and ticker belong; the shell fills it in from the current symbol
unless you turn that off.
A chart carrying a dozen overlays has a legend to match, and a reader who knows what is on
it wants the chart back. collapsed shows the first row only, and collapsible puts a
toggle on that row so the reader decides. Both the toggle and the option go through the
same three methods, so a host can drive it from its own button and store the answer:
chart.setLegendCollapsed(true);
chart.isLegendCollapsed(); // → true
const off = chart.onLegendCollapse((e) => save(e.collapsed)); // fires on either route
The shell has the button already: add "legend" to its toolbar items, and the state
travels in the layout as legendCollapsed.
Price Scales
Each scale is configured independently, per pane.
chart.priceScale("right").applyOptions({
mode: "logarithmic", // "normal" | "logarithmic" | "percentage" | "indexedTo100"
autoScale: true,
visible: true,
scaleMargins: { top: 0.1, bottom: 0.1 },
borderVisible: true,
ticksVisible: true,
minimumWidth: 64,
invert: false,
});
chart.priceScale("right", volumePane.id).applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } });
scaleMargins are fractions of the pane height left empty above and below the data, which
is how a volume pane keeps its columns in the lower fifth. invert flips the axis, which is
occasionally right for a yield or a rate.
The Time Scale
Covered on Time, sessions and resolutions; the options are barSpacing,
minBarSpacing, maxBarSpacing, rightOffsetPx, fixLeftEdge, fixRightEdge,
followRealtime, borderVisible and visible.
Interaction and Layout
chart.applyOptions({
handleScroll: true,
handleScale: true,
kineticScroll: true, // momentum after a fast drag or flick
autoSize: true, // follow the container with a ResizeObserver
width: 900, // only when autoSize is false
height: 480,
timeAxisHeight: 26,
priceLabelGap: 36, // minimum pixels between price labels
timeLabelGap: 72, // minimum pixels between time labels
});
Raising priceLabelGap and timeLabelGap thins the labels, which is the fix for a small
chart whose axes look crowded. Lowering them packs more in.
Number Formatting
Formatting is per series, through priceFormat.
series.applyOptions({ priceFormat: { type: "price", precision: 2, minMove: 0.01 } });
series.applyOptions({ priceFormat: { type: "volume", precision: 0, minMove: 1 } });
series.applyOptions({ priceFormat: { type: "percent", precision: 2, minMove: 0.01 } });
series.applyOptions({
priceFormat: {
type: "custom",
precision: 2,
minMove: 0.01,
formatter: (v) => `${v >= 0 ? "+" : ""}${v.toFixed(2)} bp`,
},
});
type |
Renders | Used for |
|---|---|---|
price |
1,234.56 with precision decimals |
Prices and most values. |
volume |
1.23M, 4.56B |
Volume and share counts. |
percent |
12.34% |
Ratios already expressed in percent. |
custom |
Whatever formatter returns |
Anything else: basis points, currencies, ratings. |
minMove is the smallest increment the instrument trades in. It matters because the axis
adds decimals when the visible range is narrow, and minMove is the floor it will not go
below. A minMove of 0.01 on a stock and 0.0001 on a currency pair is the usual pairing
with precision 2 and 4.
formatter wins over everything else, and it is used for the axis labels, the crosshair
tags, the last-value label and the legend, so one function covers them all.
The formatting helpers are exported for host-drawn labels:
import { formatPrice, formatPercent, formatCompact, formatVolume, precisionFromMinMove } from "@ortex-charts/math";
import { formatValue } from "@ortex-charts/financial";
formatPrice(1234.5678, { precision: 2 }); // "1,234.57"
formatPercent(0.1234, { precision: 2 }); // percent formatting
formatCompact(1234567, { precision: 2 }); // "1.23M"
formatVolume(1234567); // volume shorthand
precisionFromMinMove(0.0001); // 4
formatValue(1234.5, series.options.priceFormat); // exactly what the chart would print
Dates, Locales and Language
Dates and numbers go through Intl, so the chart's locale option controls month names,
date ordering and grouping separators.
createFinancialChart(el, { locale: "de-DE", timeZone: "Europe/Berlin" });
There is a limit worth stating plainly: the interface strings are English only. The
toolbar labels, menu entries, dialog titles and tooltips in @ortex-charts/ui are not
translated and there is no string table to supply. Dates and numbers localize; words do not.
A host that needs another language today replaces the chrome with its own controls and
drives the chart through the shell methods, which are all public.
Per-Series Overrides at a Glance
| Series type | The options that override the theme |
|---|---|
candlestick |
upColor, downColor, wickUpColor, wickDownColor, borderVisible, hollowUp, bodyWidthRatio |
bar |
upColor, downColor, thin, openVisible |
line |
color, lineWidth, lineStyle, curve, pointMarkers, pointRadius |
area |
color, topColor, bottomColor, baseValue, curve |
baseline |
baseValue, topLineColor, bottomLineColor, topFillColor, bottomFillColor |
histogram |
color, upColor, downColor, colorMode, base, widthRatio |
See Series types for what each one does.
Styling the Chrome
The toolbar and drawing rail in @ortex-charts/ui are ordinary DOM. They are styled with
CSS custom properties derived from the chart theme, injected once per document, so a theme
switch restyles them along with the canvas.
import { applyThemeVars, UI_CSS, ensureStyles } from "@ortex-charts/ui";
ensureStyles(document) injects the stylesheet if it is not already there, and
applyThemeVars(element, theme) writes the custom properties onto an element. The shell
does both for you; the exports exist so a host can style its own controls to match, or
override the properties in its own stylesheet.
Class names are prefixed oc-ui-, which makes them stable targets for a host stylesheet
without fighting a CSS-in-JS runtime.
A Practical Recipe: Two Themes and a Toggle
import { createFinancialChart, darkTheme, lightTheme, type Theme } from "@ortex-charts/financial";
const brand = (base: Theme, name: string): Theme => ({
...base,
name,
fontFamily: '"Inter", system-ui, sans-serif',
palette: ["#FF7A00", "#7C5CE6", "#00B8D9", "#36B37E", "#FFAB00", "#FF5630"],
});
const themes = { dark: brand(darkTheme, "brand-dark"), light: brand(lightTheme, "brand-light") };
const chart = createFinancialChart(el, { theme: themes.dark, data: bars });
matchMedia("(prefers-color-scheme: light)").addEventListener("change", (e) => {
chart.chart.setTheme(e.matches ? themes.light : themes.dark);
});
Deriving both themes from one function keeps the pair consistent, and the switch is a repaint, so it can be wired to a media query without any care about timing.