Styling and localization
Themes, locales, and formatters let you align the chart with your brand and language requirements without touching the rendering core.
Theme anatomy
ChartTheme is split into predictable sections:
| Key | Description |
|---|---|
base | Built-in light or dark values used to complete a definition. |
backgroundColor | Canvas background color. |
grid | { color, width } controlling horizontal and vertical guides. |
candle, bar, line, area, hlcArea | Style specific controller types. |
volume | Up/down fill colors for the histogram. |
xAxis, yAxis | Typography, colors, and separators for labels. |
priceAxisAnnotation | Shared price-line and Y-axis annotation styling. |
crosshair | Line color/dash plus tooltip styling and info-line labels. |
randomColors | Palette used when multiple indicators request auto colors. |
Register theme definitions when the chart is created. Missing values are resolved from a built-in base, and runtime changes select a registered key.
import { FinancialChart, type ChartThemeMap } from "@ardinsys/contour";
const themes = {
light: {
area: {
color: "#2d7dff",
fill: [
[0, "rgba(45, 125, 255, 0.35)"],
[1, "rgba(45, 125, 255, 0)"],
],
},
},
dark: {
backgroundColor: "#0f111b",
crosshair: {
color: "#727cf5",
},
},
} satisfies ChartThemeMap;
const chart = new FinancialChart(root, {
stepSize: 15 * 60 * 1000,
theme: "dark",
themes,
});The built-in "light" and "dark" keys are always available. Definitions registered under those keys inherit from the corresponding built-in theme. Other keys inherit from light unless they declare base: "dark":
const themes = {
"brand-night": {
base: "dark",
backgroundColor: "#090b10",
},
} satisfies ChartThemeMap;Definitions are copied when the chart is constructed and remain registered for that chart's lifetime. getOptions().theme exposes the complete resolved theme, including its active key and built-in base. Indicators use the custom key when they define it and otherwise fall back to that base.
The theme's randomColors field is an explicit deterministic palette. Custom indicators can select from it without coupling a utility to the chart:
import { paletteColor } from "@ardinsys/contour/engine";
const color = paletteColor(chart.getOptions().theme.randomColors, seriesIndex);Switch registered themes at runtime by key:
chart.updateOptions({ theme: "light" });Responding to user preference
Detect the active color scheme and switch themes live.
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const chart = new FinancialChart(root, {
timeRange: "auto",
type: "hlc-area",
theme: prefersDark ? "dark" : "light",
themes,
stepSize: 15 * 60 * 1000,
maxZoom: 150,
volume: true,
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
chart.updateOptions({
theme: event.matches ? "dark" : "light",
});
});Localization and formatter overrides
Configure locale, timezone, number/date formatting, and chart UI strings together. Missing UI strings merge into built-in English defaults, so you only override what changes. For the focused i18n path, see i18n.
const chart = new FinancialChart(root, {
timeRange: "auto",
type: "candle",
stepSize: 15 * 60 * 1000,
maxZoom: 150,
volume: true,
locale: "en-US",
timeZone: "UTC",
localeValues: {
"en-US": {
common: {
sources: {
open: "Open price",
high: "High",
low: "Low",
close: "Close",
volume: "Volume",
},
},
indicators: {
actions: {
show: "Show",
hide: "Hide",
settings: "Settings",
remove: "Remove",
},
},
},
},
});
chart.updateOptions({
locale: "hu-HU",
timeZone: "Europe/Budapest",
localeValues: {
"hu-HU": {
common: {
sources: {
open: "Nyitó",
high: "Max",
low: "Min",
close: "Záró",
volume: "Forgalom",
},
},
indicators: {
actions: {
show: "Megjelenítés",
hide: "Elrejtés",
settings: "Beállítás",
remove: "Törlés",
},
},
},
},
});DefaultFormatter accepts locale, timezone, and Intl option overrides:
import { DefaultFormatter } from "@ardinsys/contour";
const formatter = new DefaultFormatter({
locale: "en-US",
timeZone: "UTC",
dateTimeFormatOptions: {
tooltipDate: { dateStyle: "medium", timeStyle: "short" },
second: { hour: "numeric", minute: "2-digit", second: "2-digit" },
subMinute: {
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
},
},
numberFormatOptions: {
price: { maximumFractionDigits: 2 },
},
volumeFormatOptions: {
compactThreshold: 10_000,
},
});A formatter is stateful: its locale and timezone are updated by its owning chart. Construct one formatter per chart instead of sharing an instance across charts with potentially different localization settings.
For completely custom label formatting, extend the Formatter interface (or DefaultFormatter) and pass an instance to the chart:
import { DefaultFormatter, FinancialChart } from "@ardinsys/contour";
// Example: user formatter (not shipped with the library)
class CustomFormatter extends DefaultFormatter {
formatPrice(value: number): string {
return `${value.toFixed(2)} USD`;
}
formatTooltipDate(value: number): string {
return new Date(value).toLocaleString("en-US");
}
}
chart.updateOptions({
locale: "en-US",
timeZone: "America/New_York",
formatter: new CustomFormatter(),
});Remember to import @ardinsys/contour/style.css when using indicators so the UI labels inherit the base styling. To restyle or replace indicator labels and pane dividers, see Design-system adapter.
Wiring an i18n bundle to the chart
If you already have a localization bundle (for example from @ardinsys/intl), forward the same messages to the chart so indicator UI strings stay consistent with the rest of the app. The chart merges your bundle with its built-in English defaults under the default key.
import { createIntl } from "@ardinsys/intl";
import { FinancialChart } from "@ardinsys/contour";
const { locale, setLocale, t } = createIntl("en", {
en: {
messages: {
common: {
sources: {
open: "Open",
high: "High",
low: "Low",
close: "Close",
volume: "Volume",
},
},
indicators: {
actions: {
show: "Show",
hide: "Hide",
settings: "Settings",
remove: "Remove",
},
},
},
},
hu: {
messages: {
common: {
sources: {
open: "Nyitó",
high: "Max",
low: "Min",
close: "Záró",
volume: "Forgalom",
},
},
indicators: {
actions: {
show: "Megjelenítés",
hide: "Elrejtés",
settings: "Beállítás",
remove: "Törlés",
},
},
},
},
});
const chart = new FinancialChart(root, {
timeRange: "auto",
type: "candle",
stepSize: 15 * 60 * 1000,
maxZoom: 150,
volume: true,
locale: "en",
});
// Keep chart labels in sync with the active app locale
function switchLocale(nextLocale: string, timeZone?: string) {
setLocale(nextLocale);
chart.updateOptions({
locale: nextLocale,
timeZone,
localeValues: {
[nextLocale]: {
indicators: {
actions: {
show: t("indicators.actions.show"),
hide: t("indicators.actions.hide"),
settings: t("indicators.actions.settings"),
remove: t("indicators.actions.remove"),
},
},
common: {
sources: {
open: t("common.sources.open"),
high: t("common.sources.high"),
low: t("common.sources.low"),
close: t("common.sources.close"),
volume: t("common.sources.volume"),
},
},
},
},
});
}The chart will pick the matching locale key or fall back to the default block when a translation is missing.
Vue example: rebuild the chart locale bundle when the app locale changes
Use a computed value to regenerate the chart's locale bundle whenever your i18n store locale flips. This avoids relying on whichever locale was active at import time.
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref, watchEffect } from "vue";
import { FinancialChart } from "@ardinsys/contour";
import "@ardinsys/contour/style.css";
import { createIntl } from "@ardinsys/intl";
const { locale, setLocale, t } = createIntl("en", {
en: {
messages: {
common: {
sources: {
open: "Open",
high: "High",
low: "Low",
close: "Close",
volume: "Volume",
},
},
indicators: {
actions: {
show: "Show",
hide: "Hide",
settings: "Settings",
remove: "Remove",
},
},
},
},
hu: {
messages: {
common: {
sources: {
open: "Nyitó",
high: "Max",
low: "Min",
close: "Záró",
volume: "Forgalom",
},
},
indicators: {
actions: {
show: "Megjelenítés",
hide: "Elrejtés",
settings: "Beállítás",
remove: "Törlés",
},
},
},
},
});
const container = ref<HTMLElement | null>(null);
const chart = ref<FinancialChart | null>(null);
const appTimeZone = ref("UTC");
const chartLocaleBundle = computed(() => ({
[locale.value.toUpperCase()]: {
common: {
sources: {
open: t("common.sources.open"),
high: t("common.sources.high"),
low: t("common.sources.low"),
close: t("common.sources.close"),
volume: t("common.sources.volume"),
},
},
indicators: {
actions: {
show: t("indicators.actions.show"),
hide: t("indicators.actions.hide"),
settings: t("indicators.actions.settings"),
remove: t("indicators.actions.remove"),
},
},
},
}));
onMounted(() => {
if (!container.value) return;
const instance = new FinancialChart(container.value, {
timeRange: "auto",
type: "candle",
stepSize: 15 * 60 * 1000,
maxZoom: 150,
volume: true,
locale: locale.value.toUpperCase(),
timeZone: appTimeZone.value,
});
chart.value = instance;
});
watchEffect(() => {
if (!chart.value) return;
const nextLocale = locale.value.toUpperCase();
chart.value.updateOptions({
locale: nextLocale,
timeZone: appTimeZone.value,
localeValues: chartLocaleBundle.value,
});
});
onBeforeUnmount(() => chart.value?.dispose());
</script>Switch the intl store with setLocale("hu") (or any supported code) and the chart picks up the rebuilt bundle automatically.
Custom formatter that delegates to your i18n toolkit
Formatter methods control every label rendered by the chart. You can forward formatting calls to your own toolkit to share currency and date rules across the app.
import { DefaultFormatter } from "@ardinsys/contour";
import { createIntl } from "@ardinsys/intl";
const intl = createIntl("en", {
en: { numberFormats: { money: { style: "currency", currency: "USD" } } },
hu: { numberFormats: { money: { style: "currency", currency: "HUF" } } },
});
// Example: user formatter (not shipped with the library)
class IntlFormatter extends DefaultFormatter {
formatTooltipPrice(price: number, decimals: number): string {
return intl.n("money", Number(price.toFixed(decimals)));
}
formatTooltipDate(timestamp: number): string {
return intl.d(new Date(timestamp));
}
setLocale(locale: string): void {
intl.setLocale(locale);
super.setLocale(locale); // keep axis/volume formatters in sync
}
}
const chart = new FinancialChart(root, {
timeRange: "auto",
type: "candle",
stepSize: 5 * 60 * 1000,
maxZoom: 200,
formatter: new IntlFormatter(),
locale: "en",
timeZone: "UTC",
});Theme keys and indicator labels
Themes carry a key ("light" / "dark" by default). Indicator labels receive this key in IndicatorLabelModel.themeKey, and the default DOM adapter also writes it to data-theme-key. Custom adapters can use that value to choose app-specific label styling.