Customization
Colors, fonts and formatting
Theme tokens, per-series colors, number and date formatting, and localization.
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"],
});
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 Legend
chart.applyOptions({
legend: {
visible: true,
showValues: true, // OHLC or value at the crosshair, not just titles
logo: "https://logos.example.com/aapl.png",
paneControls: true, // the collapse toggle on pane legends
},
});
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.
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.