Subscription Churn Analytics
blockanalyticsChartMogul and Baremetrics style SaaS subscription churn and MRR analytics dashboard. Features 4 key SaaS metrics cards (Net MRR growth, logo churn rate, net revenue churn, and LTV/CAC), an MRR movement waterfall decomposition bridge, a churn reasons distribution table with lost ARR attribution and trend indicators, and automated churn prevention playbook quick actions.
Installation
$pnpm dlx uipkge-ng@latest add subscription-churn-analytics$npx uipkge-ng@latest add subscription-churn-analytics$yarn dlx uipkge-ng@latest add subscription-churn-analytics$bunx uipkge-ng@latest add subscription-churn-analyticsManifest:
https://uipkge.dev/r/angular/subscription-churn-analytics.jsonInstalls to:src/app/components/blocks/subscription-churn-analytics/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ChurnPrimaryMetricinterface ChurnPrimaryMetric {
id: string
label: string
value: string
unit: string
badge: string
badgeVariant: Extract<BadgeVariant, 'success' | 'outline' | 'secondary'>
badgeIcon: LucideIconData
description: string
}MrrMovementinterface MrrMovement {
id: string
label: string
amount: string
impact: 'positive' | 'warning' | 'negative' | 'info'
share: string
count: string
colorClass: string
badgeClass: string
barClass: string
icon: LucideIconData
}ChurnReasoninterface ChurnReason {
id: string
reason: string
category: string
share: number
lostArr: string
accounts: number
trend: 'Increasing' | 'Decreasing'
trendDelta: string
trendVariant: Extract<BadgeVariant, 'destructive' | 'success' | 'warning'>
}ChurnQuickActioninterface ChurnQuickAction {
id: string
title: string
description: string
impactBadge: string
buttonText: string
buttonVariant: Extract<ButtonVariant, 'default' | 'outline'>
icon: LucideIconData
}Files installed (2)
src/app/components/blocks/subscription-churn-analytics/subscription-churn-analytics.component.ts23.9 kB
import { Component, Input, signal, ChangeDetectionStrategy } from '@angular/core' import { AlertCircle, ArrowDownRight, ArrowUpRight, Calendar, CheckCircle2, DollarSign, Download, LucideAngularModule, PhoneCall, RefreshCw, RotateCcw, TrendingDown, TrendingUp, UserMinus, UserPlus, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component' import { UiButtonComponent, type ButtonVariant } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface ChurnPrimaryMetric { id: string label: string value: string unit: string badge: string badgeVariant: Extract<BadgeVariant, 'success' | 'outline' | 'secondary'> badgeIcon: LucideIconData description: string } export interface MrrMovement { id: string label: string amount: string impact: 'positive' | 'warning' | 'negative' | 'info' share: string count: string colorClass: string badgeClass: string barClass: string icon: LucideIconData } export interface ChurnReason { id: string reason: string category: string share: number lostArr: string accounts: number trend: 'Increasing' | 'Decreasing' trendDelta: string trendVariant: Extract<BadgeVariant, 'destructive' | 'success' | 'warning'> } export interface ChurnQuickAction { id: string title: string description: string impactBadge: string buttonText: string buttonVariant: Extract<ButtonVariant, 'default' | 'outline'> icon: LucideIconData } const primaryMetrics: ChurnPrimaryMetric[] = [ { id: 'net-mrr', label: 'Net MRR Growth', value: '+$12,480', unit: '/ mo', badge: '+8.4%', badgeVariant: 'success', badgeIcon: TrendingUp, description: 'Gross additions outpacing cancellations by 6.8x', }, { id: 'user-churn', label: 'User Churn Rate', value: '1.8%', unit: '/ mo', badge: 'Below 2% benchmark', badgeVariant: 'success', badgeIcon: CheckCircle2, description: 'Logo churn down 0.4% from previous quarter', }, { id: 'revenue-churn', label: 'Revenue Churn Rate', value: '0.9%', unit: '/ mo', badge: 'Negative Net Churn', badgeVariant: 'outline', badgeIcon: TrendingDown, description: 'Expansion ($6,450) exceeds total churn ($2,170)', }, { id: 'ltv', label: 'Customer Lifetime Value', value: '$4,850.00', unit: '', badge: 'CAC Payback 4.2 mo', badgeVariant: 'secondary', badgeIcon: DollarSign, description: '3.4x LTV to CAC ratio across active cohort', }, ] const mrrWaterfall: MrrMovement[] = [ { id: 'new', label: 'New Subscriptions', amount: '+$8,200.00', impact: 'positive', share: '52.9%', count: '38 new accounts', colorClass: 'text-success', badgeClass: 'bg-success/10 text-success border-success/20', barClass: 'bg-success', icon: UserPlus, }, { id: 'expansion', label: 'Expansion / Upgrades', amount: '+$6,450.00', impact: 'positive', share: '41.6%', count: '24 accounts expanded', colorClass: 'text-success', badgeClass: 'bg-success/10 text-success border-success/20', barClass: 'bg-success/80', icon: ArrowUpRight, }, { id: 'contraction', label: 'Contraction / Downgrades', amount: '-$1,120.00', impact: 'warning', share: '7.2%', count: '9 tier downgrades', colorClass: 'text-warning', badgeClass: 'bg-warning/10 text-warning border-warning/20', barClass: 'bg-warning', icon: ArrowDownRight, }, { id: 'churn', label: 'Churn / Cancellations', amount: '-$1,050.00', impact: 'negative', share: '6.8%', count: '7 accounts lost', colorClass: 'text-destructive', badgeClass: 'bg-destructive/10 text-destructive border-destructive/20', barClass: 'bg-destructive', icon: UserMinus, }, { id: 'reactivations', label: 'Reactivations', amount: '+$850.00', impact: 'info', share: '5.5%', count: '4 win-back returns', colorClass: 'text-info', badgeClass: 'bg-info/10 text-info border-info/20', barClass: 'bg-info bg-info', icon: RefreshCw, }, ] const churnReasons: ChurnReason[] = [ { id: 'competitor', reason: 'Switched to competitor', category: 'Product Fit / Competition', share: 38, lostArr: '-$24,800.00', accounts: 19, trend: 'Increasing', trendDelta: '+12% QoQ', trendVariant: 'destructive', }, { id: 'budget', reason: 'Budget constraints', category: 'Economic / Spend Freeze', share: 28, lostArr: '-$18,300.00', accounts: 14, trend: 'Decreasing', trendDelta: '-8% QoQ', trendVariant: 'success', }, { id: 'missing-sso', reason: 'Missing feature: SAML SSO', category: 'Security / Compliance', share: 21, lostArr: '-$13,750.00', accounts: 8, trend: 'Decreasing', trendDelta: '-15% QoQ', trendVariant: 'success', }, { id: 'project-ended', reason: 'Project ended', category: 'Seasonal / Lifecycle', share: 13, lostArr: '-$8,500.00', accounts: 6, trend: 'Increasing', trendDelta: '+4% QoQ', trendVariant: 'warning', }, ] const quickActions: ChurnQuickAction[] = [ { id: 'winback', title: 'Trigger Discount Win-Back Campaign', description: 'Dispatch automated 20% annual renewal incentives to 25 accounts canceled within the last 45 days.', impactBadge: 'Est. $4,800 MRR recovered', buttonText: 'Trigger Win-Back', buttonVariant: 'default', icon: RotateCcw, }, { id: 'interviews', title: 'Schedule Exit Discovery Interviews', description: 'Book 15-minute product alignment calls with churned enterprise accounts ($10k+ ARR) for feature feedback.', impactBadge: '3 enterprise accounts flagged', buttonText: 'Schedule Interviews', buttonVariant: 'outline', icon: PhoneCall, }, { id: 'health-alerts', title: 'Configure Churn Risk Telemetry Alerts', description: 'Automate high-priority notifications when weekly active seat engagement drops below 40% threshold.', impactBadge: '12 accounts in risk zone', buttonText: 'Configure Triggers', buttonVariant: 'outline', icon: AlertCircle, }, ] /** * ChartMogul / Baremetrics style SaaS subscription churn and MRR analytics dashboard: four * SaaS metric cards, the MRR movement waterfall bridge, the churn reason / lost ARR table and * the churn prevention playbooks. Angular port of the React `SubscriptionChurnAnalytics` * block (same markup, classes, copy and sample data). */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-subscription-churn-analytics, [ui-subscription-churn-analytics]', standalone: true, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], host: { '[attr.data-uipkge]': '""', '[attr.data-slot]': '"subscription-churn-analytics"', '[class]': 'hostClass', }, template: ` <!-- Header Section --> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> Subscription Churn & MRR Analytics </h2> <span ui-badge variant="outline" class="border-border text-muted-foreground hidden font-mono text-xs sm:inline-flex" > SaaS Metrics </span> </div> <p class="text-muted-foreground text-sm"> Track recurring revenue dynamics, logo retention cohorts, and cancellation root causes. </p> </div> <div class="flex flex-wrap items-center gap-3"> <div class="w-44"> <ui-select [value]="selectedRange()" (valueChange)="selectedRange.set($event)"> <button ui-select-trigger size="sm" class="w-full"> <!-- [class]: a static class on lucide-icon lands on the host and the svg, doubling mr-1. --> <lucide-icon [img]="Calendar" [class]="'text-muted-foreground mr-1 size-3.5'" aria-hidden="true" /> <ui-select-value placeholder="Select period" /> </button> <ui-select-content> <ui-select-item value="30d">Last 30 Days</ui-select-item> <ui-select-item value="90d">Last 90 Days</ui-select-item> <ui-select-item value="12m">Last 12 Months</ui-select-item> <ui-select-item value="ytd">Year to Date</ui-select-item> </ui-select-content> </ui-select> </div> <button ui-button aria-label="Download attachment" size="sm" variant="outline" class="gap-2 shadow-xs"> <lucide-icon [img]="Download" class="text-muted-foreground size-4" aria-hidden="true" /> <span>Export Retention Report</span> </button> </div> </div> <!-- 4 Primary SaaS Metrics Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> @for (metric of primaryMetrics; track metric.id) { <ui-card class="border-border bg-card hover:border-border/80 shadow-xs transition-colors"> <ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ metric.label }} </ui-card-title> <span ui-badge [variant]="metric.badgeVariant" class="gap-1 font-medium tabular-nums"> <lucide-icon [img]="metric.badgeIcon" class="size-3" aria-hidden="true" /> <span>{{ metric.badge }}</span> </span> </div> </ui-card-header> <ui-card-content class="space-y-1 pb-4"> <div class="flex items-baseline gap-1"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl"> {{ metric.value }} </span> @if (metric.unit) { <span class="text-muted-foreground text-xs font-normal">{{ metric.unit }}</span> } </div> <p class="text-muted-foreground text-xs leading-relaxed">{{ metric.description }}</p> </ui-card-content> </ui-card> } </div> <!-- MRR Movement Waterfall Breakdown Card --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-header class="pb-4"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <ui-card-title class="text-foreground text-base font-semibold" >MRR Movement Waterfall Breakdown</ui-card-title > <span ui-badge variant="outline" class="border-border bg-muted/30 text-xs font-medium"> ChartMogul Bridge </span> </div> <ui-card-description class="mt-1 text-xs"> Decomposition of new customer revenue, expansion, contractions, logo churn, and reactivations. </ui-card-description> </div> <div class="border-border/80 bg-muted/20 flex items-center gap-2 self-start rounded-lg border px-3 py-1.5 sm:self-auto" > <span class="text-muted-foreground text-xs font-medium">Net MRR Change:</span> <span class="text-success text-sm font-bold tabular-nums">+$13,330.00</span> <span ui-badge variant="success" class="ml-1 text-xs">+9.3%</span> </div> </div> </ui-card-header> <ui-card-content class="space-y-6"> <!-- Visual Waterfall Proportional Bar --> <div class="space-y-2"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span> Starting MRR: <strong class="text-foreground font-semibold tabular-nums">$142,500.00</strong> </span> <span> Ending MRR: <strong class="text-foreground font-semibold tabular-nums">$155,830.00</strong> </span> </div> <div class="bg-muted/60 flex h-3 w-full overflow-hidden rounded-full p-0.5 shadow-inner"> <div class="bg-success h-full w-[48%] rounded-l-full transition-[width] duration-300" title="New Subscriptions (+$8,200.00)" ></div> <div class="bg-success/90 h-full w-[38%] transition-[width] duration-300" title="Expansion (+$6,450.00)" ></div> <div class="bg-info h-full w-[5%] transition-[width] duration-300" title="Reactivations (+$850.00)"></div> <div class="bg-warning h-full w-[5%] transition-[width] duration-300" title="Contraction (-$1,120.00)" ></div> <div class="bg-destructive h-full w-[4%] rounded-r-full transition-[width] duration-300" title="Churn (-$1,050.00)" ></div> </div> <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 pt-1 text-xs"> <div class="flex items-center gap-1.5"> <span class="bg-success inline-block size-2 rounded-full"></span> <span>New (+$8.2k)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-success inline-block size-2 rounded-full"></span> <span>Expansion (+$6.45k)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-info inline-block size-2 rounded-full"></span> <span>Reactivation (+$850)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-warning inline-block size-2 rounded-full"></span> <span>Contraction (-$1.12k)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-destructive inline-block size-2 rounded-full"></span> <span>Churn (-$1.05k)</span> </div> </div> </div> <ui-separator class="border-border" /> <!-- Detailed Waterfall Grid --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> @for (item of mrrWaterfall; track item.id) { <div class="border-border bg-card/60 hover:bg-muted/20 flex items-start justify-between rounded-lg border p-3.5 shadow-xs transition-colors" > <div class="flex items-start gap-3"> <div [class]=" cn( 'border-border flex size-9 shrink-0 items-center justify-center rounded-md border shadow-xs', item.badgeClass ) " > <lucide-icon [img]="item.icon" class="size-4" aria-hidden="true" /> </div> <div class="space-y-0.5"> <p class="text-foreground text-xs font-semibold">{{ item.label }}</p> <p class="text-muted-foreground text-xs">{{ item.count }}</p> </div> </div> <div class="text-right"> <p [class]="cn('text-sm font-bold tabular-nums', item.colorClass)">{{ item.amount }}</p> <span class="text-muted-foreground text-xs tabular-nums">{{ item.share }} gross</span> </div> </div> } <!-- Net Summary Tile --> <div class="border-success/30 bg-success/5 flex items-start justify-between rounded-lg border p-3.5 shadow-xs" > <div class="flex items-start gap-3"> <div class="border-success/30 bg-success/15 text-success flex size-9 shrink-0 items-center justify-center rounded-md border shadow-xs" > <lucide-icon [img]="TrendingUp" class="size-4" aria-hidden="true" /> </div> <div class="space-y-0.5"> <p class="text-foreground text-xs font-semibold">Net MRR Movement</p> <p class="text-muted-foreground text-xs">All streams combined</p> </div> </div> <div class="text-right"> <p class="text-success text-sm font-bold tabular-nums">+$13,330.00</p> <span ui-badge variant="success" class="text-xs tabular-nums">Net Positive</span> </div> </div> </div> </ui-card-content> </ui-card> <!-- Churn Reason Distribution Table --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-header class="pb-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <ui-card-title class="text-foreground text-base font-semibold"> Churn Reason Distribution & Lost ARR Attribution </ui-card-title> <ui-card-description class="mt-1 text-xs"> Primary cancellation drivers captured via post-cancellation exit surveys and customer success logs. </ui-card-description> </div> <div class="flex items-center gap-2"> <span class="text-muted-foreground text-xs">Total Lost ARR:</span> <span class="text-destructive text-sm font-bold tabular-nums">-$65,350.00</span> </div> </div> </ui-card-header> <ui-card-content class="p-0"> <div class="overflow-x-auto"> <div ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="border-border hover:bg-transparent"> <th ui-table-head class="text-foreground text-xs font-semibold">Cancellation Reason</th> <th ui-table-head class="text-foreground text-xs font-semibold">Distribution Share</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Lost ARR ($)</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Impacted Accounts</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Quarterly Trend</th> </tr> </thead> <tbody ui-table-body> @for (row of churnReasons; track row.id) { <tr ui-table-row class="border-border hover:bg-muted/30 transition-colors"> <td ui-table-cell class="py-3.5"> <div class="text-foreground text-sm font-medium">{{ row.reason }}</div> <div class="text-muted-foreground text-xs">{{ row.category }}</div> </td> <td ui-table-cell class="min-w-[180px] py-3.5"> <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium tabular-nums">{{ row.share }}%</span> <span class="text-muted-foreground text-xs">of total churn</span> </div> <ui-progress [value]="row.share" class="bg-muted h-1.5" /> </div> </td> <td ui-table-cell class="text-foreground py-3.5 text-right text-sm font-semibold tabular-nums"> {{ row.lostArr }} </td> <td ui-table-cell class="text-muted-foreground py-3.5 text-right text-xs tabular-nums"> <span class="text-foreground font-medium">{{ row.accounts }}</span> accounts </td> <td ui-table-cell class="py-3.5 text-right"> <span ui-badge [variant]="row.trendVariant" class="gap-1 text-xs font-medium tabular-nums"> <span>{{ row.trend === 'Increasing' ? '↑' : '↓' }}</span> <span>{{ row.trend }}</span> <span class="opacity-75">({{ row.trendDelta }})</span> </span> </td> </tr> } </tbody> </div> </div> </ui-card-content> </ui-card> <!-- Churn Prevention Quick Actions --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-header class="pb-3"> <div class="flex items-center gap-2"> <ui-card-title class="text-foreground text-base font-semibold"> Churn Prevention & Win-Back Quick Actions </ui-card-title> <span ui-badge variant="secondary" class="text-xs">Playbooks</span> </div> <ui-card-description class="text-xs"> High-leverage intervention workflows to stem revenue loss and re-engage churned customer cohorts. </ui-card-description> </ui-card-header> <ui-card-content> <div class="grid grid-cols-1 gap-4 md:grid-cols-3"> @for (action of quickActions; track action.id) { <div class="border-border bg-card/60 hover:border-border/80 hover:bg-muted/10 flex flex-col justify-between rounded-lg border p-4 shadow-xs transition-colors" > <div class="space-y-3"> <div class="flex items-center justify-between"> <div class="border-border bg-muted/40 text-foreground flex size-9 items-center justify-center rounded-md border shadow-xs" > <lucide-icon [img]="action.icon" class="size-4" aria-hidden="true" /> </div> <span ui-badge variant="outline" class="border-border text-muted-foreground font-mono text-xs"> {{ action.impactBadge }} </span> </div> <div class="space-y-1"> <h4 class="text-foreground text-sm leading-snug font-semibold">{{ action.title }}</h4> <p class="text-muted-foreground text-xs leading-relaxed">{{ action.description }}</p> </div> </div> <div class="mt-4 pt-2"> <button ui-button [variant]="action.buttonVariant" size="sm" class="w-full gap-2 shadow-xs"> <lucide-icon [img]="action.icon" class="size-3.5" aria-hidden="true" /> <span>{{ action.buttonText }}</span> </button> </div> </div> } </div> </ui-card-content> </ui-card> `, }) export class UiSubscriptionChurnAnalyticsComponent { @Input('class') className?: string protected readonly cn = cn protected readonly primaryMetrics = primaryMetrics protected readonly mrrWaterfall = mrrWaterfall protected readonly churnReasons = churnReasons protected readonly quickActions = quickActions protected readonly Calendar = Calendar protected readonly Download = Download protected readonly TrendingUp = TrendingUp readonly selectedRange = signal('90d') get hostClass(): string { return cn('block w-full space-y-6', this.className) } }src/app/components/blocks/subscription-churn-analytics/index.ts0.2 kB
export { UiSubscriptionChurnAnalyticsComponent, type ChurnPrimaryMetric, type ChurnQuickAction, type ChurnReason, type MrrMovement, } from './subscription-churn-analytics.component'
Raw manifest:https://uipkge.dev/r/angular/subscription-churn-analytics.json