Distribution / Intermediate

Density Plot

Compare smoothed distributions estimated from individual observations.

Back to collection

Interactive Chart Preview

JAVASCRIPT
Live, interactive exampleChart.js

Chart.js Code Example

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

// Illustrative data used by the live preview.
const chartData = {
  "observationHeaders": [
    "Channel",
    "Minutes"
  ],
  "observations": [
    {
      "group": "Email",
      "value": 4
    },
    {
      "group": "Email",
      "value": 5
    },
    {
      "group": "Email",
      "value": 6
    },
    {
      "group": "Email",
      "value": 6
    },
    {
      "group": "Email",
      "value": 7
    },
    {
      "group": "Email",
      "value": 8
    },
    {
      "group": "Email",
      "value": 9
    },
    {
      "group": "Email",
      "value": 9
    },
    {
      "group": "Email",
      "value": 10
    },
    {
      "group": "Email",
      "value": 12
    },
    {
      "group": "Email",
      "value": 14
    },
    {
      "group": "Email",
      "value": 18
    },
    {
      "group": "Chat",
      "value": 2
    },
    {
      "group": "Chat",
      "value": 2
    },
    {
      "group": "Chat",
      "value": 3
    },
    {
      "group": "Chat",
      "value": 3
    },
    {
      "group": "Chat",
      "value": 4
    },
    {
      "group": "Chat",
      "value": 4
    },
    {
      "group": "Chat",
      "value": 5
    },
    {
      "group": "Chat",
      "value": 5
    },
    {
      "group": "Chat",
      "value": 6
    },
    {
      "group": "Chat",
      "value": 7
    },
    {
      "group": "Chat",
      "value": 8
    },
    {
      "group": "Chat",
      "value": 10
    }
  ],
  "datasets": []
};

function createConfig(chartData) {
    const observations = chartData.observations;
    const groups = [...new Set(observations.map(item => item.group))];
    const palette = ['#2563eb', '#0f766e', '#b45309', '#7c3aed', '#be185d'];
    const curves = groups.map((group, index) => {
        const values = observations.filter(item => item.group === group).map(item => item.value);
        const n = values.length, mean = values.reduce((sum, value) => sum + value, 0) / n;
        const h = Math.hypot(...values.map(value => value - mean)) / Math.sqrt(n - 1) * n ** -0.2;
        const lower = Math.min(...values) - 3 * h, upper = Math.max(...values) + 3 * h;
        const points = Array.from({ length: 257 }, (_, point) => {
            const x = lower + (upper - lower) * point / 256;
            const y = values.reduce((sum, value) => sum + Math.exp(-0.5 * ((x - value) / h) ** 2), 0) / (n * h * Math.sqrt(2 * Math.PI));
            return { x, y };
        });
        return { group, n, h, points, color: palette[index % palette.length] };
    });
    return {
        type: 'line', data: { datasets: curves.map(curve => ({ label: `${curve.group} (n=${curve.n})`, data: curve.points, borderColor: curve.color, backgroundColor: curve.color, borderWidth: 2, pointRadius: 0, pointHitRadius: 6, tension: 0 })) },
        options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false },
            scales: { x: { type: 'linear', title: { display: true, text: chartData.observationHeaders[1] } }, y: { min: 0, title: { display: true, text: 'Density' } } },
            plugins: { legend: { position: 'top' }, tooltip: { callbacks: { afterLabel: item => `Scott Bandwidth: ${curves[item.datasetIndex].h.toPrecision(4)}` } } }, },
    };
}

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 response times contain 12 Email and 12 Chat observations. Email ranges from 4 to 18 minutes with a mean of 9 minutes. Chat ranges from 2 to 10 minutes with a mean of about 4.92 minutes. The curves compare their concentration and spread using separate Scott bandwidths; they are not evidence about real channel performance.

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
ChannelMinutes
Email4
Email5
Email6
Email6
Email7
Email8
Email9
Email9
Email10
Email12
Email14
Email18
Chat2
Chat2
Chat3
Chat3
Chat4
Chat4
Chat5
Chat5
Chat6
Chat7
Chat8
Chat10

READ THE CHART

Estimate a Distribution From Observed Values

A density plot smooths individual observations into a curve that shows where values concentrate. It is useful for comparing distribution shapes, but the curve is an estimate whose appearance depends on its bandwidth. Keep the original observations available when interpreting small samples.

01

Start With Individual Observations

Each row contains a group and one measured value. Repeated values remain separate observations with equal weight. Histogram bin counts and box-plot summaries cannot reconstruct these measurements. This editor requires at least two different values in every group; a constant group has no positive bandwidth under the rule used here.

02

Understand the Bandwidth

Every observation contributes a Gaussian kernel. Scott bandwidth is the sample standard deviation multiplied by the group count raised to the power -1/5. Each group gets its own bandwidth. A larger bandwidth produces a smoother curve and can hide multiple peaks. This automatic rule is a starting point, not proof of the population shape.

03

Read Density as Area Over an Interval

Density has inverse measurement units, such as per minute. Height at one value is not the probability of that exact value. Probability corresponds to area across an interval. Each complete estimated curve integrates to one independently, so a taller curve does not mean a larger group. Legends include observation counts to keep sample size visible.

04

Check Tails and Measurement Boundaries

Each example evaluates 257 points from three bandwidths below the group minimum to three above its maximum. The displayed interval leaves small Gaussian tails outside the plot and is not renormalized. Kernels can extend below zero even when the measurements cannot. That tail is a smoothing artifact, not evidence of negative response times. Prefer a histogram or the raw table when smoothing obscures the question.

05

Compare the Same Estimator Across Libraries

All six examples calculate the same Gaussian estimate and evaluation grid. JavaScript libraries draw the calculated numeric lines; the Python versions calculate with NumPy. The Seaborn example uses lineplot for those calculated points instead of asking kdeplot to choose a separate grid. Two observations are enough for the calculation, but usually too few for a reliable distribution estimate.

Edit Chart Data