1 GitHub star = 1 new component.

Star the repo
Charts & Stats chartsvgzero-dependencyhistogramdistributionstatistics

Histogram Chart

Bins raw values into equal-width buckets and shows the distribution with mean and median markers.

Live Preview

Page load time

240 sessions, seconds

1.19s median
0 25 50 75 100 0.61.42.33.13.94.75.66.4
View data
BucketCount
0.6–1.0s81
1.0–1.4s78
1.4–1.8s38
1.8–2.3s25
2.3–2.7s14
2.7–3.1s1
3.1–3.5s0
3.5–3.9s1
3.9–4.3s1
4.3–4.7s0
4.7–5.1s0
5.1–5.6s0
5.6–6.0s0
6.0–6.4s1

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