Skip to content
KONIGI Patterns

Dashboards / Charts and data displays / Threshold line and region

‹ 20 of 22 ›

Threshold line and region

People need a visible limit to judge the values on a chart.

Updated September 10, 2026

Problem

People need a visible reference to judge whether a charted value has crossed a meaningful limit.

Solution

Draw the limit on the chart and label its value and purpose. Use a distinct line style so it cannot be confused with measured data.

Use a shaded region when the whole range beyond a boundary matters, keeping the data readable over it. Make clear whether the boundary is an absolute value or relative to a defined range.

Keep the limit tied to an agreed target or rule with an owner. If an alert also depends on duration or other conditions, make those conditions available.

Use when

A defined limit helps people decide whether a measured value needs attention.

Don’t use when

The limit is aspirational. A line drawn at a number nobody agreed to teaches viewers that lines on charts are decorative, which then costs you the next one that matters.

Trade-offs

A hard boundary can exaggerate the difference between nearby values. Static limits can become outdated or react poorly to noisy data. Filled warning regions may compete with the chart itself.

Checklist

  • Who set this number, when, and against what?
  • Is it absolute or relative to the axis range, and does the panel say which?
  • Is this still the default 80, and if so, is 80 meaningful for this metric?
  • Line or region, and does the choice suit the reading distance?
  • Is the threshold visually distinguishable from the data series?
  • Does crossing it trigger anything, or is it decorative?
  • Does the alert behind it use the same number the chart draws?
  • How does a metric that hovers at the line render, and does it flicker?
  • If this is an SLO, does the panel show budget consumed rather than only the current value?
  • What happens to the threshold when the y-axis autoscales past it?

Compare

Grafana ships thresholds as a field property with a colour ramp, six display styles and a default that survives into production more often than it should, so the pattern is everywhere in Grafana dashboards and correct in fewer of them. Honeycomb replaces the bare line with an error budget: the objective is declared, and the chart shows how much of the allowance has been spent and how fast, which answers “should I act now” rather than only “am I over”. Datadog ties the drawn limit to the monitor definition, so the line on the chart and the condition that pages someone are the same object rather than two numbers that drift apart. Netdata ships alarm limits with its collectors, so the thresholds arrive pre-set by whoever wrote the integration, which is a good default and a bad excuse not to revisit them.

Anomaly band supplies an expected range that can vary over time. Alert rule adds notification behavior. Metric targets maintains the limit.

Semantic status color defines state cues. Time series provides the chart underneath.

Threshold line and region anatomy A limit drawn on the same axes as the series it judges, so "is this bad" becomes geometry rather than recall. Below, the three ways to draw it—a solid line, a dashed line, and a filled region—and what each one says. The limit on the data's own axes Checkout error rate 1.0% 1 2 1 THE LIMIT Once it's on the chart, "is this bad" stops being a question about recall. It can be answered from across a room. 2 WHERE IT CAME FROM A threshold is a claim that somebody decided what too much means. If that decision lives only in the panel's JSON, it's wrong within a quarter and nobody finds out. Three renderings, three different claims "Here is the limit" "This is annotation" "This whole zone is bad" A panel shipping with its default threshold at 80 is red for a reason nobody chose.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

The rule behind an alert, drawn where the data is. A number that is about to page someone should show the line it is approaching rather than leaving the reader to remember what it was.

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

Checkout error rate

13:3013:5514:400.0%0.4%0.8%1.4%1.0%

over 1.0% for 15 min · limit set by payments on-call, 2026-06-02, the SLO allows 0.5% a week and a page at 1.0% leaves budget to act · alert rule

ThresholdChart.tsxThe limit on the data's axes, with its owner and reason attached, and the span over it shaded from the crossing.

import { Line, LineChart, ReferenceArea, ReferenceLine, XAxis, YAxis } from "recharts";

/**
 * A threshold is a claim that somebody decided what too much means, so the
 * decision travels with the number. A limit with no owner is the default 80
 * that shipped with the panel, red for a reason nobody chose.
 */
export type Threshold = {
  value: number;
  /** Who set it, when, and against what. All three, or the number is decoration. */
  setBy: string;
  on: string;
  reason: string;
  /** Three renderings, three claims. A line says "here is the limit", dashed says "this is annotation, not data", a region says "this whole zone is bad". */
  style: "line" | "dashed" | "region";
  /** The alert rule that uses this same number, so the two cannot drift apart. */
  href?: string;
};

export type Sample = { t: number; value: number };

const hhmm = (t: number) => {
  const d = new Date(t);
  return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
};

/** Where the series crosses the limit, interpolated, so the shaded span starts at the crossing rather than at the nearest sample. */
export function breaches(data: Sample[], limit: number): [number, number][] {
  const out: [number, number][] = [];
  let start: number | null = null;
  for (let i = 1; i < data.length; i++) {
    const a = data[i - 1], b = data[i];
    const cross = a.t + ((limit - a.value) / (b.value - a.value)) * (b.t - a.t);
    if (a.value <= limit && b.value > limit) start = cross;
    if (a.value > limit && b.value <= limit && start !== null) { out.push([start, cross]); start = null; }
  }
  if (start !== null) out.push([start, data[data.length - 1].t]);
  return out;
}

const SERIES = "hsl(var(--chart-1))";
const LIMIT = "hsl(var(--status-critical))";

export function ThresholdChart({ title, data, threshold, format, width = 340, height = 150 }: {
  title: string;
  data: Sample[];
  threshold: Threshold;
  /** Renders a value the way the axis does, so the limit's label and the ticks agree. */
  format: (v: number) => string;
  width?: number;
  height?: number;
}) {
  const over = breaches(data, threshold.value);
  const top = Math.max(threshold.value, ...data.map((d) => d.value)) * 1.15;
  const label = { value: format(threshold.value), position: "insideTopRight" as const, fill: LIMIT, fontSize: 10 };

  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">
        <LineChart width={width} height={height} data={data} margin={{ top: 8, right: 8, bottom: 0, left: -20 }}>
          <XAxis interval={0} dataKey="t" type="number" domain={["dataMin", "dataMax"]} tickFormatter={hhmm} tick={{ fontSize: 10 }} stroke="hsl(var(--border))" />
          <YAxis domain={[0, top]} tickFormatter={format} tick={{ fontSize: 10 }} stroke="hsl(var(--border))" />
          {threshold.style === "region"
            ? <ReferenceArea y1={threshold.value} y2={top} fill={LIMIT} fillOpacity={0.15} stroke="none" label={label} />
            : <ReferenceLine y={threshold.value} stroke={LIMIT} strokeWidth={2} strokeDasharray={threshold.style === "dashed" ? "6 4" : undefined} label={label} />}
          {over.map(([a, b]) => (
            <ReferenceArea key={a} x1={a} x2={b} y1={threshold.value} y2={top} fill={LIMIT} fillOpacity={0.15} stroke="none" />
          ))}
          <Line type="linear" dataKey="value" stroke={SERIES} strokeWidth={2} dot={false} isAnimationActive={false} />
        </LineChart>
      </div>
      <p className="mt-2 text-[10px] text-muted-foreground">
        {over.length > 0 && <span className="text-status-critical">over {format(threshold.value)} for {Math.round(over.reduce((n, [a, b]) => n + b - a, 0) / 60_000)} min · </span>}
        limit set by {threshold.setBy}, {threshold.on}, {threshold.reason}
        {threshold.href && <> · <a href={threshold.href} className="underline underline-offset-2">alert rule</a></>}
      </p>
    </div>
  );
}

demo.tsxHow it is called: an afternoon of checkout errors crossing 1.0% for twenty minutes.

import { ThresholdChart, type Sample } from "./ThresholdChart";

/**
 * Checkout error rate through an afternoon, crossing the 1.0% limit for
 * about twenty minutes. The limit carries who set it and why, and links to
 * the alert that fires on the same number.
 */
const T0 = Date.UTC(2026, 8, 15, 13, 30);
const DATA: Sample[] = [0.2, 0.28, 0.22, 0.42, 1.12, 1.22, 0.52, 0.6].map((value, i) => ({ t: T0 + i * 600_000, value }));

export default function Demo() {
  return (
    <ThresholdChart
      title="Checkout error rate"
      data={DATA}
      format={(v) => `${v.toFixed(1)}%`}
      threshold={{
        value: 1.0,
        setBy: "payments on-call",
        on: "2026-06-02",
        reason: "the SLO allows 0.5% a week and a page at 1.0% leaves budget to act",
        style: "line",
        href: "/alerts/checkout-error-rate",
      }}
    />
  );
}
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.

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.
Show 1 more example Hide the rest

Grafana

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

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.