Comparison / Intermediate
Funnel Chart
Follow one cohort through successive stages and compare retention.
Interactive Chart Preview
JAVASCRIPTEdit the column names and values to update your chart. Changes stay in this page until you reload.
| Row# | Ordered Stage | Whole Count | Remove row |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 | |||
| 4 |
Use distinct stage names and whole nonnegative counts for one cohort. Counts must stay equal or decrease in table order. Fill every cell. A zero denominator has no percentage; many stages are easier to inspect in the table.
Chart.js Code Example
JAVASCRIPTimport Chart from 'chart.js/auto';
// Illustrative data used by the live preview.
const chartData = {
"categoryLabel": "Stage",
"labels": [
"Invited",
"Started",
"Submitted",
"Activated"
],
"datasets": [
{
"label": "People",
"data": [
1000,
800,
520,
390
],
"backgroundColor": "#3b82f6",
"borderColor": "#3b82f6"
}
]
};
function createConfig(chartData) {
const labels = chartData.labels, counts = chartData.datasets[0].data;
const measure = chartData.datasets[0].label;
const maximum = counts[0] || 1;
const percentage = (value, total) => total > 0 ? `${(value / total * 100).toFixed(1)}%` : 'Undefined (zero denominator)';
const retention = counts.map((count, row) => [percentage(count, counts[0]), row === 0 ? 'Not applicable' : percentage(count, counts[row - 1])]);
return {
type: 'bar', data: { labels, datasets: [{ label: measure, data: counts.map(count => [-count / 2, count / 2]), backgroundColor: '#3b82f6', barPercentage: 0.9, categoryPercentage: 0.85 }] },
plugins: [{ id: 'stage-counts', afterDatasetsDraw: chart => {
const ctx = chart.ctx;
ctx.save();
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
counts.forEach((count, row) => {
const x = chart.scales.x.getPixelForValue(0), y = chart.scales.y.getPixelForValue(row);
const text = String(count), width = ctx.measureText(text).width + 8;
ctx.fillStyle = '#ffffff';
ctx.fillRect(x - width / 2, y - 9, width, 18);
ctx.fillStyle = '#1e293b';
ctx.fillText(text, x, y);
});
ctx.restore();
} }],
options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { display: false }, tooltip: { callbacks: { label: item => [
`${measure}: ${counts[item.dataIndex]}`, `Of First: ${retention[item.dataIndex][0]}`, `Of Previous: ${retention[item.dataIndex][1]}`,
] } } },
scales: { x: { min: -maximum * 0.55, max: maximum * 0.55, display: false }, y: { grid: { display: false }, title: { display: true, text: chartData.categoryLabel } } },
},
};
}
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
This illustrative onboarding cohort starts with 1,000 invited people. Of those, 800 start, 520 submit, and 390 activate. Activation is 39% of the initial cohort. The consecutive transitions retain 80%, 65%, and 75%, so Started to Submitted has the lowest previous-stage retention. The four counts are not additive parts.
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
| Stage | People |
|---|---|
| Invited | 1000 |
| Started | 800 |
| Submitted | 520 |
| Activated | 390 |
READ THE CHART
Follow the Same Cohort Through Each Stage
A funnel chart compares how many members of a cohort reach successive stages. Width represents the supplied count. Each later stage is a subset of the earlier one in this example, so the counts stay level or decrease as you read from top to bottom.
Keep Stage Order Meaningful
The chart follows table order without sorting by value. Enter whole nonnegative counts for the same cohort and observation window. An increase is an invalid draft here because it breaks the subset assumption. Use a bar chart for independent stage snapshots or a process that gains new entrants along the way.
Name the Percentage Denominator
Interactive tooltips report retention against the first stage and against the previous stage separately. The first compares overall progress; the second isolates a transition. The first stage has no previous-stage percentage. Any percentage with a zero denominator is undefined, including an all-zero funnel.
Do Not Add Stage Counts
A person can appear in several stages, so adding their counts does not give the cohort size. The initial stage is the overall denominator. A smaller width shows fewer people reaching that stage, but the funnel does not explain why they stopped or how long each step took.
Read Widths and Labels, Not Polygon Areas
Most examples use centered rectangular stages. ECharts uses native tapered sections, and Plotly may connect adjacent stages. Those polygon areas are not proportional to counts. Compare the stage labels and width encoding. Zero stages have no width but remain in the data, and a single stage has no transition to compare.