Comparison / Intermediate

Funnel Chart

Follow one cohort through successive stages and compare retention.

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 = {
  "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
Sample values used in the chart
StagePeople
Invited1000
Started800
Submitted520
Activated390

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.

01

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.

02

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.

03

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.

04

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.

Edit Chart Data