Skip to content

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.

  1. The theme is a flat token object. Every color the engine paints, when nothing more specific is set, comes from here.
  2. Series options override the theme for one series: color, upColor, topColor and so on.
  3. 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.

Theme tokens, live@ortex-charts/financial
Loading theme tokens, live
Change the palette, grid, fonts and candle colors and watch the chart repaint without being recreated.

Open this example with its source

Throughout this page chart is the core ChartsetTheme, 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.