v1.2.0
  1. ds
  2. Data display
  3. Distribution

Distribution

Histograms and the p50, p95 and p99 strip.

Averages hide the requests people complain about. A distribution shows the whole shape: where most requests land, and how long the tail is. Use a histogram for one series, and box strips to compare many.

Histogram with percentiles

POST /v1/checkout, request duration

1,200 requests, last hour. Bars past p95 are amber, past p99 red.

Show code
<figure class="chart boxed">
  <figcaption class="ch-head">
    <div><p class="ch-title">POST /v1/checkout, request duration</p><p class="ch-sub">1,200 requests, last hour. Bars past p95 are amber, past p99 red.</p></div>
  </figcaption>
  <div class="plot" style="--h:11rem" role="img" aria-label="Request duration histogram. p50 82 ms, p95 213 ms, p99 305 ms.">
  <div class="ch-grid"><i style="--at:0" data-v="0"></i><i style="--at:0.3333" data-v="50"></i><i style="--at:0.6667" data-v="100"></i><i style="--at:1" data-v="150"></i></div>
  <div class="ch-cols" style="--gap:0.12"><i class="s1" style="--v:0"></i><i class="s1" style="--v:0.02"></i><i class="s1" style="--v:0.22"></i><i class="s1" style="--v:0.4533"></i><i class="s1" style="--v:0.7067"></i><i class="s1" style="--v:0.82"></i><i class="s1" style="--v:0.6867"></i><i class="s1" style="--v:0.8133"></i><i class="s1" style="--v:0.76"></i><i class="s1" style="--v:0.5133"></i><i class="s1" style="--v:0.5067"></i><i class="s1" style="--v:0.4467"></i><i class="s1" style="--v:0.34"></i><i class="s1" style="--v:0.3"></i><i class="s1" style="--v:0.2133"></i><i class="s1" style="--v:0.14"></i><i class="s1" style="--v:0.1333"></i><i class="s1" style="--v:0.1533"></i><i class="s1" style="--v:0.14"></i><i class="s1" style="--v:0.1133"></i><i class="s1" style="--v:0.1"></i><i class="s1" style="--v:0.0533"></i><i class="c-warn" style="--v:0.0333"></i><i class="c-warn" style="--v:0.0533"></i><i class="c-warn" style="--v:0.0267"></i><i class="c-warn" style="--v:0.0667"></i><i class="c-warn" style="--v:0.0267"></i><i class="c-warn" style="--v:0.02"></i><i class="c-warn" style="--v:0.0133"></i><i class="c-warn" style="--v:0.0333"></i><i class="c-warn" style="--v:0.0133"></i><i class="c-down" style="--v:0.0067"></i><i class="c-down" style="--v:0.0067"></i><i class="c-down" style="--v:0"></i><i class="c-down" style="--v:0.0133"></i><i class="c-down" style="--v:0"></i><i class="c-down" style="--v:0.0067"></i><i class="c-down" style="--v:0"></i><i class="c-down" style="--v:0"></i><i class="c-down" style="--v:0.0067"></i></div>
  <i class="ch-pct" style="--x:0.2050" data-v="p50 82"></i>
  <i class="ch-pct warn" style="--x:0.5325" data-v="p95 213"></i>
  <i class="ch-pct down" style="--x:0.7625" data-v="p99 305"></i>
  <div class="ch-x"><span style="--at:0">0 ms</span><span style="--at:0.25">100 ms</span><span style="--at:0.5">200 ms</span><span style="--at:0.75">300 ms</span><span style="--at:1">400 ms</span></div>
</div>
</figure>

Columns with a small gap, so the shape reads as one mass. Percentile markers are solid lines with their value on a surface chip; p95 and p99 take the warn and down colours, and so do the bars beyond them, which is where an objective usually sits.

Box strips

  • GET /v1/orders/{id}49 / 112 / 189 ms
  • GET /v1/search133 / 322 / 506 ms
  • POST /v1/checkout210 / 530 / 785 ms
  • POST /v1/exports376 / 964 / 1397 ms
  • GET /v1/reports/daily524 / 1268 / 1755 ms
  • 03006009001.2 sp50 / p95 / p99
Show code
<ul class="boxes">
  <li><span class="bx-l"><code>GET /v1/orders/{id}</code></span><span class="box" style="--min:0.0067;--p25:0.0275;--p50:0.0408;--p75:0.0600;--p95:0.0933;--p99:0.1575;--max:0.2483" role="img" aria-label="p50 49 ms, p95 112 ms, p99 189 ms"><i></i><b></b><s class="p95"></s><s class="p99"></s></span><span class="bx-v"><b>49</b> / 112 / 189 ms</span></li>
  <li><span class="bx-l"><code>GET /v1/search</code></span><span class="box" style="--min:0.0183;--p25:0.0792;--p50:0.1108;--p75:0.1650;--p95:0.2683;--p99:0.4217;--max:0.7092" role="img" aria-label="p50 133 ms, p95 322 ms, p99 506 ms"><i></i><b></b><s class="p95"></s><s class="p99"></s></span><span class="bx-v"><b>133</b> / 322 / 506 ms</span></li>
  <li><span class="bx-l"><code>POST /v1/checkout</code></span><span class="box" style="--min:0.0308;--p25:0.1175;--p50:0.1750;--p75:0.2500;--p95:0.4417;--p99:0.6542;--max:0.7442" role="img" aria-label="p50 210 ms, p95 530 ms, p99 785 ms"><i></i><b></b><s class="p95"></s><s class="p99"></s></span><span class="bx-v"><b>210</b> / 530 / 785 ms</span></li>
  <li><span class="bx-l"><code>POST /v1/exports</code></span><span class="box" style="--min:0.0458;--p25:0.2158;--p50:0.3133;--p75:0.4517;--p95:0.8033;--p99:1.0000;--max:1.0000" role="img" aria-label="p50 376 ms, p95 964 ms, p99 1397 ms"><i></i><b></b><s class="p95"></s><s class="p99"></s></span><span class="bx-v"><b>376</b> / 964 / 1397 ms</span></li>
  <li><span class="bx-l"><code>GET /v1/reports/daily</code></span><span class="box" style="--min:0.0925;--p25:0.3067;--p50:0.4367;--p75:0.6250;--p95:1.0000;--p99:1.0000;--max:1.0000" role="img" aria-label="p50 524 ms, p95 1268 ms, p99 1755 ms"><i></i><b></b><s class="p95"></s><s class="p99"></s></span><span class="bx-v"><b>524</b> / 1268 / 1755 ms</span></li>
  <li><span></span><span class="bx-axis"><span style="--at:0">0</span><span style="--at:.25">300</span><span style="--at:.5">600</span><span style="--at:.75">900</span><span style="--at:1">1.2 s</span></span><span class="bx-v">p50 / p95 / p99</span></li>
</ul>

One row per endpoint on a shared axis. The whisker runs from the fastest to the slowest request, the box from p25 to p75, the line is the median, and the amber and red dots are p95 and p99. Sort by the percentile you care about; here it is p50.

ClassOnWhat it does
.ch-pctiA percentile line in a plot at --x, label from data-v. Ink by default; warn and down.
.boxesulRows of label, box and values on a shared axis.
.boxspanA box strip. --min, --p25, --p50, --p75, --p95, --p99, --max, all 0 to 1. Children: i the box, b the median, s.p95 and s.p99 the dots.
.bx-axisspanTick labels under the middle column at --at.
bins() / pct()src/lib/chart.tsEqual-width histogram counts and nearest-rank percentiles.
Do
  • Show p50, p95 and p99 together: the gap between them is the story.
  • A shared axis for every row you compare.
  • Cut the x axis at a round value and say so: "requests over 400 ms not shown".
Don't
  • Averages for latency.
  • Log scales without labelling them as log.
  • Per-row axes in a comparison.

Accessibility

  • Every box strip has its percentiles in the label and in the value column, so nothing depends on reading positions.