Histograms and box plots
Twelve hundred request latencies from three services as the two charts that show a distribution: a histogram with a bin slider where the three series share the same edges, stacked or side by side, with a p95 marker; and box plots per service and region with an adjustable whisker rule, outlier dots, and a log scale for a service an order of magnitude slower.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
Distribution charts: rowsToHistogramSpec bins a sample by count, width or rule (sturges, fd, sqrt) with one series per group sharing the same edges, stacked or side by side, and a reference line marks the p95; rowsToBoxSpec draws the five-number summary per category and series with whisker as the IQR rule and everything past the whiskers as outlier dots; yScale log for a group an order of magnitude apart. Free, in @svgrid/grid.
Twelve hundred request latencies from three services, as the two charts that show a distribution rather than a value:
rowsToHistogramSpecbins the sample: a bin count, a bin width, or a rule (sturges,fd,sqrt); one series per service shares the same edges so the bars line up.stackedpiles them, otherwise they sit side by side. A reference line marks the p95 of the whole sample.rowsToBoxSpecdraws the five-number summary per service and per region: the box is the middle half, the whiskers reach the last point inside 1.5 IQR (whisker), and everything past them is an outlier dot. Flip the whisker to 3 IQR and watch the outliers get absorbed.yScale: 'log'on the box plot when one service is an order of magnitude slower.
Free, in @svgrid/grid.
Imports, features and API used
Imports: @svgrid/grid, ../shared/ChartDataGrid.svelte, ../shared/ViewSwitch.svelte
Frequently asked questions
How do I choose the bins?
Pass bins for a count, binWidth for a fixed width, or method: sturges, fd or sqrt for a rule to rowsToHistogramSpec. Every series is binned with the same edges so the bars line up.
What are the outlier dots?
Points past the whiskers. The whiskers stop at the last observation inside whisker times the interquartile range, 1.5 by default; a larger rule absorbs more points.
Can I draw one box per group and series?
Yes: rowsToBoxSpec takes a category and a series field, and draws the boxes side by side within each category.
Related documentation
Source code (444-chart-distribution.svelte)
<!-- Documented in: docs/help/charts/gallery.md -->
<script lang="ts">
/**
* 444. Histograms and box plots
* -----------------------------
* Twelve hundred request latencies from three services, as the two charts
* that show a distribution rather than a value:
*
* - `rowsToHistogramSpec` bins the sample: a bin count, a bin width, or a
* rule (`sturges`, `fd`, `sqrt`); one series per service shares the same
* edges so the bars line up. `stacked` piles them, otherwise they sit side
* by side. A reference line marks the p95 of the whole sample.
* - `rowsToBoxSpec` draws the five-number summary per service and per
* region: the box is the middle half, the whiskers reach the last point
* inside 1.5 IQR (`whisker`), and everything past them is an outlier dot.
* Flip the whisker to 3 IQR and watch the outliers get absorbed.
* - `yScale: 'log'` on the box plot when one service is an order of
* magnitude slower.
*
* Free, in @svgrid/grid.
*/
import { SvChart, rowsToBoxSpec, rowsToHistogramSpec, type ChartSpec } from '@svgrid/grid'
import ChartDataGrid from '../shared/ChartDataGrid.svelte'
import ViewSwitch from '../shared/ViewSwitch.svelte'
type Row = { service: string; region: string; ms: number }
let seed = 5
const rnd = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648)
// Log-normal latencies: most requests fast, a long right tail.
const lognormal = (mu: number, sigma: number) => {
const u = 1 - rnd()
const v = rnd()
const z = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
return Math.exp(mu + sigma * z)
}
const rows: Row[] = []
const services = [
{ name: 'search', mu: 4.2, sigma: 0.35 },
{ name: 'checkout', mu: 4.9, sigma: 0.45 },
{ name: 'reports', mu: 5.8, sigma: 0.6 },
]
for (const s of services) for (const region of ['us', 'eu', 'ap']) for (let i = 0; i < 130; i += 1) rows.push({ service: s.name, region, ms: Math.round(lognormal(s.mu + (region === 'ap' ? 0.2 : 0), s.sigma)) })
const sorted = rows.map((r) => r.ms).sort((a, b) => a - b)
const p95 = sorted[Math.floor(sorted.length * 0.95)]!
let bins = $state(30)
let stacked = $state(true)
let whisker = $state(1.5)
let logY = $state(false)
const histogram = $derived<ChartSpec>({
...rowsToHistogramSpec(rows.filter((r) => r.ms < 1500), { value: 'ms', series: 'service', bins }),
stacked,
title: 'Latency distribution',
subtitle: `${rows.length} requests in ${bins} bins`,
xAxis: { title: 'Latency (ms)' },
yAxis: { title: 'Requests', gridLines: true },
referenceLines: [{ value: p95, axis: 'x', label: `p95 ${p95} ms`, color: '#dc2626', dashed: true }],
height: 320,
})
const boxes = $derived<ChartSpec>({
...rowsToBoxSpec(rows, { category: 'service', value: 'ms', series: 'region', whisker }),
title: 'Latency by service and region',
subtitle: `Whiskers at ${whisker} IQR; dots are outliers`,
yScale: logY ? 'log' : 'linear',
yAxis: { title: 'Latency (ms)', gridLines: true },
height: 320,
})
// Chart | Grid: the same spec as the chart or as the rows behind it.
let view = $state<'chart' | 'grid'>('chart')
</script>
<section class="wrap">
<header class="chrome">
<ViewSwitch bind:value={view} options={[['chart', 'Chart'], ['grid', 'Grid']]} />
<label class="ctl">
Bins
<input type="range" min="8" max="80" step="1" bind:value={bins} />
<span class="muted">{bins}</span>
</label>
<label class="chk"><input type="checkbox" bind:checked={stacked} /> Stack the services</label>
<label class="ctl">
Whisker
<select bind:value={whisker}>
<option value={1.0}>1 IQR</option>
<option value={1.5}>1.5 IQR</option>
<option value={3}>3 IQR</option>
</select>
</label>
<label class="chk"><input type="checkbox" bind:checked={logY} /> Log scale on the boxes</label>
<span class="note">
A histogram with shared bins across three series and a p95 marker; box plots per service and
region with an adjustable whisker rule and outlier dots.
</span>
</header>
<div class="row">
<div class="pane">
{#if view === 'chart'}
<SvChart spec={histogram} legend="bottom" tooltipMode="shared" autosize />
{:else}
<ChartDataGrid spec={histogram} />
{/if}
</div>
<div class="pane">
{#if view === 'chart'}
<SvChart spec={boxes} legend="bottom" autosize />
{:else}
<ChartDataGrid spec={boxes} />
{/if}
</div>
</div>
</section>
<style>
.wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 10px;
overflow: auto;
}
.chrome {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
flex: none;
}
.note,
.muted {
font-size: 12px;
color: var(--sg-muted, #64748b);
}
.chk,
.ctl {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--sg-fg, #0f172a);
white-space: nowrap;
}
.ctl select {
font: inherit;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 6px;
background: var(--sg-bg, #fff);
color: inherit;
padding: 2px 6px;
}
.row {
display: flex;
flex: none;
gap: 12px;
flex-wrap: wrap;
}
.pane {
flex: 1 1 420px;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 10px;
background: var(--sg-bg, #fff);
padding: 8px 10px;
min-width: 0;
}
</style>More Charts examples
- Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
- Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
- Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
- Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
- Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.