Skip to content
KONIGI

Dashboards / Color / Sequential and diverging scales

6 of 6

Sequential and diverging scales

Magnitude and direction need encoding in color, and the wrong ramp lies.

Updated September 10, 2026

Problem

A heatmap, a choropleth or a cohort grid encodes a quantity as colour. Whether the viewer sees the real structure or a flattened smear depends entirely on a ramp somebody picked from a dropdown.

Solution

Match the ramp to the data’s shape, and there are exactly three shapes worth distinguishing.

Sequential for values running from low to high with no meaningful middle. The ramp should vary monotonically in lightness, because lightness is the channel people read magnitude from most reliably. A ramp that only changes hue—blue to green to red—has no perceptual order, so viewers cannot tell which end is more without consulting the legend on every cell.

Diverging for values with a meaningful centre: change versus previous, deviation from a target, positive and negative. Two sequential ramps meeting at a neutral midpoint. The critical detail is that the midpoint must sit at the real zero. A diverging ramp centred at the data’s mean rather than at zero will colour a set of entirely positive values as if half of them were negative, which is a straightforward lie the chart tells confidently.

Cyclical for wrapping quantities—hour of day, angle, phase—where the ends must meet. Rare on dashboards and worth naming so nobody reaches for sequential and puts a hard discontinuity at midnight.

The rainbow ramp deserves specific mention because it remains a default in older tools and is the best-known bad choice in visualisation. It is not perceptually uniform: equal steps in value produce wildly unequal perceived changes, so it invents boundaries where the data is smooth and hides real transitions where the hues happen to be similar. It also fails completely in greyscale and for colourblind viewers.

The range is as consequential as the ramp. Grafana’s heatmap offers Start color scale from value and End color scale at value for exactly this: without clamping, one extreme cell consumes the entire ramp and everything else becomes one shade.

Use when

Colour carries a quantity rather than an identity. Heatmaps, choropleths, cohort grids, correlation matrices, calendar grids.

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

Colour is the least precise encoding available, so any ramp answers “roughly” and never “exactly”, which means a companion number or hover is usually mandatory. Ramps are theme-sensitive and a scheme tuned on white loses its low end on charcoal. Perceptually uniform schemes look duller than rainbow ones, which is a real problem in stakeholder review and a fake one everywhere else. And clamping trades fidelity for legibility: it makes the body of the distribution readable and hides how extreme the extremes are.

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 is the pattern most dependent on getting this right. Geo map is where a bad ramp meets area distortion and compounds it. Calendar heatmap depends on the range choice more than the ramp choice. Categorical series palette is the unordered counterpart. Cohort grid is the other grid where clamping decides whether anything is visible.

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

Captures whose hotspots reference this pattern, grouped by product and dated. The dashed boxes are this pattern; hover any box for the note.

GitHub

The contribution graph is the calendar heatmap every other product copied, and its five-step bucketing is the deliberate trade that made it legible.

Profile / contribution graph September 11, 2026 github.com, signed out (public profile) medium · light · desktop-web
The calendar heatmap everyone else copied, and a good demonstration of what it trades away. 3,703 contributions across 365 days is about ten a day, and the scale has five steps, so almost every cell here lands in the middle two and the year reads as one flat field of mid-green. A day with five commits and a day with twenty-five are the same colour. That is the deliberate choice—coarse buckets make the rhythm readable and make any individual day approximate—but on a profile this active there is no rhythm left to read either, because the bucketing has flattened the variation it was meant to reveal. The layout still earns its place: weeks as columns and weekdays as rows means a weekend effect would show as two pale rows across the full width, and here it doesn't, which tells you something true about this person's week. The panel beside it is worse off. It is a four-axis diagram of code review, issues, pull requests and commits, and with 100% commits it collapses to a single straight line.
  • Calendar heatmap Fifty-three columns of weeks, seven rows of weekdays, and only Mon, Wed and Fri labelled to save the space.
  • KPI tile The total sits above the grid, so the cells have a denominator. Most calendar heatmaps omit this.
  • Sequential and diverging scales Five discrete steps rather than a continuous ramp, which is why ten commits and thirty share a colour.
  • Time-range picker Sixteen years as a list. No arbitrary range, no relative window—the only unit on offer is a calendar year.
Show 8 more examples Hide the rest

Tableau Public

Thousands of dashboards made by people who are not designers, published without a review step. The best available sample of what the pattern language looks like in the wild.

Cohort Retention Analysis September 11, 2026 Tableau Public embed view; workbook published by Tyran Christian medium · light · desktop-web
The triangle, exactly as the entry describes it: thirteen monthly cohorts down the side, thirteen periods since joining across the top, and each row one cell shorter than the last because younger cohorts have had less time. Reading down column two gives the comparison the grid exists for—37%, 22%, 19%, 15%, then a slow climb back to 24%—which no single averaged retention number could show. Two things undercut it. The first column is 100% for every cohort by definition, and it takes the darkest step on the ramp, so the one column carrying no information anchors the scale and compresses every real value between 2% and 50% into what is left. And look along the staircase edge: 9%, 4%, 3%, 2%, 4%, 2%, 4%, 2%, 3%, 4%. Every row's final cell falls off a cliff relative to its neighbour, because that period is still in progress and is being drawn as though it were complete.
  • Cohort grid One row per cohort, one column per period since joining. The triangle is the shape, not a rendering accident.
  • Cohort grid 50% then 9%. The last cell in every row is a partial period drawn as a finished one.
  • Sequential and diverging scales Column one is 100% for everyone and takes the darkest step, so a constant sets the top of the ramp.
  • Ratio and rate Cohort dates, and no cohort size. A 100% first cell could be ten people or ten thousand.
Emergency Department / Clinical Dashboard September 11, 2026 Tableau Public embed view; emergency department patient flow workbook dense · light · desktop-web
Worth recording partly for what it is not. I went looking for a bed map, and what the public web has instead is this: analysis about an emergency department rather than the board the department actually runs on. There is no row per bed, no occupancy, no waiting-for column. Track boards live inside the patient record and never leave it, which is why that pattern has no example here and probably never will. What this does have is the punch card the calendar-heatmap entry names as the better answer for anything with a daily rather than weekly shape: weekday down the side, hour of day across the top, and the busy band from roughly ten to twenty-one is legible instantly without anyone labelling it. Its ramp is the problem—a blue-to-orange diverging scale on a patient count, which has no meaningful centre, so the midpoint sits wherever the data happened to average. The treemap beneath it degenerates into a mosaic of unlabelled slivers about a third of the way across.
  • Calendar heatmap The punch-card variant: hour of day against weekday. The busy band reads in a second, from the layout alone.
  • Small multiples Twelve month panels on one shared y-axis, so the seasonal fall from 265 in May to 52 in December is comparable across all of them.
  • Sequential and diverging scales A diverging blue-orange ramp on wait time, which has no meaningful centre, so the midpoint is wherever the mean fell.
  • Target and progress Each unit against a median reference line, green below and red above. A target marker doing the work of a threshold.
  • Filter bar One dropdown, full width, showing its selected value rather than a count. Everything below is scoped to it.

Grafana

The reference implementation for panel grids, template variables, and stat panels; most other tools are defined by how they differ from it.

Examples / Gauge September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
Nine gauges, and the page makes Few's argument against them without meaning to. The panel at bottom left holds seven arcs reading 30.6, 30.8, 30.8, 30.9, 30.9, 31.0 and 31.3 GB. Seven squares, seven needles, to say that seven disks are all about the same. The same seven values as a bullet graph would be seven short bars in a fraction of the height, and the one that was different would be obvious rather than requiring you to read each label. What the gauges do carry is a bounded range where the endpoints mean something, which is the case the pattern survives: these are percentages and capacities, not rates. The panel beside them is where it stops working—three circular gauges at 40.8, 20 and 48.6 GB, all with a red arc, so a value and another value two and a half times larger get the same verdict.
  • Gauge and dial Seven arcs to report that seven disks agree. This is the area argument in one panel.
  • Sparkline A sparkline inside the arc: the same series encoded twice in one square.
  • Sequential and diverging scales The arc runs pink through green with no perceptual order, so position on it means nothing without the number.
  • Semantic status color 40.8, 20 and 48.6 all arc red. The threshold fires for everything, so it says nothing.
  • Threshold line and region The coloured band around the rim is the threshold, drawn as a region rather than a line.
Examples / Geomap September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
Four maps of the same flight data, and the base map does more damage than any of the data settings. On the three dark panels the markers are small green dots on dark grey, which at this size is close to the contrast floor; on the satellite panel they are small green dots on terrain that is already green, brown and blue, and they effectively disappear. The base layer is supposed to be context and recede, and imagery is the loudest option available. The density layer along the bottom is the honest one: it stops pretending individual points resolve, and the clusters on both coasts read immediately. Its radius is doing a great deal of unlabelled work, though, and the blobs merge into clouds at a setting nobody can see. Three of the four panels also carry a legend that says "Layer 1", which is a legend occupying a corner and naming nothing.
Examples / Histogram September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
This page accidentally contains the cleanest demonstration of the bucket problem I have found in a shipped product. The panel top right is the source series, a line oscillating between 24 and 32. Directly under it, the same series on automatic buckets: roughly twenty-four bars, and it is plainly bimodal, with one hump around 27 and a taller one at 30.4. To the left of that, labelled "Force timeseries into bucket size=3", is the identical data at a bucket width of three. Four bars. One hump. The second mode has not been smoothed or de-emphasised, it is simply gone, and nothing about the chart indicates that a decision was made. Neither panel is wrong. One of them answers "is this one population or two" and the other cannot, and the only difference between them is a number in a field.
Examples / State timeline and Status history September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
The most useful thing on this page is an accident of layout: the third panel down the left column draws three series as state bands, and the panel directly underneath draws the identical query as an ordinary time series. Same data, stacked one above the other. The line chart is three tangled traces oscillating between 57 and 64, and reading it means picking a series, tracing it, and thresholding it in your head. The state timeline above has already done that and shows the conclusion, and the blue stretches—below fifty—are findable without being looked for. That is the whole argument for the encoding, and Grafana has put the before and after in adjacent panels. Worth noticing too that every coloured region here carries its word inside it. LOW, HIGH, NORMAL, CRITICAL, True, False. Colour is never the only carrier on this page.
  • Status history Continuous regions with the state name written inside each one.
  • Threshold line and region Thresholds turning a numeric series into discrete bands: under 50, 50, 300.
  • Time series The same query as the panel above, untranslated. Three traces you have to threshold yourself.
  • Sequential and diverging scales One mark per sample, green through red by temperature—so green here means cold, not good.
  • Semantic status color A closed set of two, both labelled. The clearest case on the page of colour plus a word.
Examples / Table September 10, 2026 Grafana Play (signed out; no version string exposed) dense · dark · desktop-web
Seven tables demonstrating what a cell can hold, and the range is the point: a threshold colour on the whole cell, a continuous gradient down a column, an image, a bar rendered inside the cell, a pill, and a markdown block carrying three metrics at once. Two of them stop working at this size. The bar gauge column on the right is squeezed to a sliver about six pixels wide, so the encoding that was supposed to make magnitude visible carries nothing, and the catch-phrase column beside it truncates mid-word with no way to see the rest. The bottom-left table is the one to look at hardest. It is sorted, it has column headers you can click, and the pager under it reads page 1 of 72. Any sort applied there is a sort of the page unless the query re-runs, and nothing on screen tells you which of those is happening.
  • Data table Threshold colour filling the cell, which makes a column scannable without being read.
  • Sequential and diverging scales A continuous ramp down a sorted column: the table doing a heatmap's job.
  • Data table In-cell bar gauge clipped to a sliver, and text truncated mid-word with no escape to the full value.
  • Semantic status color Value mappings turning raw levels into a closed set, each with its word in the cell.
  • Delta indicator Pills reading up, down and down fast: direction with no magnitude and no base. Also page 1 of 72.
Grafana Heatmaps September 9, 2026 Grafana Play (signed out; no version string exposed) dense · dark · desktop-web
Grafana's own teaching dashboard for the heatmap panel, and it teaches well because it holds the data constant and varies exactly one thing. The top-left panel is the source series. The eight heatmaps are all those same numbers under different y-bucket scales: linear, log2, log10, each with and without a split, two of them clamped to a 700-15k range. Read across the grid and the argument makes itself. The linear version crushes almost everything into a band near the bottom of the axis; the log versions spread the same distribution across the full height and the dense region moves. Same data, different readings, and nothing on any single panel tells you which bucketing you're looking at except the title someone remembered to write.
  • Time series The source series. Every heatmap on the page is built from this.
  • Heatmap Linear y buckets, which is the default and the one that hides the low end.
  • Small multiples Nine panels, one variable changed. The comparison is the content.
  • Sequential and diverging scales Every panel carries its own ramp legend, 1 to 30, so the colour is readable per panel but not across them.