Funnel Chart
Depict progressive stages in a process, highlighting conversion or drop-off between steps.
Source
LLM docs
Package
npm install @plocks/chartsExamples
6
Properties
47
Playground
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');
},
}}
/>
);
}Related charts
Further reading
React Native design for iOS, Android, and Web.
Quick Links
Resources