Charts & Stats chartsvgzero-dependencybox-plotdistributionstatisticsquartiles
Box Plot Chart
Distribution summary per group with median, IQR box, whiskers and outliers computed from raw data.
Live Preview
Distribution of p50 latency, ms
View data
| Group | Min | Q1 | Median | Q3 | Max | Outliers |
|---|---|---|---|---|---|---|
| us-east | 54ms | 80ms | 89ms | 99ms | 110ms | 2 |
| us-west | 57ms | 85ms | 97ms | 103ms | 126ms | 2 |
| eu-central | 45ms | 61ms | 69ms | 76ms | 96ms | 2 |
| ap-south | 73ms | 124ms | 139ms | 159ms | 191ms | 0 |
| sa-east | 115ms | 146ms | 156ms | 174ms | 206ms | 0 |
Usage
Copy and paste this code into your Astro page:
astro
---
import { BoxPlotChart } from 'astro-component-kit/charts';
---
<BoxPlotChart
groups={[
{ label: 'us-east', values: [62, 70, 81, 85, 90, 120] },
{ label: 'eu-west', values: [55, 60, 66, 71, 79, 160] },
]}
suffix="ms"
/> ---
import { BoxPlotChart } from 'astro-component-kit/charts';
---
<BoxPlotChart
groups={[
{ label: 'us-east', values: [62, 70, 81, 85, 90, 120] },
{ label: 'eu-west', values: [55, 60, 66, 71, 79, 160] },
]}
suffix="ms"
/> Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/lib/BoxPlotChart.astro:
astro
---
/**
* BoxPlotChart — Distribution summary per group: median, interquartile box,
* 1.5×IQR whiskers and outlier dots. Computes stats from raw values.
*
* @param {Array<{label: string, values: number[]}>} groups - Raw observations per group.
* @param {string} suffix - Value suffix.
* @param {string} color - Box color.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
import ChartFrame from './ChartFrame.astro';
import { scaleLinear, niceTicks, quantile } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
groups?: Array<{ label: string; values: number[] }>;
suffix?: string;
height?: number;
color?: string;
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const rand = (seed: number, n: number, mean: number, spread: number) =>
Array.from({ length: n }, (_, i) => {
const a = Math.sin((i + 1) * seed) * 9999;
const b = Math.sin((i + 3) * seed * 1.7) * 9999;
return Math.round(mean + ((a - Math.floor(a)) + (b - Math.floor(b)) - 1) * spread);
});
const {
title = 'Response time by region',
subtitle = 'Distribution of p50 latency, ms',
groups = [
{ label: 'us-east', values: [...rand(1.3, 40, 82, 40), 190] },
{ label: 'us-west', values: rand(2.7, 40, 95, 45) },
{ label: 'eu-central', values: [...rand(4.1, 40, 70, 30), 160, 172] },
{ label: 'ap-south', values: rand(5.9, 40, 140, 70) },
{ label: 'sa-east', values: rand(8.3, 40, 160, 55) },
],
suffix = 'ms',
height = 290,
color = 'var(--chart-1, #3987e5)',
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const stats = groups.map((g) => {
const s = [...g.values].sort((a, b) => a - b);
const q1 = quantile(s, 0.25);
const med = quantile(s, 0.5);
const q3 = quantile(s, 0.75);
const iqr = q3 - q1;
const lo = s.find((v) => v >= q1 - 1.5 * iqr) ?? s[0];
const hi = [...s].reverse().find((v) => v <= q3 + 1.5 * iqr) ?? s[s.length - 1];
const outliers = s.filter((v) => v < lo || v > hi);
return { label: g.label, q1, med, q3, lo, hi, outliers, n: s.length };
});
const W = 640;
const H = height;
const m = { t: 14, r: 12, b: 30, l: 48 };
const allV = groups.flatMap((g) => g.values);
const ticks = niceTicks(Math.min(...allV), Math.max(...allV), 4);
const y = scaleLinear(ticks[0], ticks[ticks.length - 1], H - m.b, m.t);
const band = (W - m.l - m.r) / groups.length;
const bw = Math.min(56, band * 0.45);
const f = (v: number) => `${Math.round(v)}${suffix}`;
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
table={{ headers: ['Group', 'Min', 'Q1', 'Median', 'Q3', 'Max', 'Outliers'], rows: stats.map((s) => [s.label, f(s.lo), f(s.q1), f(s.med), f(s.q3), f(s.hi), s.outliers.length]) }}
>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: box plot of ${groups.length} groups`}>
{ticks.map((t) => (
<g>
<line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
<text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{f(t)}</text>
</g>
))}
{stats.map((s, i) => {
const cx = m.l + i * band + band / 2;
return (
<g class="cf-mark cf-dim" tabindex="0" role="img"
aria-label={`${s.label}: median ${f(s.med)}, IQR ${f(s.q1)}–${f(s.q3)}, range ${f(s.lo)}–${f(s.hi)}, ${s.outliers.length} outliers`}
data-tip-title={s.label} data-tip={`Max: ${f(s.hi)}|Q3: ${f(s.q3)}|${color}::Median: ${f(s.med)}|Q1: ${f(s.q1)}|Min: ${f(s.lo)}${s.outliers.length ? `|Outliers: ${s.outliers.length}` : ''}`}>
<rect x={cx - band / 2} y={m.t} width={band} height={H - m.t - m.b} fill="transparent" />
<line x1={cx} x2={cx} y1={y(s.hi)} y2={y(s.q3)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<line x1={cx} x2={cx} y1={y(s.q1)} y2={y(s.lo)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<line x1={cx - bw / 4} x2={cx + bw / 4} y1={y(s.hi)} y2={y(s.hi)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<line x1={cx - bw / 4} x2={cx + bw / 4} y1={y(s.lo)} y2={y(s.lo)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<rect x={cx - bw / 2} y={y(s.q3)} width={bw} height={Math.max(1, y(s.q1) - y(s.q3))} rx="4" fill={color} fill-opacity="0.3" stroke={color} stroke-width="1.5" />
<line x1={cx - bw / 2} x2={cx + bw / 2} y1={y(s.med)} y2={y(s.med)} stroke="var(--c-text-1, #fff)" stroke-width="2.5" />
{s.outliers.map((o) => <circle cx={cx} cy={y(o)} r="3.5" fill="none" stroke={color} stroke-width="1.5" />)}
<text class="cf-label" x={cx} y={H - 10} text-anchor="middle">{s.label}</text>
</g>
);
})}
</svg>
</ChartFrame> ---
/**
* BoxPlotChart — Distribution summary per group: median, interquartile box,
* 1.5×IQR whiskers and outlier dots. Computes stats from raw values.
*
* @param {Array<{label: string, values: number[]}>} groups - Raw observations per group.
* @param {string} suffix - Value suffix.
* @param {string} color - Box color.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
import ChartFrame from './ChartFrame.astro';
import { scaleLinear, niceTicks, quantile } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
groups?: Array<{ label: string; values: number[] }>;
suffix?: string;
height?: number;
color?: string;
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const rand = (seed: number, n: number, mean: number, spread: number) =>
Array.from({ length: n }, (_, i) => {
const a = Math.sin((i + 1) * seed) * 9999;
const b = Math.sin((i + 3) * seed * 1.7) * 9999;
return Math.round(mean + ((a - Math.floor(a)) + (b - Math.floor(b)) - 1) * spread);
});
const {
title = 'Response time by region',
subtitle = 'Distribution of p50 latency, ms',
groups = [
{ label: 'us-east', values: [...rand(1.3, 40, 82, 40), 190] },
{ label: 'us-west', values: rand(2.7, 40, 95, 45) },
{ label: 'eu-central', values: [...rand(4.1, 40, 70, 30), 160, 172] },
{ label: 'ap-south', values: rand(5.9, 40, 140, 70) },
{ label: 'sa-east', values: rand(8.3, 40, 160, 55) },
],
suffix = 'ms',
height = 290,
color = 'var(--chart-1, #3987e5)',
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const stats = groups.map((g) => {
const s = [...g.values].sort((a, b) => a - b);
const q1 = quantile(s, 0.25);
const med = quantile(s, 0.5);
const q3 = quantile(s, 0.75);
const iqr = q3 - q1;
const lo = s.find((v) => v >= q1 - 1.5 * iqr) ?? s[0];
const hi = [...s].reverse().find((v) => v <= q3 + 1.5 * iqr) ?? s[s.length - 1];
const outliers = s.filter((v) => v < lo || v > hi);
return { label: g.label, q1, med, q3, lo, hi, outliers, n: s.length };
});
const W = 640;
const H = height;
const m = { t: 14, r: 12, b: 30, l: 48 };
const allV = groups.flatMap((g) => g.values);
const ticks = niceTicks(Math.min(...allV), Math.max(...allV), 4);
const y = scaleLinear(ticks[0], ticks[ticks.length - 1], H - m.b, m.t);
const band = (W - m.l - m.r) / groups.length;
const bw = Math.min(56, band * 0.45);
const f = (v: number) => `${Math.round(v)}${suffix}`;
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
table={{ headers: ['Group', 'Min', 'Q1', 'Median', 'Q3', 'Max', 'Outliers'], rows: stats.map((s) => [s.label, f(s.lo), f(s.q1), f(s.med), f(s.q3), f(s.hi), s.outliers.length]) }}
>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: box plot of ${groups.length} groups`}>
{ticks.map((t) => (
<g>
<line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
<text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{f(t)}</text>
</g>
))}
{stats.map((s, i) => {
const cx = m.l + i * band + band / 2;
return (
<g class="cf-mark cf-dim" tabindex="0" role="img"
aria-label={`${s.label}: median ${f(s.med)}, IQR ${f(s.q1)}–${f(s.q3)}, range ${f(s.lo)}–${f(s.hi)}, ${s.outliers.length} outliers`}
data-tip-title={s.label} data-tip={`Max: ${f(s.hi)}|Q3: ${f(s.q3)}|${color}::Median: ${f(s.med)}|Q1: ${f(s.q1)}|Min: ${f(s.lo)}${s.outliers.length ? `|Outliers: ${s.outliers.length}` : ''}`}>
<rect x={cx - band / 2} y={m.t} width={band} height={H - m.t - m.b} fill="transparent" />
<line x1={cx} x2={cx} y1={y(s.hi)} y2={y(s.q3)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<line x1={cx} x2={cx} y1={y(s.q1)} y2={y(s.lo)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<line x1={cx - bw / 4} x2={cx + bw / 4} y1={y(s.hi)} y2={y(s.hi)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<line x1={cx - bw / 4} x2={cx + bw / 4} y1={y(s.lo)} y2={y(s.lo)} stroke="var(--c-text-2, #a3a3a3)" stroke-width="1.5" />
<rect x={cx - bw / 2} y={y(s.q3)} width={bw} height={Math.max(1, y(s.q1) - y(s.q3))} rx="4" fill={color} fill-opacity="0.3" stroke={color} stroke-width="1.5" />
<line x1={cx - bw / 2} x2={cx + bw / 2} y1={y(s.med)} y2={y(s.med)} stroke="var(--c-text-1, #fff)" stroke-width="2.5" />
{s.outliers.map((o) => <circle cx={cx} cy={y(o)} r="3.5" fill="none" stroke={color} stroke-width="1.5" />)}
<text class="cf-label" x={cx} y={H - 10} text-anchor="middle">{s.label}</text>
</g>
);
})}
</svg>
</ChartFrame>
Quick Info
- Category
- Charts & Stats
- Filename
BoxPlotChart.astro- Dependencies
- apexcharts (peer)
- Tags
- chartsvgzero-dependencybox-plotdistributionstatisticsquartiles