Skip to content

Colors, theming, and dark mode ​

A chart has two kinds of color. Series colors (bars, lines, markers, slices) come from the config: the color palette, per-series style overrides, or a color ramp. Chrome (the title, axis lines, tick marks and labels, grid lines, the legend, the crosshair) defaults to the CSS keyword currentColor, so it resolves to whatever CSS color the chart's container inherits from your page.

That split is most of the theming story. Put the chart on a page whose text color flips with the theme and every piece of chrome follows along, with no config and no second stylesheet: the live examples on this site restyle when you toggle the site theme (try it). Series colors never follow the page; they stay whatever the palette or your config says.

The page color doesn't have to be a theme. This chart's host element sets style="color: #7c3aed", and every default-styled piece of chrome takes the tint while the series keep their palette colors:

Series color palettes ​

Mochart's default is Paul Tol's Bright qualitative color scheme, in its recommended order:

js
['#4477aa', '#ee6677', '#228833', '#ccbb44', '#66ccee', '#aa3377', '#bbbbbb']

It is designed to distinguish unordered categories for people with common forms of color-vision deficiency. A default series shape takes the color at its series index; a style set to categoryIndex instead takes the color at the datum's category index. Indices wrap, so index 7 reuses index 0. If that would make two things that readers must identify share a color, add another visual encoding or choose a suitable palette with more entries (see Color and visual encoding). A series styled with categoryIndex has no single color, so its legend and tooltip icons show the palette's first colors as a striped swatch.

Set both strokeColors and fillColors when line and filled renderers should share a palette. The arrays are replacements, not extensions of the defaults. This example replaces the normal series palette with Tol's three-color high-contrast scheme:

ts
// Replace the normal series palette with Paul Tol's compact high-contrast
// qualitative scheme. Default focused/defocused styles keep the normal color.
import type { MochartInputConfig } from '@mochart/core';

const colors = ['#004488', '#ddaa33', '#bb5566'];

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Support Requests by Channel' },
  categoryAxis: { property: 'month', type: 'string', scale: 'ordinal' },
  valueAxes: [{ title: { text: 'requests' }, gridLine: { visible: true } }],
  colorPalette: {
    shape: {
      normal: {
        strokeColors: colors,
        fillColors: colors
      }
    }
  },
  seriesDefaults: { renderer: 'line', marker: { shape: 'circle' } },
  series: [
    { property: 'web', title: 'Web' },
    { property: 'email', title: 'Email' },
    { property: 'phone', title: 'Phone' }
  ]
};

export const data = [
  { month: 'Jan', web: 48, email: 31, phone: 18 },
  { month: 'Feb', web: 55, email: 28, phone: 21 },
  { month: 'Mar', web: 51, email: 35, phone: 17 },
  { month: 'Apr', web: 63, email: 38, phone: 23 },
  { month: 'May', web: 68, email: 34, phone: 20 },
  { month: 'Jun', web: 72, email: 41, phone: 25 }
];

The default focused and defocused series styles use same, so the normal color remains in effect during interaction and the example only needs to override normal. If a custom style uses seriesIndex or categoryIndex directly in either focus state, configure that state's palette arrays too.

The four top-level palette groups serve different elements:

  • shape colors the main line, area, bar, or pie shape.
  • marker, label, and errorBar provide independent colors when those elements' styles explicitly use seriesIndex or categoryIndex.
  • By default, markers and error bars use the owning series color, while labels use the page's currentColor, so changing shape is usually enough.

See the colorPalette reference for the complete shape and the config model for merge behavior. A qualitative palette identifies separate categories; to map a numeric magnitude through a continuous or diverging ramp, use colorProperty and colorScale instead.

Chrome and currentColor ​

Chrome style fields default to 'currentColor', which is written to the rendered SVG as-is, and the browser resolves it against the inherited color, so mochart never computes a theme itself. The defaults that resolve this way:

  • the title text, and the axis title texts
  • axis lines, base lines, tick marks, and tick label text
  • grid lines, threshold lines and titles, the axis focus range, and focus tick marks
  • the crosshair lines
  • legend item text, and the series-icon borders in the legend and tooltip
  • series value labels, and the pie center labels
  • the clip indicator band and its label

Each comes with a tuned default opacity so a single value reads correctly over both light and dark backgrounds: grid lines at strokeOpacity 0.13, axis lines and tick marks at 0.65, the crosshair at 0.3, text at or near 1. Chrome contrast is therefore adjusted through opacities, not by picking new colors per theme.

What does not follow the page ​

  • Series colors. The palette and the color-ramp fields produce concrete colors by design, because chart data should look the same on every page. Restyle them per theme by passing a different config (for example a different colorPalette) when your theme changes.
  • Colors you set yourself. Any literal color in your config is used exactly as written, in every theme.
  • The tooltip surface: see below.

Dark mode ​

For chrome there is nothing to configure: when your page (or the chart's container) sets a light text color on a dark background, the chart follows.

The chart's own surfaces are transparent, so whatever sits behind the chart on your page shows through and follows the theme with it. No config is needed. To give a surface its own background, every part that has one takes a backgroundStyle: chart, plot, legend, title and the axes. Those are concrete colors like any other, so a config that sets them needs a per-theme variant.

The tooltip is the one exception. It is an HTML overlay, and its background and border form a surface that must sit at the opposite end of the contrast pair from the text on top of it (something no inherited text color can express), so its defaults are a translucent white background with a dark border. Its text does inherit the page color, which is right for a light surface in both themes; if you keep the light surface on a dark page, scope a color override to the tooltip instead. To flip the surface itself, override it with CSS scoped to your dark theme (the colors are inline styles, so the overrides need !important):

css
html.dark .mochart-tooltip-container .mochart-tooltip {
  background: rgba(32, 33, 39, 0.94) !important;
  border-color: rgba(140, 145, 160, 0.55) !important;
}

This is exactly what this docs site does for its live examples. Alternatively, keep it in config: pass a config with a dark tooltip.backgroundStyle when your theme changes.

Using currentColor in your config ​

Every style color field accepts 'currentColor' (the config model covers the style shape), so any element you restyle can opt back into following the page, for example a series drawn in the page's text color:

js
series: [{
  property: 'total',
  shapeStyle: { normal: { fillColor: 'currentColor' } }
}]

The places it is rejected are the series color-scale bounds (colorScale.min, colorScale.max, colorScale.missing and colorScale.base.*), colorPalette entries, and gradient stop colors: those are interpolated by d3 scales, which need concrete colors, so validation turns a keyword away rather than letting it produce NaN colors.

Typography ​

Text takes its font from the host page by default: the chart writes no font of its own, so every label renders in whatever font-family, font-size, font-weight and font-style the container inherits. To set typography from the config instead, every text part has a font with four members, each null by default:

  • family, written as font-family
  • size, a number of pixels or any CSS font-size string such as '0.85em', '120%' or 'large', written as font-size
  • weight, a hundred from 100 to 900 or one of normal, bold, lighter and bolder, written as font-weight
  • style, one of normal, italic and oblique, written as font-style

chart.font is the chart-wide default. The title and its prefix and suffix, legend items, axis tick labels, axis titles, threshold titles, series labels, the pie center label and total, the clip indicator label and the tooltip each have a font of their own, a sibling of their textStyle. A part's member is used when it is set, otherwise the chart.font member, so a config can size everything once and single out one part.

The chart measures its text in whatever font is in effect at the time, and measures again when the config object changes, when the chart is resized, and when the document reports a web font finishing loading (document.fonts), so a page font that arrives after the chart mounts, the ordinary case with font-display: swap, does not leave the axes sized and the labels truncated for the fallback font. A data change or refresh() alone measures nothing again.

Setting fonts from the config looks like this:

js
chart: { font: { family: 'Georgia, serif', size: 12 } },
title: { text: 'Support Tickets by Month', font: { size: 18, weight: 'bold' } },
valueAxes: [{ title: { text: 'tickets', font: { style: 'italic' } } }],
legend: { item: { font: { size: '0.85em' } } }

This chart renders in Georgia while the rest of this page keeps the site font, with a larger bold title, an italic value axis title and legend items at 0.85em. A relative size is written on the element as given, so it resolves the way CSS does, against the font size the element inherits from the page, not against chart.font. The same holds for the minor tick labels of an axis: minorTickLabel.font.size of '0.85em' resolves against the page font, not against tickLabel.font.size, so with tickLabel.font.size set to 16 on a page with a 12px font the minor labels are 10.2px, not 13.6px:

A member that is null in both places is left to CSS, so the page keeps deciding it. A member that is set is written as an inline style on the text element itself (on the tooltip's box for the HTML tooltip, and on the clip indicator's group, which its label inherits), which is why a configured value wins over any host page CSS rule that matches the element, including a reset. The chart measures its text after writing the font, so a larger size reserves more room for the labels it applies to.

The font is not part of the style states: normal, focused and defocused change colors and opacities, never the font, so the layout does not move when the focus does.

Exports ​

Exported images inline the chart's resolved colors, so a chart exported from a dark page has light chrome, so pass the export a background color that matches the page, or export transparent. See Exporting images.

Released under the MIT License.