Skip to content
KONIGI Patterns

Dashboards / Charts and data displays / Histogram and distribution

‹ 10 of 22 ›

Histogram and distribution

People need to see how values are spread out rather than relying on an average.

Updated September 10, 2026

Problem

An average does not show whether values are tightly grouped, spread out, or split into distinct groups.

Solution

Divide the value range into intervals and show how many observations fall in each one. Label the measure, intervals, and count or percentage axis.

Choose intervals that reveal useful structure without making random variation look like a pattern. Make the boundaries available, especially when the source has already grouped the data.

Use consistent intervals and scales for comparisons. Keep unusually large or small values visible, or clearly label any limits that exclude them.

Use when

People need to understand the spread, clusters, or extreme values in a set of measurements.

Don’t use when

The counts are small, where a histogram is a bar chart with a misleading name. Or when the change over time is the point, in which case the histogram is one frame of a film and a heatmap is the film.

Trade-offs

A histogram combines the selected time period and can hide when a change happened. Interval choices can merge or split apparent groups. A long tail can compress the rest of the chart, while clipping it hides the extremes.

Checklist

  • How many buckets, and who chose that number?
  • Would a different bucket count merge or split a mode that matters?
  • Are the bucket edges shown, and do any of them fall on a value people care about, like a timeout?
  • Is the x-axis linear when the data is skewed, and would a log axis show more?
  • Is the tail clipped, and does the chart say so?
  • Is the y-axis a count or a proportion, and does that match the neighbouring panels?
  • If two histograms sit side by side, do they share bucket edges and axis ranges?
  • Are these buckets computed at query time or fixed at instrumentation time?
  • Does the panel report the sample size?
  • Would a heatmap answer the viewer’s actual question by adding time back?

Compare

Grafana exposes bucket count, size and offset as panel options, which is honest about the decision and also means two panels of the same metric can disagree about its shape with nothing on screen to explain why. Prometheus moves the argument upstream: classic histograms fix the buckets at instrumentation time, native histograms hold a relative resolution instead, so the same query returns a usefully different answer depending on which the service emits. Honeycomb keeps raw events rather than pre-aggregating, so a distribution is something you ask for at query time over whatever dimension you just thought of, rather than something you predicted a month ago. Datadog leans on distribution metrics with globally accurate percentiles, which trades the ability to see the shape for the ability to aggregate the summary correctly across hosts.

Heatmap adds time to the distribution. Percentile summary reports selected positions within it. Ratio and rate can express counts as proportions.

Stacked composition divides totals into categories. Data table provides exact interval counts.

Histogram and distribution anatomy The same six thousand requests bucketed twice. Eight buckets merge a fast path and a slow path into one hump and bury the timeout. Forty buckets separate them and expose the wall at two seconds where everything was truncated. Same population, two bucket counts 0 1s 2s 8 buckets · one hump 0 1s 2s 40 buckets · two, and a wall 1 2 1 TOO FEW MERGES THE HUMPS Too many and the shape dissolves into noise. Neither failure announces itself, and the count is usually somebody's default. 2 THE WALL A spike against the right edge is a timeout truncating the tail. Everything past it was recorded as exactly two seconds. The same decision was already made once, before the panel saw anything: classic histograms fix their buckets at instrumentation time, which is why the quantiles that come back out are bounded by bucket width and by nothing about the data. Is this one population or two, is there a tail, is there a wall. No summary says.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

A distribution, not a time series with the axis relabelled. The bucket width is the whole argument: too wide and two humps merge, too narrow and noise looks like structure.

shadcn
npx shadcn@latest add toggle-group
npm
recharts
Tokens
--card--muted--muted-foreground--border--chart-1--status-warn
01s2s

40 buckets of 50ms · 6,000 requests · 401 recorded as exactly 2s, which is a timeout, not a mode

Histogram.tsxBins raw samples itself, so the bucket count is a prop and not the collector's default. Flags a wall at the top of the range.

import { Bar, BarChart, ReferenceLine, XAxis, YAxis } from "recharts";

/**
 * The bucket count is the whole argument, so it is a required prop rather
 * than a default someone forgot. Too few and two humps merge; too many and
 * the shape dissolves into noise. Neither failure announces itself.
 */
export type Bin = { lo: number; hi: number; count: number };

export function bin(samples: number[], buckets: number, min: number, max: number): Bin[] {
  const width = (max - min) / buckets;
  const bins: Bin[] = Array.from({ length: buckets }, (_, i) => ({ lo: min + i * width, hi: min + (i + 1) * width, count: 0 }));
  for (const v of samples) {
    // The top edge is inclusive, so a sample at exactly `max` lands in the
    // last bin instead of falling off the chart.
    const i = Math.min(buckets - 1, Math.floor((v - min) / width));
    if (i >= 0) bins[i].count++;
  }
  return bins;
}

/**
 * A spike against the right edge is a timeout truncating the tail:
 * everything past it was recorded as exactly `max`. Flagged when the last
 * bin holds several times what the bins before it do.
 */
export function wall(bins: Bin[]): Bin | null {
  const last = bins[bins.length - 1];
  const before = bins.slice(-6, -1);
  const mean = before.reduce((n, b) => n + b.count, 0) / before.length;
  return last.count > 0 && last.count > 4 * mean ? last : null;
}

const fmt = (v: number) => (v === 0 ? "0" : v % 1000 === 0 ? `${v / 1000}s` : `${v}ms`);

export function Histogram({ samples, buckets, min = 0, max, width = 300, height = 150 }: {
  /** Raw values, in ms. The component bins them, so changing `buckets` rebins the same population. */
  samples: number[];
  buckets: number;
  min?: number;
  /** The top of the range. Required, because a histogram cut at the data's max hides the wall. */
  max: number;
  width?: number;
  height?: number;
}) {
  const bins = bin(samples, buckets, min, max);
  const cut = wall(bins);
  const step = (max - min) / buckets;
  const data = bins.map((b) => ({ mid: b.lo + step / 2, count: b.count }));

  return (
    <div>
      <div className="overflow-x-auto">
        <BarChart width={width} height={height} data={data} barCategoryGap={1} margin={{ top: 4, right: 8, bottom: 0, left: 8 }}>
          <XAxis interval={0} dataKey="mid" type="number" domain={[min, max]} ticks={[min, (min + max) / 2, max]} tickFormatter={fmt}
            tick={{ fontSize: 10 }} stroke="hsl(var(--border))" />
          <YAxis hide domain={[0, "dataMax"]} />
          {cut && <ReferenceLine x={cut.lo} stroke="hsl(var(--status-warn))" strokeDasharray="3 3" />}
          <Bar dataKey="count" fill="hsl(var(--chart-1))" isAnimationActive={false} />
        </BarChart>
      </div>
      <p className="mt-1 text-[10px] tabular-nums text-muted-foreground">
        {buckets} buckets of {fmt(Math.round(step))} · {samples.length.toLocaleString("en-GB")} requests
        {cut && <span className="text-status-warn"> · {cut.count} recorded as exactly {fmt(max)}, which is a timeout, not a mode</span>}
      </p>
    </div>
  );
}

demo.tsxHow it is called: six thousand requests at 40 buckets, with a toggle to rebin them at 8 and watch the humps merge.

import { useState } from "react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Histogram } from "./Histogram";

/**
 * Six thousand checkout requests. The collector stored them in forty fixed
 * 50ms buckets, so the samples are rebuilt at each bucket's midpoint; the
 * 401 that hit the two-second timeout were recorded as exactly 2s. Eight
 * buckets merge the fast and slow paths into one hump and bury the wall.
 */
const PER_BUCKET = [
  375, 1865, 920, 240, 60, 21, 39, 60, 88, 103, 146, 190, 188, 164, 164, 162, 159, 108, 95, 74,
  66, 68, 53, 51, 26, 32, 21, 11, 10, 13, 6, 5, 5, 3, 3, 2, 3, 0, 0, 0,
];
const TIMED_OUT = 401;

const SAMPLES = [
  ...PER_BUCKET.flatMap((n, i) => Array.from({ length: n }, () => i * 50 + 25)),
  ...Array.from({ length: TIMED_OUT }, () => 2000),
];

export default function Demo() {
  const [buckets, setBuckets] = useState(40);
  return (
    <div className="rounded-lg border bg-card p-4">
      <Histogram samples={SAMPLES} buckets={buckets} max={2000} />
      <ToggleGroup type="single" value={String(buckets)} onValueChange={(v) => v && setBuckets(Number(v))}
        className="mt-3 justify-start gap-1" aria-label="bucket count">
        {[8, 20, 40].map((n) => (
          <ToggleGroupItem key={n} value={String(n)} size="sm" className="h-6 rounded-full border px-2 text-[10px] data-[state=on]:bg-muted">
            {n} buckets
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
    </div>
  );
}
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 / 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.
Show 1 more example Hide the rest

Kibana

A query bar and filtering controls define the data shown in charts and document tables.

Discover / filebeat logs September 10, 2026 Elastic demo environment (guest session; no version string exposed) dense · light · desktop-web
The view places a searchable field list beside a large table of documents, with a volume chart above. Search is useful for navigating the hundreds of available fields. The default Summary column combines many fields into each row, making quick comparisons difficult until people choose more focused columns. Pagination and sorting are visible, but the scope of each operation needs to be clear.