Box plot
latency distribution
Latency distribution. The chart that shows the tail a mean hides.
- Format.d3
- Length112 lines
- Includesnone
The source
112 lines of D3 JavaScript, and uses only the bundled D3 build. Copy it, or open the template inside Gnomon and render it as it is.
// Box plot — median, quartiles, 1.5×IQR whiskers, outliers plotted.
// Compact enough to line up a dozen services side by side, which is what a
// mean-and-error-bar chart cannot honestly do.
const rng = d3.randomLcg(31);
const normal = d3.randomNormal.source(rng);
const services = [
{ name: 'gateway', mu: 45, sigma: 12 },
{ name: 'orders', mu: 120, sigma: 40 },
{ name: 'catalogue', mu: 80, sigma: 25 },
{ name: 'payments', mu: 210, sigma: 90 },
{ name: 'identity', mu: 60, sigma: 18 },
{ name: 'search', mu: 150, sigma: 55 },
];
const groups = services.map(s => {
const draw = normal(s.mu, s.sigma);
const values = d3.range(60).map(() => Math.max(5, draw())).sort(d3.ascending);
const q1 = d3.quantile(values, 0.25);
const median = d3.quantile(values, 0.5);
const q3 = d3.quantile(values, 0.75);
const iqr = q3 - q1;
const lo = Math.max(values[0], q1 - 1.5 * iqr);
const hi = Math.min(values[values.length - 1], q3 + 1.5 * iqr);
return {
name: s.name, q1, median, q3, lo, hi,
outliers: values.filter(v => v < lo || v > hi),
};
});
const margin = { top: 44, right: 30, bottom: 44, left: 60 };
const x = d3.scaleBand()
.domain(groups.map(g => g.name))
.range([margin.left, width - margin.right])
.padding(0.4);
const y = d3.scaleLinear()
.domain([0, d3.max(groups, g => Math.max(g.hi, d3.max(g.outliers) ?? 0))]).nice()
.range([height - margin.bottom, margin.top]);
const color = d3.scaleOrdinal(groups.map(g => g.name), theme.palette);
const svg = d3.select(container).append('svg')
.attr('viewBox', [0, 0, width, height])
.attr('width', width)
.attr('height', height)
.attr('font-family', 'system-ui, sans-serif')
.attr('font-size', 11);
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(6))
.call(g => g.select('.domain').remove())
.call(g => g.selectAll('text').attr('fill', theme.muted))
.call(g => g.selectAll('line')
.attr('stroke', theme.muted).attr('stroke-opacity', 0.25)
.attr('x2', width - margin.left - margin.right));
svg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.call(g => g.select('.domain').attr('stroke', theme.muted))
.call(g => g.selectAll('text').attr('fill', theme.foreground))
.call(g => g.selectAll('line').attr('stroke', theme.muted));
const g = svg.selectAll('g.box')
.data(groups)
.join('g')
.attr('class', 'box')
.attr('transform', d => `translate(${x(d.name) + x.bandwidth() / 2},0)`);
g.append('line')
.attr('y1', d => y(d.lo)).attr('y2', d => y(d.hi))
.attr('stroke', theme.muted).attr('stroke-width', 1.5);
for (const key of ['lo', 'hi']) {
g.append('line')
.attr('x1', -x.bandwidth() / 4).attr('x2', x.bandwidth() / 4)
.attr('y1', d => y(d[key])).attr('y2', d => y(d[key]))
.attr('stroke', theme.muted).attr('stroke-width', 1.5);
}
g.append('rect')
.attr('x', -x.bandwidth() / 2)
.attr('width', x.bandwidth())
.attr('y', d => y(d.q3))
.attr('height', d => Math.max(1, y(d.q1) - y(d.q3)))
.attr('rx', 2)
.attr('fill', d => color(d.name))
.attr('fill-opacity', 0.55)
.attr('stroke', d => color(d.name));
g.append('line')
.attr('x1', -x.bandwidth() / 2).attr('x2', x.bandwidth() / 2)
.attr('y1', d => y(d.median)).attr('y2', d => y(d.median))
.attr('stroke', theme.mode === 'dark' ? '#ffffff' : '#1a1a1a')
.attr('stroke-width', 2);
g.each(function (d) {
d3.select(this).selectAll('circle')
.data(d.outliers)
.join('circle')
.attr('cy', v => y(v))
.attr('r', 2.5)
.attr('fill', theme.palette[2]);
});
svg.append('text')
.attr('x', margin.left)
.attr('y', 22)
.attr('fill', theme.foreground)
.attr('font-size', 13)
.attr('font-weight', 600)
.text('Request latency by service (ms)');Render this offline
This template ships in Gnomon and renders on your machine, with no account and nothing sent to a server. The browser editor is free and needs no install.
Others in D3 visualisations
- Sankey — request flowFlow with volume. The best chart here for "where does it all go".
- Chord — service interactionWho talks to whom, when the traffic is bidirectional.
- Arc diagram — dependenciesNodes on one axis, arcs above. Readable where a force graph is not, provided the ordering means something.
- Adjacency matrix — couplingAdjacency matrix. Unfashionable, and better than a force graph for dense dependencies.
- Edge bundling — module importsHierarchical edge bundling. For import graphs big enough that straight edges become a hairball.
- Sunburst — nested spendNested hierarchy, radially. Prettier than an icicle, harder to compare.
- Icicle — nested spend (linear)A sunburst unrolled flat. Harder to love, much easier to compare siblings.
- Circle packing — nested sizeCircle packing. Nested size when the nesting matters more than reading exact areas.
- Treemap — portfolio costNested size. Good for cost, storage, lines of code.
- Tidy tree — structureA tidy tree. The default for anything with one parent per node.
- Radial tree — structure (radial)The same tree bent into a circle. Fits more depth on a slide, costs you easy comparison.
- Dendrogram — clusteringClustering, with join height carrying the distance. Not merely a tree with curves.
- Indented tree — file/spec outlineA file or spec outline. The chart that looks like the thing it describes.
- Force graph — service mapService map. Use for clusters, not for reading individual edges.
- Force graph — disjoint clustersForce graph that keeps unconnected clusters apart rather than flinging them off screen.
- Force graph — radial tiersForce layout pinned to rings, so tier is a position instead of a colour legend.
- Directed graph — call directionDirected edges with arrowheads. For when direction is the question, not just adjacency.
- Force tree — blast radiusBlast radius from one node: what breaks if this goes.
- Calendar heatmap — daily activityDaily activity over a year: deploys, incidents, commits.
- Streamgraph — shifting mixShifting composition over time. Good for the mix, poor for reading any single value.
- Gantt — delivery roadmapDelivery roadmap, rendered from data.
- Radar — capability scoringCapability scoring across axes. Fine for one subject, misleading with four overlaid.
- Bullet chart — target vs actualTarget against actual, in one line. The chart a gauge wishes it were.
- Beeswarm — distribution by groupEvery point, grouped, without the overplotting a strip plot suffers.
- Horizon chart — many series, little spaceMany series in little space. Takes a moment to learn, then very dense.
- Slope chart — before and afterBefore and after, two points, one line each. Devastatingly clear.
- Parallel coordinates — multi-criteriaMulti-criteria comparison: for option analysis.
- Grouped bar — category comparisonCategory comparison. Unglamorous, and usually the right answer.
- Multi-line — metrics over timeMetrics over time. Keep it under about five series, or switch to horizon.