Distribution / Intermediate

Box Plot

Display statistical distribution with quartiles and outliers.

Back to collection

Interactive Chart Preview

JAVASCRIPT

Custom Drawing: A Chart.js plugin draws the supplied quartiles, median, and whisker endpoints. It does not calculate statistics from raw observations.

Live, interactive exampleChart.js

Chart.js Code Example

JAVASCRIPT
import Chart from 'chart.js/auto';

// Illustrative data used by the live preview.
const chartData = {
  "boxHeaders": [
    "Label",
    "Min",
    "Q1",
    "Median",
    "Q3",
    "Max"
  ],
  "boxSummary": [
    {
      "label": "Product A",
      "min": 5,
      "q1": 15,
      "median": 20,
      "q3": 25,
      "max": 35
    },
    {
      "label": "Product B",
      "min": 8,
      "q1": 20,
      "median": 25,
      "q3": 30,
      "max": 40
    },
    {
      "label": "Product C",
      "min": 3,
      "q1": 10,
      "median": 15,
      "q3": 20,
      "max": 28
    },
    {
      "label": "Product D",
      "min": 12,
      "q1": 23,
      "median": 28,
      "q3": 35,
      "max": 45
    }
  ],
  "datasets": []
};

function createConfig(chartData) {
    const boxes = chartData.boxSummary ?? [];
    const headers = chartData.boxHeaders ?? ['Label', 'Min', 'Q1', 'Median', 'Q3', 'Max'];
    return {
        type: 'scatter',
        data: {
            datasets: [
                {
                    label: headers[0],
                    data: boxes.map((box, x) => ({ x, y: box.median })),
                    pointRadius: 0,
                    pointHitRadius: 25,
                },
            ],
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            scales: {
                x: {
                    title: { display: true, text: headers[0] },
                    min: -0.5,
                    max: boxes.length - 0.5,
                    afterBuildTicks: (axis) => {
                        axis.ticks = boxes.map((_, value) => ({ value }));
                    },
                    ticks: { stepSize: 1, callback: (value) => boxes[Number(value)]?.label ?? '' },
                    grid: { display: false },
                },
                y: {
                    min: Math.min(0, ...boxes.map((box) => box.min)),
                    max: Math.max(0, ...boxes.map((box) => box.max)) || 1,
                    title: { display: true, text: 'Value' },
                },
            },
            plugins: {
                legend: { display: false },
                tooltip: {
                    callbacks: {
                        title: (items) => boxes[items[0].dataIndex].label,
                        label: (item) => {
                            const box = boxes[item.dataIndex];
                            return [
                                `${headers[1]}: ${box.min}`,
                                `${headers[2]}: ${box.q1}`,
                                `${headers[3]}: ${box.median}`,
                                `${headers[4]}: ${box.q3}`,
                                `${headers[5]}: ${box.max}`,
                            ];
                        },
                    },
                },
            },
        },
        // Draw supplied summaries without inferring quartiles from drawing segments.
        plugins: [
            {
                id: 'summary-boxes',
                afterDatasetsDraw(chart) {
                    const { ctx, scales, chartArea } = chart;
                    const halfWidth = Math.min(22, (chartArea.width / boxes.length) * 0.2);
                    ctx.save();
                    ctx.strokeStyle = '#4054e8';
                    ctx.fillStyle = '#d7dfff';
                    ctx.lineWidth = 2;
                    boxes.forEach((box, i) => {
                        const x = scales.x.getPixelForValue(i);
                        const y = (value) => scales.y.getPixelForValue(value);
                        ctx.beginPath();
                        ctx.moveTo(x, y(box.min));
                        ctx.lineTo(x, y(box.max));
                        ctx.moveTo(x - halfWidth / 2, y(box.min));
                        ctx.lineTo(x + halfWidth / 2, y(box.min));
                        ctx.moveTo(x - halfWidth / 2, y(box.max));
                        ctx.lineTo(x + halfWidth / 2, y(box.max));
                        ctx.stroke();
                        ctx.fillRect(x - halfWidth, y(box.q3), halfWidth * 2, y(box.q1) - y(box.q3));
                        ctx.strokeRect(x - halfWidth, y(box.q3), halfWidth * 2, y(box.q1) - y(box.q3));
                        ctx.beginPath();
                        ctx.moveTo(x - halfWidth, y(box.median));
                        ctx.lineTo(x + halfWidth, y(box.median));
                        ctx.stroke();
                    });
                    ctx.restore();
                },
            },
        ],
    };
}

const chart = new Chart(document.getElementById('myChart'), createConfig(chartData));

Preview and code follow the same library selection.

Run This Example

Install the package below in a browser project such as Vite. Put the copied code in a client JavaScript module and add this container to the page before the module runs.

npm install chart.js@4.5.1
<div style="height:320px"><canvas id="myChart"></canvas></div>

The example includes its sample data. When removing the chart, destroy or dispose of its instance and disconnect any resize observer.

Reading This Example

These illustrative summaries compare four products. Product A has minimum 5, first quartile 15, median 20, third quartile 25, and maximum 35. The whiskers mark the supplied minimum and maximum. No sample size or outlier test can be recovered from these five values alone.

All library tabs use these illustrative values. Rendering styles and library APIs differ, so use the table to compare exact numbers.

About this example & sample data
Sample values used in the chart
ProductMinQ1MedianQ3Max
Product A515202535
Product B820253040
Product C310152028
Product D1223283545

READ THE CHART

Compare the Center and Spread

A box plot summarizes a numeric distribution using its median, quartiles, and whiskers. It is useful when comparing several groups, although it hides details such as multiple peaks and the number of observations.

01

Read the Box

The box spans the first to the third quartile, covering the middle portion of the observations. The line inside marks the median. A taller box indicates a larger interquartile range on the same numeric scale.

02

Check the Whisker Rule

Whiskers may use the minimum and maximum or a rule based on the interquartile range. State the convention. Points beyond the whiskers are not automatically errors and should be investigated in context.

03

Keep Groups Comparable

Compare groups measured in the same units and show sample sizes when they differ. A box plot summarizes a distribution, not uncertainty in an estimated mean. Small datasets and different quartile conventions can change its appearance.

04

Use Supplied Summaries Carefully

The Chart.js tab draws boxes with a custom plugin. Every tab uses supplied quartiles and whisker endpoints rather than estimating them from raw observations. Use a histogram when distribution shape matters.

Edit Chart Data