Funnel Chart

Depict progressive stages in a process, highlighting conversion or drop-off between steps.

npm install @plocks/charts

Basics

Shows progressive reduction of data through stages (conversion pipeline).

Loading demo…

import { FunnelChart, formatCompactNumber } from '@plocks/charts'; import { SALES_FUNNEL } from './data'; export function Demo() { return ( <FunnelChart title="Product acquisition funnel" maw={420} h={420} series={SALES_FUNNEL} layout={{ shape: 'trapezoid', gap: 8, showConversion: false, align: 'center', connectors: { show: false }, }} valueFormatter={(value) => formatCompactNumber(value)} legend={{ show: false }} tooltip={{ show: true, formatter: (step) => `${step.label}: ${step.value.toLocaleString()}`, }} /> ); }

Data Pipeline Quality

Loading demo…

import { FunnelChart, formatCompactNumber } from '@plocks/charts'; import { PIPELINE_QUALITY, PipelineMeta } from './data'; export function Demo() { return ( <FunnelChart title="Data pipeline quality checks" subtitle="From ingestion to certified datasets" maw={520} h={440} series={PIPELINE_QUALITY} layout={{ shape: 'trapezoid', gap: 8, align: 'center', showConversion: false, connectors: { show: false }, }} valueFormatter={(value) => formatCompactNumber(value)} legend={{ show: false }} tooltip={{ show: true, formatter: (step) => { const idx = PIPELINE_QUALITY.steps.findIndex((candidate) => candidate.label === step.label); const previous = idx > 0 ? PIPELINE_QUALITY.steps[idx - 1] : undefined; const dropValue = previous ? previous.value - step.value : 0; const dropRate = previous && previous.value > 0 ? (dropValue / previous.value) * 100 : 0; const meta = step.meta as PipelineMeta | undefined; return [ step.label, `${step.value.toLocaleString()} rows`, previous ? `Filtered: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)` : 'Ingestion baseline', meta?.note, ] .filter(Boolean) .join('\n'); }, }} /> ); }

Ecommerce Checkout Payments

Loading demo…

import { FunnelChart, formatCompactNumber } from '@plocks/charts'; import { CHECKOUT_FUNNEL, CheckoutMeta } from './data'; const formatPaymentSplit = (split: CheckoutMeta['paymentSplit']) => { if (!split) return undefined; return `Payment mix: ${Math.round(split.stripe * 100)}% Stripe • ${Math.round(split.paypal * 100)}% PayPal • ${Math.round(split.bnpl * 100)}% BNPL`; }; export function Demo() { return ( <FunnelChart title="Ecommerce checkout conversion" subtitle="Drop-off by stage" maw={520} h={440} series={CHECKOUT_FUNNEL} layout={{ shape: 'trapezoid', gap: 8, align: 'center', showConversion: false, connectors: { show: false }, }} valueFormatter={(value) => formatCompactNumber(value)} legend={{ show: false }} tooltip={{ show: true, formatter: (step) => { const idx = CHECKOUT_FUNNEL.steps.findIndex((candidate) => candidate.label === step.label); const previous = idx > 0 ? CHECKOUT_FUNNEL.steps[idx - 1] : undefined; const dropValue = previous ? previous.value - step.value : 0; const dropRate = previous && previous.value > 0 ? (dropValue / previous.value) * 100 : 0; const meta = step.meta as CheckoutMeta | undefined; const paymentSplit = formatPaymentSplit(meta?.paymentSplit); return [ step.label, `${step.value.toLocaleString()} sessions`, previous ? `Drop: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)` : 'Entry point', meta?.insight, paymentSplit, ] .filter(Boolean) .join('\n'); }, }} /> ); }

Hiring Funnel Role Family

Loading demo…

import { FunnelChart, formatCompactNumber } from '@plocks/charts'; import { HIRING_SERIES, HiringMeta, STEP_LOOKUP } from './data'; const findSeriesContext = (step: unknown) => STEP_LOOKUP.get(step as any) ?? null; export function Demo() { return ( <FunnelChart title="Hiring funnel — Staff engineer" subtitle="External candidates vs. internal transfers" maw={620} h={480} series={HIRING_SERIES} layout={{ shape: 'bar', gap: 10, align: 'center', showConversion: false, seriesMode: 'grouped', connectors: { show: false }, }} valueFormatter={(value) => formatCompactNumber(value)} tooltip={{ show: true, formatter: (step) => { const lookup = findSeriesContext(step as any) ?? undefined; if (!lookup) { return `${step.label}: ${step.value.toLocaleString()} candidates`; } const series = lookup.series; const stepIndex = lookup.stepIndex; const previous = stepIndex > 0 ? series.steps[stepIndex - 1] : undefined; const dropValue = previous ? previous.value - step.value : 0; const dropRate = previous && previous.value > 0 ? (dropValue / previous.value) * 100 : 0; const meta = step.meta as HiringMeta | undefined; return [ `${step.label} • ${series.name}`, `${step.value.toLocaleString()} candidates`, previous ? `Drop: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)` : 'Pipeline intake', meta?.medianDays != null ? `Median time in stage: ${meta.medianDays} days` : undefined, meta?.topDeclineReason ? `Top decline reason: ${meta.topDeclineReason}` : undefined, ] .filter(Boolean) .join('\n'); }, }} /> ); }

Incident Response Workflow

Loading demo…

import { FunnelChart, formatCompactNumber } from '@plocks/charts'; import { INCIDENT_RESPONSE, IncidentMeta } from './data'; export function Demo() { return ( <FunnelChart title="Incident response workflow" subtitle="Volume flowing through each stage" maw={520} h={460} series={INCIDENT_RESPONSE} layout={{ shape: 'trapezoid', gap: 8, align: 'center', showConversion: false, connectors: { show: false }, }} valueFormatter={(value) => formatCompactNumber(value)} legend={{ show: false }} tooltip={{ show: true, formatter: (step) => { const idx = INCIDENT_RESPONSE.steps.findIndex((candidate) => candidate.label === step.label); const previous = idx > 0 ? INCIDENT_RESPONSE.steps[idx - 1] : undefined; const dropValue = previous ? previous.value - step.value : 0; const dropRate = previous && previous.value > 0 ? (dropValue / previous.value) * 100 : 0; const meta = step.meta as IncidentMeta | undefined; return [ `${step.label}`, `${step.value.toLocaleString()} incidents`, meta?.medianDuration, previous ? `Drop since prior: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)` : 'Start of workflow', meta?.automationWin ? `Automation impact: ${meta.automationWin}` : undefined, ] .filter(Boolean) .join('\n'); }, }} /> ); }

Saas Trial Conversion

Loading demo…

import { FunnelChart, formatCompactNumber } from '@plocks/charts'; import { TRIAL_CONVERSION, TrialMeta } from './data'; export function Demo() { return ( <FunnelChart title="SaaS trial-to-paid conversion" subtitle="Retention from sign-up to paid" maw={520} h={440} series={TRIAL_CONVERSION} layout={{ shape: 'trapezoid', gap: 8, align: 'center', showConversion: false, connectors: { show: false }, }} valueFormatter={(value) => formatCompactNumber(value)} legend={{ show: false }} tooltip={{ show: true, formatter: (step) => { const index = TRIAL_CONVERSION.steps.findIndex((candidate) => candidate.label === step.label); const previous = index > 0 ? TRIAL_CONVERSION.steps[index - 1] : undefined; const dropValue = previous ? previous.value - step.value : 0; const dropRate = previous && previous.value > 0 ? (dropValue / previous.value) * 100 : 0; const reason = (step.meta as TrialMeta | undefined)?.dropReason; return [ `${step.label}`, `${step.value.toLocaleString()} accounts`, previous ? `Drop: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)` : 'Starting cohort', reason ? `Top reason: ${reason}` : undefined, ] .filter(Boolean) .join('\n'); }, }} /> ); }
Scan to open this page on your phoneplocks.dev/charts/FunnelChart

Related charts