Charts & Stats chartsvgzero-dependencyhistogramdistributionstatistics
Histogram Chart
Bins raw values into equal-width buckets and shows the distribution with mean and median markers.
Live Preview
240 sessions, seconds
1.19s median
View data
| Bucket | Count |
|---|---|
| 0.6–1.0s | 81 |
| 1.0–1.4s | 78 |
| 1.4–1.8s | 38 |
| 1.8–2.3s | 25 |
| 2.3–2.7s | 14 |
| 2.7–3.1s | 1 |
| 3.1–3.5s | 0 |
| 3.5–3.9s | 1 |
| 3.9–4.3s | 1 |
| 4.3–4.7s | 0 |
| 4.7–5.1s | 0 |
| 5.1–5.6s | 0 |
| 5.6–6.0s | 0 |
| 6.0–6.4s | 1 |
Usage
Copy and paste this code into your Astro page:
astro
---
import { HistogramChart } from 'astro-component-kit/charts';
---
<HistogramChart values={loadTimes} bins={12} unit="s" showStats /> ---
import { HistogramChart } from 'astro-component-kit/charts';
---
<HistogramChart values={loadTimes} bins={12} unit="s" showStats /> Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/lib/HistogramChart.astro:
astro
---
/**
* HistogramChart — Bins raw values into equal-width buckets and draws the
* distribution, with optional mean and median markers.
*
* @param {number[]} values - Raw observations.
* @param {number} bins - Number of buckets.
* @param {boolean} showStats - Draw mean & median reference lines.
* @param {string} unit - Unit suffix for bucket edges.
* @param {string} color - Bar 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, sum } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
values?: number[];
bins?: number;
showStats?: boolean;
unit?: string;
height?: number;
color?: string;
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
// Deterministic sample: a right-skewed page-load distribution.
const sample = Array.from({ length: 240 }, (_, i) => {
const a = Math.sin(i * 12.9898) * 43758.5453;
const b = Math.sin(i * 78.233) * 12345.678;
const u = a - Math.floor(a);
const v = b - Math.floor(b);
return Math.round((0.6 + Math.abs(Math.sqrt(-2 * Math.log(u + 1e-9)) * Math.cos(2 * Math.PI * v)) * 0.9) * 100) / 100;
});
const {
title = 'Page load time',
subtitle = '240 sessions, seconds',
values = sample,
bins = 14,
showStats = true,
unit = 's',
height = 260,
color = 'var(--chart-1, #3987e5)',
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const W = 640;
const H = height;
const m = { t: 22, r: 12, b: 30, l: 40 };
const sorted = [...values].sort((a, b) => a - b);
const lo = sorted[0];
const hi = sorted[sorted.length - 1];
const width = (hi - lo) / bins || 1;
const counts = Array.from({ length: bins }, () => 0);
values.forEach((v) => {
counts[Math.min(bins - 1, Math.floor((v - lo) / width))] += 1;
});
const yTicks = niceTicks(0, Math.max(...counts), 4);
const y = scaleLinear(0, yTicks[yTicks.length - 1], H - m.b, m.t);
const x = scaleLinear(lo, hi, m.l, W - m.r);
const mean = sum(values) / values.length;
const median = quantile(sorted, 0.5);
const edge = (i: number) => (lo + i * width).toFixed(1);
---
<ChartFrame
title={title}
subtitle={subtitle}
value={`${median.toFixed(2)}${unit}`}
delta="median"
variant={variant}
size={size}
className={className}
table={{ headers: ['Bucket', 'Count'], rows: counts.map((c, i) => [`${edge(i)}–${edge(i + 1)}${unit}`, c]) }}
>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: histogram of ${values.length} values, median ${median.toFixed(2)}${unit}`}>
{yTicks.map((t) => (
<g>
<line class={t === 0 ? 'cf-axis' : '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">{t}</text>
</g>
))}
{counts.map((c, i) => {
const bx = x(lo + i * width) + 1;
const bw = x(lo + (i + 1) * width) - x(lo + i * width) - 2;
return (
<rect class="cf-mark cf-dim" x={bx} y={y(c)} width={Math.max(1, bw)} height={Math.max(0, y(0) - y(c))} rx="2" fill={color}
tabindex="0" role="img" aria-label={`${edge(i)} to ${edge(i + 1)}${unit}: ${c} values`}
data-tip-title={`${edge(i)}–${edge(i + 1)}${unit}`} data-tip={`${color}::${c} values · ${((c / values.length) * 100).toFixed(1)}%`} />
);
})}
{Array.from({ length: bins + 1 }, (_, i) => i).filter((i) => i % 2 === 0).map((i) => (
<text class="cf-label" x={x(lo + i * width)} y={H - 10} text-anchor="middle">{edge(i)}</text>
))}
{showStats && (
<g aria-hidden="true">
<line x1={x(median)} x2={x(median)} y1={m.t - 6} y2={H - m.b} stroke="var(--c-text-1, #fff)" stroke-width="1.5" />
<text class="cf-label cf-label--strong" x={x(median) + 5} y={m.t}>Median</text>
<line x1={x(mean)} x2={x(mean)} y1={m.t + 10} y2={H - m.b} stroke="var(--chart-2, #d95926)" stroke-width="1.5" />
<text class="cf-label" x={x(mean) + 5} y={m.t + 16}>Mean {mean.toFixed(2)}{unit}</text>
</g>
)}
</svg>
</ChartFrame> ---
/**
* HistogramChart — Bins raw values into equal-width buckets and draws the
* distribution, with optional mean and median markers.
*
* @param {number[]} values - Raw observations.
* @param {number} bins - Number of buckets.
* @param {boolean} showStats - Draw mean & median reference lines.
* @param {string} unit - Unit suffix for bucket edges.
* @param {string} color - Bar 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, sum } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
values?: number[];
bins?: number;
showStats?: boolean;
unit?: string;
height?: number;
color?: string;
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
// Deterministic sample: a right-skewed page-load distribution.
const sample = Array.from({ length: 240 }, (_, i) => {
const a = Math.sin(i * 12.9898) * 43758.5453;
const b = Math.sin(i * 78.233) * 12345.678;
const u = a - Math.floor(a);
const v = b - Math.floor(b);
return Math.round((0.6 + Math.abs(Math.sqrt(-2 * Math.log(u + 1e-9)) * Math.cos(2 * Math.PI * v)) * 0.9) * 100) / 100;
});
const {
title = 'Page load time',
subtitle = '240 sessions, seconds',
values = sample,
bins = 14,
showStats = true,
unit = 's',
height = 260,
color = 'var(--chart-1, #3987e5)',
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const W = 640;
const H = height;
const m = { t: 22, r: 12, b: 30, l: 40 };
const sorted = [...values].sort((a, b) => a - b);
const lo = sorted[0];
const hi = sorted[sorted.length - 1];
const width = (hi - lo) / bins || 1;
const counts = Array.from({ length: bins }, () => 0);
values.forEach((v) => {
counts[Math.min(bins - 1, Math.floor((v - lo) / width))] += 1;
});
const yTicks = niceTicks(0, Math.max(...counts), 4);
const y = scaleLinear(0, yTicks[yTicks.length - 1], H - m.b, m.t);
const x = scaleLinear(lo, hi, m.l, W - m.r);
const mean = sum(values) / values.length;
const median = quantile(sorted, 0.5);
const edge = (i: number) => (lo + i * width).toFixed(1);
---
<ChartFrame
title={title}
subtitle={subtitle}
value={`${median.toFixed(2)}${unit}`}
delta="median"
variant={variant}
size={size}
className={className}
table={{ headers: ['Bucket', 'Count'], rows: counts.map((c, i) => [`${edge(i)}–${edge(i + 1)}${unit}`, c]) }}
>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: histogram of ${values.length} values, median ${median.toFixed(2)}${unit}`}>
{yTicks.map((t) => (
<g>
<line class={t === 0 ? 'cf-axis' : '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">{t}</text>
</g>
))}
{counts.map((c, i) => {
const bx = x(lo + i * width) + 1;
const bw = x(lo + (i + 1) * width) - x(lo + i * width) - 2;
return (
<rect class="cf-mark cf-dim" x={bx} y={y(c)} width={Math.max(1, bw)} height={Math.max(0, y(0) - y(c))} rx="2" fill={color}
tabindex="0" role="img" aria-label={`${edge(i)} to ${edge(i + 1)}${unit}: ${c} values`}
data-tip-title={`${edge(i)}–${edge(i + 1)}${unit}`} data-tip={`${color}::${c} values · ${((c / values.length) * 100).toFixed(1)}%`} />
);
})}
{Array.from({ length: bins + 1 }, (_, i) => i).filter((i) => i % 2 === 0).map((i) => (
<text class="cf-label" x={x(lo + i * width)} y={H - 10} text-anchor="middle">{edge(i)}</text>
))}
{showStats && (
<g aria-hidden="true">
<line x1={x(median)} x2={x(median)} y1={m.t - 6} y2={H - m.b} stroke="var(--c-text-1, #fff)" stroke-width="1.5" />
<text class="cf-label cf-label--strong" x={x(median) + 5} y={m.t}>Median</text>
<line x1={x(mean)} x2={x(mean)} y1={m.t + 10} y2={H - m.b} stroke="var(--chart-2, #d95926)" stroke-width="1.5" />
<text class="cf-label" x={x(mean) + 5} y={m.t + 16}>Mean {mean.toFixed(2)}{unit}</text>
</g>
)}
</svg>
</ChartFrame>
Quick Info
- Category
- Charts & Stats
- Filename
HistogramChart.astro- Dependencies
- apexcharts (peer)
- Tags
- chartsvgzero-dependencyhistogramdistributionstatistics