Skip to content
KONIGI Patterns

Dashboards / Charts and data displays / Anomaly band

1 of 22 ›

Anomaly band

People need to distinguish unusual changes from a metric’s normal ups and downs.

Updated September 9, 2026

Problem

A metric can rise and fall on a regular schedule. A fixed threshold may miss unusual changes or flag normal ones.

Solution

Draw a quiet band showing the expected range at each point in time, based on the metric’s history. Keep the actual line readable and clearly mark values outside the range.

Explain how the expected range is calculated and what affects its width. Account for recurring patterns when the data supports them. Make clear that an unusual value needs investigation and is not automatically a problem.

Use when

The metric has an established rhythm and people need to spot departures from it.

Don’t use when

The metric is supposed to be flat, in which case a threshold line is clearer and cheaper. Or when history is short; a band trained on three days of data is a guess drawn with authority. And never on a chart whose viewer can’t see or change the algorithm, because they’ll trust it anyway.

Trade-offs

A wide band can hide meaningful changes, while a narrow one creates false alarms. Expected ranges can become outdated after a deliberate change. The band also adds visual weight and shifts attention away from absolute values.

Checklist

  • Is the band visibly quieter than the series? Gray or very low alpha, no outline.
  • When the series leaves the band, does only that segment change, and does the color match the site’s semantic status scale?
  • Can the viewer find out how the band is computed and over what history?
  • Does the band exist on the time range the viewer is looking at, or only on the last few hours?
  • What happens at the right edge, where the newest points have the least history?
  • Does hover show the expected range as numbers, not just the actual value?
  • If an alert is attached to the band, does the chart say so?
  • Does the band survive the switch to dark theme without becoming invisible or glowing?

Compare

Datadog draws the band on the chart, gray, and recolors excursions; it’s the version most people picture. Netdata puts anomaly rate in a separate ribbon above every chart, computed per second, so the band never obscures the data but the eye has to travel. Grafana has no native band; teams fake one with a shaded series from a forecast query, which is a tell that the pattern is more product than chart type. Honeycomb skips the band entirely in favor of BubbleUp, which asks the user to draw the anomaly and then explains it, a cross-filter answer to the same problem.

Time series shows the measured values. Threshold line provides a fixed reference. Alert rule can notify someone about a departure.

Auto-insight describes detected changes in words. Semantic status color defines how warnings appear.

Anomaly band anatomy A series inside a band whose width follows the metric's own daily rhythm, wide overnight and narrow at midday, with the one segment that leaves the band marked. Below, the same data under a band too wide to ever fire and one so tight it is a static threshold with extra steps. The band is the expectation, not the limit Requests per second · 24h 03:00 12:00 21:00 1 2 3 1 WIDE OVERNIGHT The band is computed from the metric's own history, with the weekday and the hour in it. 2 NARROW AT MIDDAY Which is the whole difference from a flat threshold: the same value is normal at noon and alarming at four in the morning. 3 THE ONE ALARM The band stays quiet and grey. Colour appears only where the line leaves it. The width is the tolerance, made visible Too wide · never fires Too tight · a threshold again
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

A band saying what normal looked like, so a spike is read against an expectation rather than against a reader's memory. The band has to say how it was computed, because a model nobody can inspect is an opinion drawn as a fact.

npm
recharts
Tokens
--card--muted--muted-foreground--border--popover--status-critical--chart-1

Requests per second · 24h

03:0012:0021:00

Band: median ± 2 MAD for the same weekday and hour, from the last four weeks. 3 of 24 points outside it.

AnomalyBand.tsxA quiet range area under the series, colour only on the segment that leaves it, and a required method line saying how the band was computed.

import { Area, ComposedChart, Line, Tooltip, XAxis, YAxis } from "recharts";

export type Point = {
  /** The x label, "13:00". */
  t: string;
  value: number;
  /** What normal looks like at this moment: the band's floor and ceiling. */
  expected: [lo: number, hi: number];
};

/**
 * The band has to say how it was computed. A model nobody can inspect is an
 * opinion drawn as a fact, so the method and its history are required, and
 * they print under the chart rather than living in a tooltip.
 */
export type Method = { name: string; history: string };

const BAND = "hsl(var(--muted))";
const SERIES = "hsl(var(--chart-1))";
const ALARM = "hsl(var(--status-critical))";

const outside = (p: Point) => p.value < p.expected[0] || p.value > p.expected[1];

export function AnomalyBand({ title, points, method, width = 400, height = 180 }: {
  title: string;
  points: Point[];
  method: Method;
  width?: number;
  height?: number;
}) {
  /**
   * Two lines over one series. The normal line is null wherever the value
   * leaves the band; the alarm line is null wherever it stays in. Each keeps
   * the boundary point on either side so the two meet instead of gapping.
   * Colour appears only where the line leaves the band, and nowhere else.
   */
  const data = points.map((p, i) => {
    const out = outside(p);
    const nearOut = out || outside(points[i - 1] ?? p) || outside(points[i + 1] ?? p);
    return { ...p, normal: out ? null : p.value, alarm: nearOut ? p.value : null };
  });
  const alarms = points.filter(outside).length;

  return (
    <div className="rounded-lg border bg-card p-4">
      <p className="border-b pb-2 text-[11px] uppercase tracking-wide text-muted-foreground">{title}</p>
      <div className="mt-3 overflow-x-auto">
        <ComposedChart width={width} height={height} data={data} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
          <XAxis interval={0} dataKey="t" ticks={["03:00", "12:00", "21:00"]} tickLine={false} axisLine={{ stroke: "hsl(var(--border))" }}
            tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} />
          <YAxis hide domain={["dataMin - 100", "dataMax + 100"]} />
          {/* Quiet and grey. No outline: an edge would read as a limit. */}
          <Area dataKey="expected" fill={BAND} stroke="none" fillOpacity={1} isAnimationActive={false} />
          <Line dataKey="normal" stroke={SERIES} strokeWidth={2} dot={false} isAnimationActive={false} />
          <Line dataKey="alarm" stroke={ALARM} strokeWidth={2.5} dot={false} isAnimationActive={false} />
          {/* Hover gives the range as numbers, so "outside" can be checked. */}
          <Tooltip
            isAnimationActive={false}
            contentStyle={{ fontSize: 11, background: "hsl(var(--popover))", borderColor: "hsl(var(--border))" }}
            formatter={(v, name, item) => {
              const p = item.payload as Point;
              return name === "expected" ? [`${p.expected[0]}–${p.expected[1]}`, "expected"] : [String(v), "actual"];
            }}
          />
        </ComposedChart>
      </div>
      <p className="mt-2 text-[11px] text-muted-foreground">
        Band: {method.name}, from {method.history}.
        {alarms > 0 && ` ${alarms} of ${points.length} points outside it.`}
      </p>
    </div>
  );
}

demo.tsxHow it is called: 24 hourly points with a band that follows the daily rhythm, and three that leave it in the evening.

import { AnomalyBand, type Point } from "./AnomalyBand";

/**
 * Twenty-four hours of requests per second. The band is ±220 at 03:00 and
 * ±80 at midday because the metric's own rhythm is, and three evening points
 * leave it. Everything else is wobble the band already allows for.
 */
const HOURS: [center: number, half: number, actual: number][] = [
  [520, 190, 500], [470, 200, 450], [430, 210, 410], [410, 220, 430], [430, 210, 400], [500, 190, 520],
  [620, 170, 600], [800, 140, 830], [1000, 120, 980], [1180, 100, 1200], [1300, 90, 1290], [1380, 80, 1400],
  [1420, 80, 1410], [1400, 80, 1380], [1340, 90, 1360], [1240, 100, 1220], [1120, 110, 1140], [1000, 120, 990],
  [900, 130, 920], [820, 140, 870], [750, 150, 1260], [690, 160, 1320], [630, 170, 900], [570, 180, 640],
];

const POINTS: Point[] = HOURS.map(([center, half, actual], h) => ({
  t: `${String(h).padStart(2, "0")}:00`,
  value: actual,
  expected: [center - half, center + half],
}));

export default function Demo() {
  return (
    <AnomalyBand
      title="Requests per second · 24h"
      points={POINTS}
      method={{ name: "median ± 2 MAD for the same weekday and hour", history: "the last four weeks" }}
    />
  );
}
What it renders. Identical markup in both 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.

Netdata

A large collection of system charts with per-chart controls and anomaly-rate views.

Anomalies / Anomaly advisor September 11, 2026 Netdata Agent, Anomaly advisor (public registry node, signed out) medium · dark · desktop-web
Netdata shows the share and count of measurements flagged as unusual in separate charts. This provides a compact overview across many metrics rather than drawing an expected range on each one. People then select a time period to see which metrics contributed. The empty detail panel explains that a selection is needed before results will appear.
  • Anomaly band A separate anomaly-rate chart summarizes many metrics.
  • Explain this metric Definitions appear directly beneath section headings.
  • Cross-filter Selecting a time period reveals the contributing metrics.
  • Empty state The empty panel explains which action will produce results.
  • Share and embed A report control is available above the charts.