Distribution / Intermediate
Density Plot
Compare smoothed distributions estimated from individual observations.
Interactive Chart Preview
JAVASCRIPTEdit the column names and values to update your chart. Changes stay in this page until you reload.
| Row# | Observation Group | Individual Value | Remove row |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 | |||
| 4 | |||
| 5 | |||
| 6 | |||
| 7 | |||
| 8 | |||
| 9 | |||
| 10 | |||
| 11 | |||
| 12 | |||
| 13 | |||
| 14 | |||
| 15 | |||
| 16 | |||
| 17 | |||
| 18 | |||
| 19 | |||
| 20 | |||
| 21 | |||
| 22 | |||
| 23 | |||
| 24 |
Enter individual observations, including repeats. Every group needs at least two different values for a positive Scott bandwidth. Curves are normalized separately. New or incomplete groups remain drafts until valid.
Chart.js Code Example
JAVASCRIPTimport 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
| Channel | Minutes |
|---|---|
| 4 | |
| 5 | |
| 6 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 9 | |
| 10 | |
| 12 | |
| 14 | |
| 18 | |
| Chat | 2 |
| Chat | 2 |
| Chat | 3 |
| Chat | 3 |
| Chat | 4 |
| Chat | 4 |
| Chat | 5 |
| Chat | 5 |
| Chat | 6 |
| Chat | 7 |
| Chat | 8 |
| Chat | 10 |
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.
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.
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.
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.
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.
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.