UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

230 lines (183 loc) 7.52 kB
--- title: Legends and Color description: Map categorical or quantitative values to color, render responsive legends, and preserve meaning across themes. --- Color has one semantic path. A mark's `color` channel contributes values to the chart-level `color` scale and legend. `z` partitions series or interaction groups and supplies the color value only when `color` is omitted. `fill` and `stroke` are final paint overrides; using either bypasses scale mapping for that paint. ## Automatic categorical color When marks emit categorical color values and no color scale is supplied, TanStack Charts uses the chart theme palette. This is the convenient default for a small, stable set of categories. For persistent product semantics, supply an explicit configured ordinal scale: ```ts import { colorLegend, defineChart, lineY } from '@tanstack/charts' import { scaleOrdinal } from '@tanstack/charts/scales/ordinal' const series = ['core', 'react', 'octane'] as const const color = scaleOrdinal<string, string>() .domain(series) .range(['#2563eb', '#f97316', '#10b981']) const definition = defineChart({ marks: [ lineY(rows, { x: 'date', y: 'value', z: 'series', }), ], x, y, color: { scale: color, legend: colorLegend({ label: 'Package' }), }, }) ``` The application owns the domain order and paint assignment. This prevents a category from changing color when data is filtered or reordered. ## Quantitative color Supply a D3 color-scale factory and put the numeric field on the mark's `color` channel: ```ts import { scaleSequential } from 'd3-scale' import { interpolateBlues } from 'd3-scale-chromatic' import { colorLegend } from '@tanstack/charts' const color = { scale: () => scaleSequential(interpolateBlues), legend: colorLegend({ label: 'Requests per minute', format: (value) => value.toLocaleString(), }), } ``` The factory keeps the interpolator and lets the chart infer the numeric domain from color-channel values. Continuous and quantize factories infer a finite extent. Quantile factories receive the complete observed numeric population, including duplicates. Threshold factories require an explicit domain because their cuts are policy, not an extent: ```ts import { scaleThreshold } from 'd3-scale' const colors = ['#eff6ff', '#bfdbfe', '#60a5fa', '#1d4ed8'] const color = { scale: scaleThreshold<number, string>, domain: [5, 12, 24], range: colors, legend: colorLegend({ label: 'Incidents' }), } ``` `d3-scale` and `d3-scale-chromatic`, with their matching type packages, are optional direct application dependencies for these quantitative mappings. They are never pulled into charts that do not import them. The [scale guide](../concepts/scales-and-d3.md) owns the install and API-reference links. ## Automatic color legend `colorLegend` reads the resolved scale: - categorical scales render labeled swatches; - continuous scales render a sampled ramp; - quantize, quantile, and threshold scales render exact bins and boundaries. Options: - `label`: optional legend title; - `itemWidth`: minimum categorical item width; - `width`: preferred quantitative legend width; - `format`: numeric boundary formatter; - `placement`: `top` by default or `bottom`. The legend reserves its own layout height. It is visual guidance and is hidden from the SVG accessibility tree; essential category meaning should also be available through direct labels, surrounding HTML, or a table. ```ts group=automatic-color-legend env=charts file=/src/chart.ts entry import { colorLegend, defineChart, lineY } from '@tanstack/charts' import { scaleLinear } from '@tanstack/charts/scales/linear' import { scalePoint } from '@tanstack/charts/scales/point' import { rows } from './data' export default defineChart({ marks: [ lineY(rows, { x: 'week', y: 'downloads', z: 'package', strokeWidth: 2.5, }), ], x: { scale: () => scalePoint<string>().padding(0.2), axis: { label: 'Week' }, }, y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 }, label: 'Downloads' }, }, color: { legend: colorLegend({ label: 'Package' }) }, }) ``` ```ts group=automatic-color-legend file=/src/data.ts collapsed export const rows = [ { week: 'May 4', package: 'core', downloads: 820 }, { week: 'May 11', package: 'core', downloads: 960 }, { week: 'May 18', package: 'core', downloads: 1_140 }, { week: 'May 25', package: 'core', downloads: 1_280 }, { week: 'May 4', package: 'react', downloads: 610 }, { week: 'May 11', package: 'react', downloads: 730 }, { week: 'May 18', package: 'react', downloads: 810 }, { week: 'May 25', package: 'react', downloads: 940 }, ] ``` [Open the grouped-bar catalog case](https://tanstack.com/charts/catalog/bar-grouped/). ## Explicit gradient legend `colorGradientLegend` requires a numeric color-scale domain. Options: - `label`: optional title; - `steps`: rendered color samples, with a minimum of two; - `width`: preferred width capped by the chart; - `format`: formatter for the domain endpoints; - `placement`: `top` by default or `bottom`. Use `colorGradientLegend` only when a discrete scale should intentionally be shown as a sampled ramp. It requires a numeric domain and does not invent units or semantic thresholds. ## Controlled interactive legend Use `interactiveColorLegend` when a categorical color value is also the series identity: ```ts import { controlledSignal } from '@tanstack/charts/interaction/signal' import { interactiveColorLegend } from '@tanstack/charts/legend' const color = { domain: ['core', 'react', 'octane'], range: ['#2563eb', '#f97316', '#10b981'], legend: interactiveColorLegend({ visible: controlledSignal(visibleSeries, setVisibleSeries), placement: 'bottom', ariaLabel: 'Package visibility', }), } ``` The application stores `visibleSeries`; the legend proposes the next complete array in color-domain order. Filtering happens after scale resolution, so a hidden series keeps its color and does not change inferred position domains. The browser host renders native pressed-state buttons and preserves their focus across controlled updates. Static SVG rendering keeps a visual, noninteractive fallback. This behavior applies to marks whose `color` channel defines series identity. Keep a separate `z` channel when grouping and color mean different things. ## Direct labels versus legends Prefer direct labels when: - there are only a few lines or regions; - labels fit near endpoints; - the reader would otherwise move repeatedly between marks and a legend. Prefer a legend when: - the same category appears in many places; - marks are too dense for direct labels; - a shared mapping spans several views. It is valid to use both when the legend establishes the complete domain and direct labels help with the primary comparison. ## Theme and accessibility rules - Keep semantic category colors stable across updates. - Test every supplied color against light and dark backgrounds. - Use a sequential scale for ordered magnitude and a diverging scale only when a meaningful center exists. - Do not imply order with an unordered rainbow palette. - Do not use color as the only signal for selection, status, or error. - Use `unknown` behavior on the ordinal scale when unexpected categories must not silently join the domain. See [Themes and Styling](./themes-and-styling.md) and [Accessibility](./accessibility.md) for the surrounding policies.