DEV Community

TianYi Zhang
TianYi Zhang

Posted on

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

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

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:

  • How to compute quartiles the right way
  • How to detect outliers with Tukey's fences
  • How to render a box plot as SVG
  • Why all of this matters for real-world data analysis

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 open source too.

Try it now → aiboxplot.com


1. The Five-Number Summary

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.


2. Computing Quartiles Correctly

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]);
}
Enter fullscreen mode Exit fullscreen mode

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.


3. Detecting Outliers with Tukey's Fences

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 };
}
Enter fullscreen mode Exit fullscreen mode

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.


4. Rendering as SVG

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>
  `;
}
Enter fullscreen mode Exit fullscreen mode

The key insight: SVG is just XML. A box plot is just <line>, <rect>, and <circle> elements — no canvas, no external libraries.


5. Putting It All Together

// 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);
Enter fullscreen mode Exit fullscreen mode

Side-by-side comparison instantly reveals:

  • Class A has a higher median and wider spread
  • No outliers in either class
  • Class B scores are more tightly clustered

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


From Demo to Production

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

  • Multi-dataset comparison — stacking boxes vertically with independent colors
  • Notched boxes — 95% confidence intervals around the median (McGill-Tukey-Kramer method)
  • Jitter scatter overlays — showing every raw data point without overlap
  • Responsive scaling — charts that look sharp at any screen size
  • Export to PNG/SVG — for papers, presentations, and reports
  • AI-powered analysis — automatic insights about distribution shape and skewness

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 open source so you can see exactly how every calculation and SVG element is constructed.

Make your first box plot → aiboxplot.com


Key Takeaways

  1. Quartiles matter — use linear interpolation for correct results
  2. Tukey's fences are the standard for outlier detection
  3. SVG is underrated for data visualization — pure XML, no build step
  4. Box plots compress thousands of data points into one clear graphic
  5. All computation can be client-side — your data never needs to leave the browser

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.

Top comments (0)