Skip to content
KONIGI Patterns

Dashboards / Color / Sequential and diverging scales

‹ 6 of 6

Sequential and diverging scales

People need color scales that clearly show value differences and meaningful reference points.

Updated September 10, 2026

Problem

When a chart represents values with color, a poorly chosen scale can hide differences or create apparent patterns that are not in the data.

Solution

Use a sequential scale for values running from low to high. Make the lightness change steadily so the order is easy to read.

Use a diverging scale when there is a meaningful center, such as zero change or a target. Place the neutral color at that actual reference value. For values that wrap around, such as angles, use a scale whose ends meet.

Label the range and any limits applied to extreme values. Avoid rainbow scales that introduce uneven visual steps. Test the colors in supported themes and provide exact values another way.

Use when

Color represents a quantity or ordered value rather than a category name.

Don’t use when

The categories are unordered, where a ramp implies a ranking that does not exist. And avoid encoding anything precise in colour at all: people read position and length far more accurately, so a ramp is for pattern, not for measurement.

Trade-offs

Color supports approximate comparison, not precise measurement. Limiting the range can reveal common values while hiding how extreme the outliers are. Separate charts need matching scales when they are meant to be compared.

Checklist

  • Is this quantity sequential, diverging or cyclical, and does the ramp match?
  • If diverging, does the midpoint sit at a real zero rather than at the mean?
  • Does the ramp vary monotonically in lightness?
  • Is it perceptually uniform, or does it invent boundaries?
  • Is the scale clamped, and does one outlier otherwise consume it?
  • Is the legend present with real values, not just a gradient bar?
  • Does the ramp survive greyscale and the common colour vision deficiencies?
  • Does it have a variant tuned for the dark theme?
  • Do neighbouring panels share a scale where they are meant to be compared?
  • Is there a number available for anyone who needs precision?

Compare

ColorBrewer remains the reference set, dividing schemes into sequential, diverging and qualitative and flagging which are colourblind-safe, print-friendly and photocopy-safe, which is the taxonomy every later tool borrowed. Viridis and its family were designed explicitly for perceptual uniformity and greyscale survival, and have become the default in scientific plotting for that reason. Grafana offers scheme and opacity modes plus explicit start and end clamps on its heatmap, which is more control than most dashboards use and exactly the control that matters. Older SCADA and BI tools still default to rainbow, which is the clearest available demonstration of why this entry exists.

Heatmap relies on a readable value scale. Geo map adds the challenge of unequal region sizes. Calendar heatmap needs a useful range for daily values.

Categorical series palette handles unordered categories. Cohort grid uses color to compare values across groups.

Sequential and diverging scales anatomy Three ramp shapes stacked: sequential, diverging and cyclical. Below, seven values that are all positive, coloured first with the ramp's midpoint at zero and then with it at the data's mean, where the first three read as if they were negative. Three shapes worth telling apart 1 SEQUENTIAL Low to high, no meaningful middle. Vary monotonically in lightness, not only in hue. 2 DIVERGING Two sequential ramps meeting at a neutral middle. The middle has to sit at the real zero. 3 CYCLICAL Hour of day, angle, phase. The ends have to meet, or the ramp puts a hard edge at midnight. Where the midpoint sits Seven values. Every one of them is positive. Midpoint at zero Midpoint at the mean +2 +4 +6 +8 +10 +12 +14 The second row colours three positive numbers as if they were negative.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Two ramps with different jobs. Sequential runs one direction and must stay monotonic in lightness; diverging has a real zero in the middle and arms that match. shadcn has neither, and the difference is not decorative.

Tokens
--card--muted-foreground--scale-seq-1--scale-seq-2--scale-seq-3--scale-seq-4--scale-seq-5--scale-div-neg-2--scale-div-neg-1--scale-div-mid--scale-div-pos-1--scale-div-pos-2
Midpoint at zero · -14 to +14

scale.tsPicks the ramp from the quantity's shape, centres diverging at zero, and clamps the domain.

/**
 * The ramp follows the shape of the quantity, not the other way round.
 *
 * Sequential for a magnitude with one direction—requests, bytes, latency.
 * Diverging for a quantity with a meaningful zero—variance to forecast,
 * change against a baseline. Using a diverging ramp on a sequential quantity
 * invents a midpoint the data does not have, and readers will look for meaning
 * in it because midpoints usually mean something.
 */
export type ScaleKind = "sequential" | "diverging" | "cyclical";

// The tokens are HSL triplets, so they are only a colour inside hsl().
const SEQUENTIAL = [1, 2, 3, 4, 5].map((i) => `hsl(var(--scale-seq-${i}))`);
const DIVERGING = ["neg-2", "neg-1", "mid", "pos-1", "pos-2"].map((s) => `hsl(var(--scale-div-${s}))`);
/** Up the sequential ramp and back down it, so the ends meet. Hour of day,
 *  angle, phase: a sequential ramp here puts a hard edge at midnight. */
const CYCLICAL = [...SEQUENTIAL, ...SEQUENTIAL.slice(0, -1).reverse()];

export const RAMP: Record<ScaleKind, string[]> = { sequential: SEQUENTIAL, diverging: DIVERGING, cyclical: CYCLICAL };

/**
 * Clamp to a percentile rather than to the extremes.
 *
 * One outlier otherwise consumes the whole ramp and every other value collapses
 * into the first step. The legend has to say the domain was clamped, or the
 * reader believes the top of the scale is the maximum.
 */
export function clampDomain(values: number[], p = 0.98): [number, number] {
  const sorted = [...values].sort((a, b) => a - b);
  const at = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * (sorted.length - 1)))];
  return [at(1 - p), at(p)];
}

export function rampColor(value: number, kind: ScaleKind, domain: [number, number]): string {
  const steps = RAMP[kind];
  if (kind === "diverging") {
    // The midpoint is a real zero, never the mean of the data.
    const span = Math.max(Math.abs(domain[0]), Math.abs(domain[1])) || 1;
    const t = Math.max(-1, Math.min(1, value / span));
    return steps[Math.round((t + 1) / 2 * (steps.length - 1))];
  }
  const t = (value - domain[0]) / (domain[1] - domain[0] || 1);
  return steps[Math.round(Math.max(0, Math.min(1, t)) * (steps.length - 1))];
}

ColorScale.tsxThe legend: a swatch per value, and a caption that says where the midpoint sits and whether the domain was clamped.

import { clampDomain, rampColor, type ScaleKind } from "./scale";

/**
 * A legend with real values on it, not a bare gradient. Each value gets its
 * swatch from the ramp the quantity's shape calls for, and the caption says
 * the two things a reader cannot see: where the midpoint sits, and whether
 * the domain was clamped. Colour answers "roughly"; the numbers are here for
 * anyone who needs "exactly".
 */
export function ColorScale({ values, kind, format = String, clampAt = 0.98, onPick }: {
  values: number[];
  /** Sequential, diverging or cyclical. Required, because the default is how a diverging ramp ends up on a magnitude. */
  kind: ScaleKind;
  format?: (v: number) => string;
  /** Percentile the domain is clamped to. 1 means the extremes, and the caption says so either way. */
  clampAt?: number;
  onPick?: (value: number) => void;
}) {
  const domain = clampDomain(values, clampAt);
  const lo = Math.min(...values), hi = Math.max(...values);
  const clamped = domain[0] > lo || domain[1] < hi;

  const caption =
    kind === "diverging" ? `Midpoint at zero · ${format(-Math.max(Math.abs(domain[0]), Math.abs(domain[1])))} to ${format(Math.max(Math.abs(domain[0]), Math.abs(domain[1])))}`
    : kind === "cyclical" ? `Wraps · ${format(domain[0])} to ${format(domain[1])} and back`
    : `Low to high · ${format(domain[0])} to ${format(domain[1])}`;

  return (
    <figure className="m-0">
      <figcaption className="mb-1.5 text-[11px] uppercase tracking-wide text-muted-foreground">
        {caption}
        {clamped && <span className="normal-case"> · clamped at p{Math.round(clampAt * 100)}, {format(hi)} shows as {format(domain[1])}</span>}
      </figcaption>
      <div className="flex h-8" role="list">
        {values.map((v, i) => (
          <button key={i} type="button" role="listitem" className="flex-1" style={{ background: rampColor(v, kind, domain) }}
            title={format(v)} aria-label={format(v)} onClick={() => onPick?.(v)} />
        ))}
      </div>
      <div className="mt-1 flex text-center text-[11px] tabular-nums text-muted-foreground" aria-hidden="true">
        {values.map((v, i) => <span key={i} className="flex-1">{format(v)}</span>)}
      </div>
    </figure>
  );
}

demo.tsxHow it is called: seven positive changes on a diverging ramp, all on one arm.

import { ColorScale } from "./ColorScale";

/**
 * Seven week-on-week changes, every one of them positive. The ramp is
 * diverging because the quantity has a real zero, and the legend says the
 * midpoint sits there, so all seven land on one arm instead of the first
 * three being coloured as losses. Seven values have no outlier to clamp.
 */
export default function Demo() {
  return (
    <div className="rounded-lg border bg-card p-4">
      <ColorScale
        kind="diverging"
        values={[2, 4, 6, 8, 10, 12, 14]}
        format={(v) => `${v > 0 ? "+" : ""}${v}`}
        clampAt={1}
        onPick={(v) => console.log("filter to", v)}
      />
    </div>
  );
}
What it renders. Identical markup in all three panes, with only the token values changing.

Examples

Screenshots are grouped by product and include the capture date. Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.

GitHub

The contribution calendar shows daily activity using a small set of color levels.

Profile / contribution graph September 11, 2026 github.com, signed out (public profile) medium · light · desktop-web
GitHub arranges daily contributions by week and weekday, with a yearly total above the grid. The five color levels make the calendar compact but limit precise comparisons between days. In this active profile, many cells have similar shades, making variation harder to see. The weekday rows still provide a useful way to look for recurring gaps or patterns.
Show 8 more examples Hide the rest

Tableau Public

Published workbooks show how individual authors arrange charts and interactions in Tableau.

Cohort Retention Analysis September 11, 2026 Tableau Public embed view; workbook published by Tyran Christian medium · light · desktop-web
The grid follows monthly groups across periods since joining. Newer groups have fewer completed periods, creating the triangular shape. The first column is always 100%, taking the darkest color and leaving a narrower visual range for later values. Final cells also show sharp drops; the view needs to clarify whether those periods are complete before readers compare them. Group sizes are not shown.
Emergency Department / Clinical Dashboard September 11, 2026 Tableau Public embed view; emergency department patient flow workbook dense · light · desktop-web
This workbook analyzes emergency-department activity rather than showing a live bed board. Its hour-by-weekday grid makes recurring busy periods easy to locate. Monthly small multiples support seasonal comparison on a shared scale. The color ramps need careful labeling: a diverging scale on a count or duration needs a meaningful center, not an unexplained midpoint.

Grafana

Examples of panel grids, shared selectors, stat tiles, and chart configuration.

Examples / Gauge September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
The showcase includes several gauges with similar disk values. Comparing them requires reading the numbers, while each arc uses substantial space. A compact set of bars would make those values easier to compare. Elsewhere, different values share the same red treatment, making the threshold definition important to understanding the result. The examples also mix color ramps, sparklines, and threshold bands.
Examples / Geomap September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
The same flight data appears on several map backgrounds. Small green markers are difficult to distinguish on the dark maps and compete with terrain colors on the satellite view. The density layer makes broad clusters easier to see, though its settings affect how widely those clusters spread. Legends labeled Layer 1 do not explain the data being shown.
Examples / Histogram September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
The source series appears with two histogram settings. Automatic intervals reveal two peaks, while the wider bucket-size setting combines the data into four bars and makes that distinction harder to see. The underlying data is the same. This is a useful example of why interval size belongs in the interpretation of a histogram, not only in its settings.
Examples / State timeline and Status history September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
This page places a state timeline above a time-series view of the same query. The timeline converts numeric ranges into named states, making the periods easier to scan. The line chart keeps the numeric variation available for closer reading. Labels inside the colored regions explain their meaning, reducing reliance on color alone.
Examples / Table September 10, 2026 Grafana Play (signed out; no version string exposed) dense · dark · desktop-web
The tables demonstrate colored cells, gradients, images, bars, pills, and richer text. Some treatments become less useful in narrow columns: a bar is squeezed into a thin strip and nearby text is cut off. Another table shows many pages of results alongside sort controls. The interface needs to make clear whether sorting applies to all returned records or a smaller subset.
Grafana Heatmaps September 9, 2026 Grafana Play (signed out; no version string exposed) dense · dark · desktop-web
This teaching dashboard shows the source series and several heatmaps built from it. Different value scales and bucket settings change the apparent distribution. The linear view concentrates much of the data near the bottom; logarithmic views spread it across more of the chart. Titles identify the settings, making the grid useful for comparing their effects.
  • Time series The source series supplies the data for the heatmaps.
  • Heatmap Linear value intervals concentrate much of this distribution near the bottom.
  • Small multiples Repeated panels make the effects of different settings easier to compare.
  • Sequential and diverging scales Each heatmap has its own legend; check the ranges before comparing colors.