Basic Box Plot — Chart.js

A box plot (box-and-whisker plot) showing the distribution of numerical data through quartiles. Displays the median, first and third quartiles as a box, with whiskers extending to show the data range. Essential for comparing distributions across categories and identifying outliers.

Basic Box Plot rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// box-basic: Basic Box Plot
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 92/100 | Created: 2026-08-24

//# anyplot-orientation: landscape
const t = window.ANYPLOT_TOKENS;

// --- Deterministic PRNG (LCG) + Box-Muller normal sampling ------------------
let seed = 42;
function lcg() {
  seed = (seed * 1664525 + 1013904223) % 4294967296;
  return seed / 4294967296;
}
function randNormal(mean, sd) {
  const u1 = Math.max(lcg(), 1e-9);
  const u2 = lcg();
  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
  return mean + z * sd;
}
function quantile(sorted, q) {
  const pos = (sorted.length - 1) * q;
  const base = Math.floor(pos);
  const rest = pos - base;
  return sorted[base + 1] !== undefined
    ? sorted[base] + rest * (sorted[base + 1] - sorted[base])
    : sorted[base];
}

// --- Data: exam scores across 5 classes -------------------------------------
const classSpecs = [
  { label: "Class A", mean: 78, sd: 8, n: 60 },
  { label: "Class B", mean: 82, sd: 6, n: 75 },
  { label: "Class C", mean: 70, sd: 10, n: 50 },
  { label: "Class D", mean: 88, sd: 5, n: 90 },
  { label: "Class E", mean: 75, sd: 12, n: 65 },
];

const categories = classSpecs.map((spec) => {
  const scores = Array.from({ length: spec.n }, () =>
    Math.min(100, Math.max(0, randNormal(spec.mean, spec.sd)))
  ).sort((a, b) => a - b);

  const q1 = quantile(scores, 0.25);
  const median = quantile(scores, 0.5);
  const q3 = quantile(scores, 0.75);
  const iqr = q3 - q1;
  const lowerFence = q1 - 1.5 * iqr;
  const upperFence = q3 + 1.5 * iqr;

  const inRange = scores.filter((v) => v >= lowerFence && v <= upperFence);
  const outliers = scores.filter((v) => v < lowerFence || v > upperFence);

  return {
    label: spec.label,
    q1,
    median,
    q3,
    whiskerMin: inRange[0],
    whiskerMax: inRange[inRange.length - 1],
    outliers,
  };
});

const allRangeValues = categories.flatMap((c) => [
  c.whiskerMin,
  c.whiskerMax,
  ...c.outliers,
]);
const dataMin = Math.min(...allRangeValues);
const dataMax = Math.max(...allRangeValues);
const pad = (dataMax - dataMin) * 0.1;
const yMin = Math.floor((dataMin - pad) / 5) * 5;
const yMax = Math.ceil((dataMax + pad) / 5) * 5;

// --- Story highlights: call out the strongest and most variable class -------
const highestMedianCat = categories.reduce((best, c) =>
  c.median > best.median ? c : best
);
const widestSpreadCat = categories.reduce((widest, c) =>
  c.q3 - c.q1 > widest.q3 - widest.q1 ? c : widest
);

// --- Custom plugin: whiskers, caps, median line, outlier points ------------
const boxPlotExtras = {
  id: "boxPlotExtras",
  afterDatasetsDraw(chart) {
    const { ctx } = chart;
    const meta = chart.getDatasetMeta(0);
    const yScale = chart.scales.y;
    ctx.save();
    ctx.lineCap = "round";

    categories.forEach((cat, i) => {
      const bar = meta.data[i];
      if (!bar) return;
      const centerX = bar.x;
      const capHalf = bar.width * 0.3;
      const yWhiskerMin = yScale.getPixelForValue(cat.whiskerMin);
      const yWhiskerMax = yScale.getPixelForValue(cat.whiskerMax);
      const yQ1 = yScale.getPixelForValue(cat.q1);
      const yQ3 = yScale.getPixelForValue(cat.q3);
      const yMedian = yScale.getPixelForValue(cat.median);

      // Whiskers
      ctx.strokeStyle = t.ink;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(centerX, yWhiskerMin);
      ctx.lineTo(centerX, yQ1);
      ctx.moveTo(centerX, yQ3);
      ctx.lineTo(centerX, yWhiskerMax);
      ctx.stroke();

      // Whisker caps
      ctx.beginPath();
      ctx.moveTo(centerX - capHalf, yWhiskerMin);
      ctx.lineTo(centerX + capHalf, yWhiskerMin);
      ctx.moveTo(centerX - capHalf, yWhiskerMax);
      ctx.lineTo(centerX + capHalf, yWhiskerMax);
      ctx.stroke();

      // Median line — page-bg stroke for contrast against the saturated fill
      ctx.strokeStyle = t.pageBg;
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(centerX - bar.width / 2, yMedian);
      ctx.lineTo(centerX + bar.width / 2, yMedian);
      ctx.stroke();

      // Outliers
      const color = t.palette[i % t.palette.length];
      cat.outliers.forEach((value) => {
        const y = yScale.getPixelForValue(value);
        ctx.beginPath();
        ctx.arc(centerX, y, 6, 0, Math.PI * 2);
        ctx.fillStyle = color;
        ctx.fill();
        ctx.lineWidth = 1.5;
        ctx.strokeStyle = t.pageBg;
        ctx.stroke();
      });

      // Story highlight — call out the standout class(es) above its whisker top
      const topValue = Math.max(cat.whiskerMax, ...cat.outliers, -Infinity);
      const isHighest = cat === highestMedianCat;
      const isWidest = cat === widestSpreadCat;
      if (isHighest || isWidest) {
        const label =
          isHighest && isWidest
            ? "Highest median · widest spread"
            : isHighest
              ? "Highest median"
              : "Widest spread (IQR)";
        const yTop = yScale.getPixelForValue(topValue);
        const yLabel = Math.max(yScale.top + 14, yTop - 14);
        ctx.font = "600 13px sans-serif";
        ctx.fillStyle = t.inkSoft;
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.fillText(label, centerX, yLabel);
      }
    });

    ctx.restore();
  },
};

// --- Mount -------------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// --- Chart: floating bars for Q1-Q3, custom plugin for the rest ------------
new Chart(canvas, {
  type: "bar",
  data: {
    labels: categories.map((c) => c.label),
    datasets: [
      {
        label: "Interquartile range",
        data: categories.map((c) => [c.q1, c.q3]),
        backgroundColor: categories.map((_, i) => t.palette[i % t.palette.length]),
        borderColor: t.ink,
        borderWidth: 2,
        borderRadius: 4,
        barPercentage: 0.45,
        categoryPercentage: 0.75,
      },
    ],
  },
  plugins: [boxPlotExtras],
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    plugins: {
      title: {
        display: true,
        text: "box-basic · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 26 },
      },
      legend: { display: false },
    },
    scales: {
      x: {
        title: { display: true, text: "Class", color: t.ink, font: { size: 16 } },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { display: false },
      },
      y: {
        min: yMin,
        max: yMax,
        title: {
          display: true,
          text: "Exam Score (0-100 pts)",
          color: t.ink,
          font: { size: 16 },
        },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { color: t.grid },
      },
    },
  },
});

Part of Basic Box Plot on anyplot.ai.

Other implementations