cd /news/developer-tools/build-a-box-plot-calculator-in-pure-… · home topics developer-tools article
[ARTICLE · art-45316] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

Build a Box Plot Calculator in Pure JavaScript — No Libraries Needed

A developer built a box plot calculator using pure JavaScript and SVG, with no external libraries. The tool computes quartiles via linear interpolation and detects outliers using Tukey's fences. The developer also created aiboxplot.com, a free platform that extends this functionality with AI analysis and multi-dataset comparison.

read5 min views1 publishedJun 30, 2026

A box plot (or box-and-whisker plot) is one of the most powerful tools in exploratory data analysis. In one compact graphic, it reveals the median, spread, skewness, and outliers of a dataset — all from just five numbers.

In this tutorial, I'll walk through building a complete box plot calculator from scratch using vanilla JavaScript and SVG. No D3, no Chart.js, no dependencies. By the end, you'll understand:

If you want to skip the code and use a ready-made tool, I built ** aiboxplot.com** — a free, no-sign-up platform that does all of this plus AI analysis, multi-dataset comparison, and one-click export to PNG/SVG/CSV. The rendering engine is

Try it now →[aiboxplot.com]

Every box plot is built on five values:

Metric Description
Minimum
The smallest value that's not an outlier
Q1 (first quartile)
25th percentile — 25% of data falls below this
Median (Q2)
50th percentile — the middle value
Q3 (third quartile)
75th percentile
Maximum
The largest value that's not an outlier

Plus one more: the Interquartile Range (IQR) = Q3 − Q1. This measures the spread of the middle 50% and is the key to detecting outliers.

The most common mistake in box plot calculators is using the wrong quartile method. There are at least 9 different ways to compute percentiles (see Hyndman & Fan, 1996). I use linear interpolation (method 7), which is the default in Python's NumPy and pandas:

/**
 * Compute a percentile value using linear interpolation.
 * @param {number[]} sorted - Sorted array of numbers
 * @param {number} p - Percentile as a fraction (0.25 = Q1, 0.5 = median, 0.75 = Q3)
 */
function percentile(sorted, p) {
  if (sorted.length === 0) return 0;
  if (sorted.length === 1) return sorted[0];

  const n = sorted.length;
  const h = (n - 1) * p;          // Real-valued index
  const lo = Math.floor(h);
  const hi = Math.ceil(h);

  if (lo === hi) return sorted[lo];

  // Linear interpolation between lo and hi
  return sorted[lo] + (h - lo) * (sorted[hi] - sorted[lo]);
}

Why this matters: for a dataset of [1, 2, 3, 4, 5]

, the median is 3. But for [1, 2, 3, 4]

, using simple rounding gives 2, while linear interpolation correctly gives 2.5. These small differences compound when you're analyzing real data.

John Tukey's fences method is elegant, simple, and still the industry standard:

function calculateStats(data, iqrMultiplier = 1.5) {
  const sorted = [...data].sort((a, b) => a - b);
  const n = sorted.length;
  const mean = sorted.reduce((a, b) => a + b, 0) / n;

  const q1 = percentile(sorted, 0.25);
  const median = percentile(sorted, 0.5);
  const q3 = percentile(sorted, 0.75);
  const iqr = q3 - q1;

  // Tukey's fences
  const lowerFence = q1 - iqrMultiplier * iqr;
  const upperFence = q3 + iqrMultiplier * iqr;

  // Values outside fences are potential outliers
  const outliers = sorted.filter(v => v < lowerFence || v > upperFence);

  // Non-outlier range defines the whiskers
  const nonOutliers = sorted.filter(v => v >= lowerFence && v <= upperFence);
  const min = nonOutliers[0];
  const max = nonOutliers[nonOutliers.length - 1];

  return { min, q1, median, q3, max, iqr, mean, lowerFence, upperFence, outliers, count: n };
}

The 1.5

multiplier is the standard. Use 2.0

for a more conservative detection, or 3.0

for extreme outliers only. At ** aiboxplot.com**, you can switch between these thresholds in real time.

Now for the fun part — turning numbers into a chart. Here's the core SVG rendering logic:

function renderBoxPlot(stats, width = 500, height = 200) {
  const { min, q1, median, q3, max, outliers } = stats;

  // Scale setup
  const padL = 50, padR = 30, padT = 20, padB = 40;
  const plotW = width - padL - padR;
  const plotH = height - padT - padB;

  const dataMin = Math.min(min, ...outliers);
  const dataMax = Math.max(max, ...outliers);
  const range = dataMax - dataMin || 1;
  const paddedMin = dataMin - range * 0.08;
  const paddedMax = dataMax + range * 0.08;
  const paddedRange = paddedMax - paddedMin;

  // Map data value → SVG x-coordinate
  const sx = (v) => padL + plotW * ((v - paddedMin) / paddedRange);

  const boxY = padT + plotH * 0.2;
  const boxH = plotH * 0.6;
  const yc = padT + plotH / 2;
  const color = "#2563eb";

  return `
    <svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
      <!-- Axis -->
      <line x1="${padL}" y1="${padT + plotH}" 
            x2="${padL + plotW}" y2="${padT + plotH}" 
            stroke="#d1d5db" stroke-width="0.5"/>

      <!-- Left whisker -->
      <line x1="${sx(min)}" y1="${yc - boxH / 4}" 
            x2="${sx(min)}" y2="${yc + boxH / 4}" 
            stroke="${color}" stroke-width="1.5" stroke-linecap="round"/>
      <line x1="${sx(min)}" y1="${yc}" 
            x2="${sx(q1)}" y2="${yc}" 
            stroke="${color}" stroke-width="0.5" stroke-dasharray="3 3"/>

      <!-- Box (Q1 to Q3) -->
      <rect x="${sx(q1)}" y="${boxY}" 
            width="${sx(q3) - sx(q1)}" height="${boxH}" 
            fill="${color}" opacity="0.15" stroke="${color}" stroke-width="1.5" rx="2"/>

      <!-- Median line -->
      <line x1="${sx(median)}" y1="${boxY}" 
            x2="${sx(median)}" y2="${boxY + boxH}" 
            stroke="${color}" stroke-width="2" stroke-linecap="round"/>

      <!-- Right whisker -->
      <line x1="${sx(q3)}" y1="${yc}" 
            x2="${sx(max)}" y2="${yc}" 
            stroke="${color}" stroke-width="0.5" stroke-dasharray="3 3"/>
      <line x1="${sx(max)}" y1="${yc - boxH / 4}" 
            x2="${sx(max)}" y2="${yc + boxH / 4}" 
            stroke="${color}" stroke-width="1.5" stroke-linecap="round"/>

      <!-- Outlier dots -->
      ${outliers.map(o => `<circle cx="${sx(o)}" cy="${yc}" r="4" 
            fill="none" stroke="${color}" stroke-width="1.5"/>`).join("")}
    </svg>
  `;
}

The key insight: SVG is just XML. A box plot is just <line>

, <rect>

, and <circle>

elements — no canvas, no external libraries.

// Example: exam scores from two classes
const dataA = [72, 85, 78, 90, 65, 88, 76, 92, 70, 84, 79, 86, 74, 91, 68, 83, 77, 89, 71, 95];
const dataB = [60, 62, 58, 70, 55, 68, 72, 64, 68, 61, 75, 66, 53, 77, 69, 71, 59, 63, 67, 74];

const statsA = calculateStats(dataA);
const statsB = calculateStats(dataB);

console.log("Class A — Median:", statsA.median, "IQR:", statsA.iqr);
console.log("Class B — Median:", statsB.median, "IQR:", statsB.iqr);
console.log("Outliers in A:", statsA.outliers);
console.log("Outliers in B:", statsB.outliers);

document.getElementById("chart-a").innerHTML = renderBoxPlot(statsA);
document.getElementById("chart-b").innerHTML = renderBoxPlot(statsB);

Side-by-side comparison instantly reveals:

This kind of insight takes seconds with a box plot — and pages of text to describe otherwise.

Building a working box plot is straightforward. Building a great one takes more:

I built ** aiboxplot.com** to solve all of this — with 8 chart types, AI chat, and zero sign-up required. The rendering engine behind it is

Make your first box plot →[aiboxplot.com]

Did this help? Let me know in the comments, or show me what you build! And if you want a ready-made solution for your next data project, give aiboxplot.com a try.

── more in #developer-tools 4 stories · sorted by recency
── more on @aiboxplot.com 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/build-a-box-plot-cal…] indexed:0 read:5min 2026-06-30 ·