Funnel Each step's drop is the finding, so the step-to-step percentage matters more than the absolute width. A funnel that only shows totals hides which stage is actually broken. Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --status-warn, --chart-1 The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // Funnel.tsx ──────────────────────────────────────────────────────────────────────── import { cn } from "@/lib/utils"; /** A step is a name and how many of the cohort reached it. Both percentages * are computed here, so the chart can never show one without the other. */ export type Step = { name: string; count: number }; type Props = { /** In sequence. The first step is the population everything is measured against. */ steps: Step[]; /** The conversion window, printed under the chart. Somebody who started * Tuesday and paid Friday either counts or does not, and the chart has to * say which. */ window: string; barWidth?: number; }; const pct = (n: number) => `${Math.round(n * 100)}%`; const fmt = new Intl.NumberFormat("en-GB"); export function Funnel({ steps, window, barWidth = 160 }: Props) { const base = steps[0]?.count ?? 0; const rows = steps.map((s, i) => ({ ...s, ofAll: s.count / base, fromLast: i === 0 ? null : s.count / steps[i - 1].count, })); // The finding is the biggest single drop, and it gets the only colour. const worst = rows.reduce((w, r, i) => (r.fromLast !== null && (w < 0 || r.fromLast < rows[w].fromLast!) ? i : w), -1); return (
Step From last Of all
    {rows.map((r, i) => (
  1. {r.name} {/* Length, never area. A taper reads worse for no gain but the metaphor. */} {r.fromLast === null ? "—" : pct(r.fromLast)} {pct(r.ofAll)}
  2. ))}

{fmt.format(base)} people, {window}

); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { Funnel } from "./Funnel"; /** * Five steps from a checkout. Ten thousand viewed, 1,083 paid. The counts are * chosen so the two percentages land on the drawing's: 41, 53, 55 and 91 from * the step before, 11% of everyone at the end. */ export default function Demo() { return ( ); }