Retention stat cards plus a weekly cohort heat grid built from token-colored cells.
Install with the CLI, or add the packages and paste the source below.
Add Analytics to your app with its source-owned dependencies.
npx cronus-ui add analytics
Install the Cronus UI packages, then paste the block below.
Copy the source for Engagement cohorts. Every class is a semantic token, so it re-themes with your app.
import {Card,CardAction,CardContent,CardDescription,CardFooter,CardHeader,CardTitle,Metric,MetricDelta,MetricLabel,MetricValue,Select,SelectContent,SelectItem,SelectTrigger,SelectValue,Sparkline,} from "@cronus-ui/ui";interface RetentionStat {label: string;value: string;delta: string;trend: "up" | "down";tone: "primary" | "success" | "info";series: number[];}interface CohortRow {id: string;label: string;users: string;cells: number[];week8: string;}const retentionStats: RetentionStat[] = [{ label: "Day-1 retention", value: "68.4%", delta: "+3.1%", trend: "up", tone: "success", series: [61, 62, 64, 63, 65, 66, 67, 68] },{ label: "Day-7 retention", value: "42.9%", delta: "+1.8%", trend: "up", tone: "primary", series: [38, 39, 41, 40, 41, 42, 42, 43] },{ label: "Day-30 retention", value: "27.6%", delta: "-0.4%", trend: "down", tone: "info", series: [29, 28, 29, 28, 28, 27, 28, 27] },];const cohortWeeks = ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"];const cohorts: CohortRow[] = [{ id: "apr-06", label: "Apr 6", users: "2,418", cells: [4, 3, 3, 2, 2, 2, 1, 1], week8: "31%" },{ id: "apr-13", label: "Apr 13", users: "2,733", cells: [4, 3, 3, 3, 2, 2, 2, 1], week8: "33%" },{ id: "apr-20", label: "Apr 20", users: "2,204", cells: [4, 4, 3, 3, 2, 2, 2, 2], week8: "36%" },{ id: "apr-27", label: "Apr 27", users: "3,012", cells: [4, 4, 3, 3, 3, 2, 2, 2], week8: "38%" },{ id: "may-04", label: "May 4", users: "2,890", cells: [4, 4, 3, 3, 3, 3, 2, 2], week8: "41%" },{ id: "may-11", label: "May 11", users: "3,247", cells: [4, 4, 4, 3, 3, 3, 3, 2], week8: "44%" },];const heatLevels = ["bg-surface-inset","bg-primary/15","bg-primary/35","bg-primary/60","bg-primary",];function heatCellClass(level: number) {return "h-7 rounded-sm " + (heatLevels[level] ?? "bg-surface-inset");}const cohortGridClass = "grid grid-cols-[5rem_repeat(8,minmax(0,1fr))_4rem] items-center gap-1.5";export function AnalyticsEngagementBlock() {return (<sectionaria-label="Engagement analytics"className="mx-auto flex w-full max-w-5xl flex-col gap-4"><div className="grid gap-4 sm:grid-cols-3">{retentionStats.map((stat) => (<Card key={stat.label} className="gap-0 py-5"><CardContent className="flex items-end justify-between gap-3"><Metric className="gap-1.5"><MetricLabel>{stat.label}</MetricLabel><MetricValue className="text-2xl">{stat.value}</MetricValue><MetricDelta trend={stat.trend}>{stat.delta}</MetricDelta></Metric><Sparklinedata={stat.series}type="line"areatone={stat.tone}width={96}height={40}className="h-10 w-24 shrink-0"aria-label={stat.label + " trend over the last 8 weeks"}/></CardContent></Card>))}</div><Card className="gap-0"><CardHeader><CardTitle className="font-display text-base">Weekly cohort retention</CardTitle><CardDescription>Share of each signup cohort active in the weeks after joining.</CardDescription><CardAction><Select defaultValue="all"><SelectTrigger className="w-44" aria-label="Filter by plan"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="all">All workspaces</SelectItem><SelectItem value="paid">Paid workspaces</SelectItem><SelectItem value="free">Free workspaces</SelectItem></SelectContent></Select></CardAction></CardHeader><CardContent className="overflow-x-auto pt-5"><div className="flex min-w-[34rem] flex-col gap-1.5"><div className={cohortGridClass + " pb-1"}><span className="text-xs font-medium text-fg-tertiary">Cohort</span>{cohortWeeks.map((week) => (<span key={week} className="text-center text-xs font-medium text-fg-tertiary">{week}</span>))}<span className="text-end text-xs font-medium text-fg-tertiary">Retained</span></div>{cohorts.map((cohort) => (<div key={cohort.id} className={cohortGridClass}><div className="flex flex-col"><span className="text-xs font-medium text-fg">{cohort.label}</span><span className="text-xs text-fg-tertiary">{cohort.users}</span></div>{cohort.cells.map((level, week) => (<div// biome-ignore lint/suspicious/noArrayIndexKey: static, never-reordered week bucketskey={cohort.id + "-w" + (week + 1)}className={heatCellClass(level)}aria-hidden="true"/>))}<span className="text-end text-xs font-medium tabular-nums text-fg-secondary">{cohort.week8}</span></div>))}</div></CardContent><CardFooter className="flex-wrap justify-between gap-3 pt-5"><span className="text-xs text-fg-tertiary">Each row follows one weekly signup cohort through its first 8 weeks.</span><div className="flex items-center gap-1.5"><span className="text-xs text-fg-tertiary">Less</span><span className="size-3 rounded-sm bg-surface-inset" aria-hidden="true" /><span className="size-3 rounded-sm bg-primary/15" aria-hidden="true" /><span className="size-3 rounded-sm bg-primary/35" aria-hidden="true" /><span className="size-3 rounded-sm bg-primary/60" aria-hidden="true" /><span className="size-3 rounded-sm bg-primary" aria-hidden="true" /><span className="text-xs text-fg-tertiary">More</span></div></CardFooter></Card></section>);}