Skip to content

Color by value

A series normally takes one color from the palette. Point colorProperty at a data property and each bar is colored per category instead, mapped through the series' colorScale ramp — a second measure encoded on the same bars.

ts
// colorProperty reads a color value per data row and maps it through the
// series colorScale ramp — here bar height is revenue while fill encodes
// margin, a second measure on the same bars. The row without a margin value
// falls back to colorScale.missing.
import type { MochartInputConfig } from '@mochart/core';

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Revenue Shaded by Margin' },
  legend: { visible: true },
  categoryAxis: { property: 'product', type: 'string', scale: 'ordinal' },
  valueAxes: [{ id: 'VA0', title: { text: 'Revenue ($k)' } }],
  series: [
    {
      property: 'revenue',
      title: 'Revenue (shaded by margin %)',
      renderer: 'bar',
      // Bar fills and strokes default to 0.8 opacity; full opacity keeps the ramp true.
      shapeStyle: { normal: { strokeOpacity: 1, fillOpacity: 1 } },
      colorProperty: 'margin',
      colorScale: { interpolation: 'lab', min: '#cde2fb', max: '#0d366b' }
    }
  ]
};

export const data = [
  { product: 'Laptops', revenue: 840, margin: 9 },
  { product: 'Phones', revenue: 720, margin: 14 },
  { product: 'Tablets', revenue: 310, margin: 12 },
  { product: 'Monitors', revenue: 260, margin: 21 },
  { product: 'Audio', revenue: 190, margin: 28 },
  { product: 'Accessories', revenue: 130, margin: 34 },
  // No margin reported — colorScale.missing (default gray) colors this bar.
  { product: 'Services', revenue: 110 }
];

How it works

  • Each category's colorProperty value maps linearly from colorScale.min to colorScale.max across the property's extent in that series — the palest bar is always the smallest color value and the darkest the largest, whatever the numbers are. Point colorProperty at the series' own property to shade each bar by its own value instead of a second measure.
  • colorScale.interpolation picks the d3 color space to interpolate in (rgb, hsl, lab, hcl). Setting colorProperty is the switch: once it's set, the scale defaults to hcl through #8f8fff#0000ff — the example above overrides the ramp, everything else is defaults.
  • Per-category color applies to bar series (including floating bars via rangeProperty) — line and area shapes and markers keep their single series color.
  • Bar fills and strokes default to 0.8 opacity, which dilutes the ramp against the background; the example sets shapeStyle.normal's two opacities to 1 so the colors read true.
  • A category with no color value gets colorScale.missing (default #cccccc, the Services bar above); set it to null to fall back to the series' own style colors instead.
  • The series' legend and tooltip color chips become a min→max ramp swatch, so the legend doubles as a compact color key (showColorInLegend / showColorInTooltip turn the chips off).
  • The tooltip shows the series value, not the color value; point tooltipProperty at the color property to surface it, as the heatmap does.
  • colorProperty cannot be combined with a gradient. A pattern replaces the per-category fill; the per-category stroke color still applies.

Diverging around a base

Set colorScale.base.value and the ramp splits in two: one color pair above the threshold, another below — growth in blue, decline in red.

ts
// colorScale.base.value splits the ramp in two: values above the base
// interpolate through the above pair, values below through the below pair.
// Each min/max anchors to its half's data domain — belowMin sits at the most
// negative value, so the saturated color goes there for the classic
// palest-at-the-base diverging look.
import type { MochartInputConfig } from '@mochart/core';

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Revenue Shaded by Growth' },
  legend: { visible: true },
  categoryAxis: { property: 'region', type: 'string', scale: 'ordinal' },
  valueAxes: [{ id: 'VA0', title: { text: 'Revenue ($k)' } }],
  series: [
    {
      property: 'revenue',
      title: 'Revenue (shaded by YoY growth)',
      renderer: 'bar',
      shapeStyle: { normal: { strokeOpacity: 1, fillOpacity: 1 } },
      colorProperty: 'growth',
      colorScale: {
        interpolation: 'hcl',
        base: {
          value: 0,
          aboveMin: '#8f8fff',
          aboveMax: '#0000ff',
          belowMin: '#ff0000',
          belowMax: '#ff8f8f'
        }
      }
    }
  ]
};

export const data = [
  { region: 'North', revenue: 620, growth: 12 },
  { region: 'South', revenue: 540, growth: -4 },
  { region: 'East', revenue: 480, growth: 22 },
  { region: 'West', revenue: 450, growth: -11 },
  { region: 'Central', revenue: 300, growth: 3 },
  { region: 'Export', revenue: 210, growth: 35 }
];
  • With base.value set, min/max must be null (their default in that case — setting them alongside a base is a validation error) and the four base colors take over. Each anchors to its half's data extent: aboveMin sits at the base and aboveMax at the highest value; belowMin sits at the most negative value and belowMax at the base. The defaults give the classic diverging look — palest at the base, saturated at both extremes.
  • Each half fits its own side of the color property's extent, so the deepest red and deepest blue always mark the current extremes.
  • The base splits only the colors. Here the bars measure revenue (all positive) while the color diverges on growth; when the color property is the series' own values, pin the value axis with base so the bars grow out of the same divide the colors split on.

For value-colored grids — rows of full-width bars sharing one global ramp — see the heatmap recipe.

Released under the MIT License.