Column Profiling Summary
blockdevopsPandas Profiling and ydata style exploratory data analysis (EDA) column profiling summary with dataset metrics, distribution histograms, quantile box plots, and categorical frequency breakdowns.
Installation
$pnpm dlx uipkge-ng@latest add column-profiling-summary$npx uipkge-ng@latest add column-profiling-summary$yarn dlx uipkge-ng@latest add column-profiling-summary$bunx uipkge-ng@latest add column-profiling-summaryManifest:
https://uipkge.dev/r/angular/column-profiling-summary.jsonInstalls to:src/app/components/blocks/column-profiling-summary/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.
ColumnProfilingHistogramBininterface ColumnProfilingHistogramBin {
bin: string
count: number
pct: number
height: number
}ColumnProfilingTierinterface ColumnProfilingTier {
name: string
count: number
pct: number
colorClass: string
badgeClass: string
}ColumnProfilingSchemaColumninterface ColumnProfilingSchemaColumn {
name: string
type: string
category: string
missing: string
distinct: string
summary: string
status: string
}Files installed (2)
src/app/components/blocks/column-profiling-summary/column-profiling-summary.component.ts55.7 kB
import { Component, Input, signal, computed, ChangeDetectionStrategy } from '@angular/core' import { AlertCircle, BarChart2, Binary, Check, CheckCircle2, Copy, Database, Download, Hash, LayoutGrid, LucideAngularModule, Search, Table as TableIcon, Tag, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type ColumnProfilingTypeFilter = 'all' | 'numeric' | 'categorical' | 'boolean' export type ColumnProfilingViewMode = 'cards' | 'table' export interface ColumnProfilingHistogramBin { bin: string count: number pct: number height: number } export interface ColumnProfilingTier { name: string count: number pct: number colorClass: string badgeClass: string } export interface ColumnProfilingSchemaColumn { name: string type: string category: string missing: string distinct: string summary: string status: string } // Column 1: monthly_spend_usd (Numeric Float64) const monthlySpendStats = { name: 'monthly_spend_usd', type: 'Numeric Float64', category: 'numeric', min: '$0.00', mean: '$142.50', median: '$120.00', max: '$2,450.00', stdDev: '48.2', iqr: '$115.00', zerosCount: 1420, zerosPct: '0.96%', missingCount: 0, missingPct: '0.0%', distinctCount: '12,410', distinctPct: '8.37%', memory: '1.19 MB', } const histogramBins: ColumnProfilingHistogramBin[] = [ { bin: '$0 - $245', count: 58400, pct: 39.4, height: 95 }, { bin: '$245 - $490', count: 42100, pct: 28.4, height: 68 }, { bin: '$490 - $735', count: 22500, pct: 15.2, height: 36 }, { bin: '$735 - $980', count: 12800, pct: 8.6, height: 21 }, { bin: '$980 - $1,225', count: 6400, pct: 4.3, height: 10 }, { bin: '$1,225 - $1,470', count: 3100, pct: 2.1, height: 5 }, { bin: '$1,470 - $1,715', count: 1650, pct: 1.1, height: 3 }, { bin: '$1,715 - $1,960', count: 840, pct: 0.6, height: 2 }, { bin: '$1,960 - $2,205', count: 380, pct: 0.3, height: 1 }, { bin: '$2,205 - $2,450', count: 120, pct: 0.1, height: 1 }, ] // Column 2: subscription_tier (Categorical) const subscriptionTierStats = { name: 'subscription_tier', type: 'Categorical', category: 'categorical', distinctCount: 4, missingCount: 0, missingPct: '0.0%', memory: '1.19 MB', topValue: 'Starter', topFreqPct: '45.0%', } const subscriptionTiers: ColumnProfilingTier[] = [ { name: 'Starter', count: 66730, pct: 45.0, colorClass: 'bg-primary', badgeClass: 'border-primary/30 text-primary' }, { name: 'Pro', count: 51901, pct: 35.0, colorClass: 'bg-success', badgeClass: 'border-success/30 text-success' }, { name: 'Enterprise', count: 22243, pct: 15.0, colorClass: 'bg-info', badgeClass: 'border-info/30 text-info' }, { name: 'Custom', count: 7416, pct: 5.0, colorClass: 'bg-warning', badgeClass: 'border-warning/30 text-warning' }, ] // Column 3: account_age_days (Numeric Int32) const accountAgeStats = { name: 'account_age_days', type: 'Numeric Int32', category: 'numeric', min: '1d', p25: '42d', p50: '180d', p75: '365d', p95: '580d', p99: '720d', max: '850d', iqr: '323d', mean: '214.6d', stdDev: '142.1', missingCount: 42, missingPct: '0.03%', distinctCount: '820', distinctPct: '0.55%', memory: '593.2 KB', } // Column 4: has_cancelled (Boolean) const hasCancelledStats = { name: 'has_cancelled', type: 'Boolean', category: 'boolean', missingCount: 0, missingPct: '0.0%', distinctCount: 2, memory: '144.8 KB', trueCount: 21057, truePct: 14.2, falseCount: 127233, falsePct: 85.8, } // Schema overview for compact table view const allSchemaColumns: ColumnProfilingSchemaColumn[] = [ { name: 'monthly_spend_usd', type: 'Numeric Float64', category: 'numeric', missing: '0 (0.0%)', distinct: '12,410 (8.4%)', summary: 'Mean: $142.50 · Median: $120.00 · Max: $2,450.00', status: 'Clean', }, { name: 'subscription_tier', type: 'Categorical', category: 'categorical', missing: '0 (0.0%)', distinct: '4 values', summary: 'Starter (45%) > Pro (35%) > Enterprise (15%)', status: 'Clean', }, { name: 'account_age_days', type: 'Numeric Int32', category: 'numeric', missing: '42 (0.03%)', distinct: '820 (0.6%)', summary: 'P25: 42d · P50: 180d · P75: 365d · P99: 720d', status: '99.97% Complete', }, { name: 'has_cancelled', type: 'Boolean', category: 'boolean', missing: '0 (0.0%)', distinct: '2 values', summary: 'False: 85.8% (127.2k) · True: 14.2% (21.1k)', status: 'Clean', }, { name: 'customer_id', type: 'Categorical UUID', category: 'categorical', missing: '0 (0.0%)', distinct: '148,290 (100%)', summary: 'Primary Key · Unique identifier', status: 'Unique Index', }, { name: 'support_tickets_count', type: 'Numeric Int16', category: 'numeric', missing: '0 (0.0%)', distinct: '18 values', summary: 'Mean: 1.4 · Median: 1.0 · Max: 16.0', status: 'Clean', }, { name: 'last_login_epoch', type: 'Numeric Int64', category: 'numeric', missing: '112 (0.08%)', distinct: '114,200', summary: 'Min: 1770000000 · Max: 1787313600', status: 'Clean', }, { name: 'signup_channel', type: 'Categorical', category: 'categorical', missing: '439 (0.30%)', distinct: '7 values', summary: 'Organic (42%) > Google Ads (28%) > Partner (16%)', status: 'Clean', }, ] const spendMetrics = [ { label: 'Minimum', value: monthlySpendStats.min }, { label: 'Mean (Avg)', value: monthlySpendStats.mean }, { label: 'Median (P50)', value: monthlySpendStats.median }, { label: 'Maximum', value: monthlySpendStats.max }, { label: 'Std. Deviation', value: monthlySpendStats.stdDev }, { label: 'Interquartile (IQR)', value: monthlySpendStats.iqr }, ] const typeFilters: { value: ColumnProfilingTypeFilter; label: string }[] = [ { value: 'all', label: 'All Types (18)' }, { value: 'numeric', label: 'Numeric (10)' }, { value: 'categorical', label: 'Categorical (6)' }, { value: 'boolean', label: 'Boolean (2)' }, ] /** * ColumnProfilingSummary -- the React block, 1:1: a ydata-profiling style EDA summary with a * dataset header (export report), four overview KPI cards, a type filter / search / view-mode * toolbar, and either four column deep-dive cards (histogram, category split, box plot, * boolean ratio) or a compact schema table. Pass `tableName`, `totalRows`, `totalColumns`, * `memoryFootprint` and `datasetVersion` to relabel it; edit the data above to change the rest. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-column-profiling-summary, [ui-column-profiling-summary]', standalone: true, // React renders the root <div> itself: the host stays out of layout, `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCardDescriptionComponent, UiCardContentComponent, UiProgressComponent, UiTableComponent, UiTableHeaderComponent, UiTableBodyComponent, UiTableRowComponent, UiTableHeadComponent, UiTableCellComponent, ], template: ` <div data-slot="column-profiling-summary" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Database" class="size-4" /> </div> <h1 class="text-foreground text-2xl font-bold tracking-tight break-all sm:text-3xl"> Table: {{ tableName }} </h1> <span ui-badge variant="outline" class="gap-1.5 font-mono text-xs font-normal"> <span class="bg-success size-1.5 rounded-full"></span> <span>Apache Parquet</span> </span> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">{{ datasetVersion }}</span> </div> <p class="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs"> <span>Profiled: 2026-08-21 14:30 UTC</span> <span>•</span> <span>Snappy Compressed</span> <span>•</span> <span class="text-foreground font-medium">EDA Engine: ydata-profiler v4.8</span> </p> </div> <div class="flex flex-wrap items-center gap-3"> <!-- Quick Meta Pills --> <div class="bg-card border-border/80 flex items-center divide-x rounded-lg border text-xs shadow-xs"> <div class="px-3 py-1.5"> <span class="text-muted-foreground">Rows: </span> <span class="text-foreground font-semibold tabular-nums">{{ totalRows }}</span> </div> <div class="px-3 py-1.5"> <span class="text-muted-foreground">Cols: </span> <span class="text-foreground font-semibold tabular-nums">{{ totalColumns }}</span> </div> <div class="px-3 py-1.5"> <span class="text-muted-foreground">Memory: </span> <span class="text-foreground font-semibold tabular-nums">{{ memoryFootprint }}</span> </div> </div> <!-- Export EDA Report Button --> <button ui-button type="button" aria-label="Download attachment" size="sm" class="gap-1.5 shadow-xs" [disabled]="isExporting()" (click)="handleExportReport()" > @if (isExporting()) { <span class="size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent"></span> } @else { <lucide-icon [img]="Download" class="size-3.5" /> } <span>{{ isExporting() ? 'Generating HTML Report...' : 'Export EDA Report' }}</span> </button> </div> </div> <!-- Export Success Banner --> @if (showExportSuccess()) { <div class="border-success/30 bg-success/10 text-foreground dark:text-foreground flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs" role="status" > <div class="flex items-center gap-2"> <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> <span class="font-medium"> Exploratory Data Analysis report exported. HTML summary & JSON schema bundle ready for download. </span> </div> <button type="button" class="text-success text-success cursor-pointer text-xs font-semibold hover:underline" (click)="showExportSuccess.set(false)" > Dismiss </button> </div> } <!-- 4 Dataset Overview KPI Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- 1. Numeric Columns --> <div ui-card class="flex flex-col justify-between"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-foreground flex size-7 items-center justify-center rounded-md border"> <lucide-icon [img]="Hash" class="text-info size-3.5" /> </div> <h3 ui-card-title class="text-sm font-medium">Numeric Columns</h3> </div> <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">55.6%</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">10</span> <span class="text-muted-foreground text-xs font-medium">Columns</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between"> <span>Types Breakdown:</span> <span class="text-foreground font-medium tabular-nums">8 Float64 · 2 Int32</span> </div> </div> </div> </div> <!-- 2. Categorical Columns --> <div ui-card class="flex flex-col justify-between"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-foreground flex size-7 items-center justify-center rounded-md border"> <lucide-icon [img]="Tag" class="text-chart-1 size-3.5" /> </div> <h3 ui-card-title class="text-sm font-medium">Categorical Columns</h3> </div> <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">33.3%</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">6</span> <span class="text-muted-foreground text-xs font-medium">Columns</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between"> <span>High Cardinality:</span> <span class="text-foreground font-medium tabular-nums">1 (customer_id)</span> </div> </div> </div> </div> <!-- 3. Missing Values % --> <div ui-card class="flex flex-col justify-between"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-foreground flex size-7 items-center justify-center rounded-md border"> <lucide-icon [img]="AlertCircle" class="text-success size-3.5" /> </div> <h3 ui-card-title class="text-sm font-medium">Missing Values</h3> </div> <span ui-badge variant="success" class="text-xs">Clean</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">0.4%</span> <span class="text-muted-foreground text-xs font-medium">Missing</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between"> <span>Total Null Cells:</span> <span class="text-foreground font-medium tabular-nums">593 / 2,669,220</span> </div> </div> </div> </div> <!-- 4. Duplicate Rows --> <div ui-card class="flex flex-col justify-between"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-foreground flex size-7 items-center justify-center rounded-md border"> <lucide-icon [img]="CheckCircle2" class="text-success size-3.5" /> </div> <h3 ui-card-title class="text-sm font-medium">Duplicate Rows</h3> </div> <span ui-badge variant="success" class="text-xs">100% Unique</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">0</span> <span class="text-muted-foreground text-xs font-medium">Duplicates</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between"> <span>Primary Key Integrity:</span> <span class="text-foreground font-medium">100.0% Valid</span> </div> </div> </div> </div> </div> <!-- Filter & Toolbar Controls --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-content class="p-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <!-- Type Filter Tabs --> <div class="flex flex-wrap items-center gap-1.5"> @for (f of typeFilters; track f.value) { <button type="button" [class]="filterClass(f.value)" (click)="activeTypeFilter.set(f.value)"> {{ f.label }} </button> } </div> <!-- Search & View Mode Switcher --> <div class="flex items-center gap-2"> <div class="relative flex items-center"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute left-2.5 size-3.5" /> <input [value]="searchQuery()" (input)="onSearch($event)" type="text" placeholder="Filter column name..." class="border-border bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-44 rounded-md border pr-2.5 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-none sm:w-56" /> </div> <div class="bg-muted border-border flex items-center rounded-md border p-0.5 text-xs"> <button type="button" [class]="viewClass('cards')" aria-label="Card deep dive view" (click)="viewMode.set('cards')" > <lucide-icon [img]="LayoutGrid" class="size-3.5" /> </button> <button type="button" [class]="viewClass('table')" aria-label="Compact table view" (click)="viewMode.set('table')" > <lucide-icon [img]="TableIcon" class="size-3.5" /> </button> </div> </div> </div> </div> </div> @if (viewMode() === 'table') { <!-- Compact Table View --> <div ui-card> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Dataset Schema & Profiling Matrix</h3> <p ui-card-description class="text-xs"> Tabular view of all columns with inferred types, missingness, cardinality, and distribution summary. </p> </div> <span ui-badge variant="outline" class="font-mono text-xs tabular-nums"> {{ filteredColumns().length }} of 18 columns </span> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <div ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="min-w-[180px]">Column Name</th> <th ui-table-head class="min-w-[140px]">Data Type</th> <th ui-table-head class="min-w-[110px]">Missing Values</th> <th ui-table-head class="min-w-[120px]">Distinct Values</th> <th ui-table-head class="min-w-[280px]">Distribution / Quantiles</th> <th ui-table-head class="text-right">Health Status</th> </tr> </thead> <tbody ui-table-body> @for (col of filteredColumns(); track col.name) { <tr ui-table-row> <td ui-table-cell class="font-mono text-xs font-medium"> <div class="flex items-center gap-1.5"> <span>{{ col.name }}</span> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer" [attr.aria-label]="'Copy column ' + col.name" (click)="handleCopy(col.name)" > @if (copiedColumn() === col.name) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> </td> <td ui-table-cell class="text-xs"> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">{{ col.type }}</span> </td> <td ui-table-cell class="text-muted-foreground font-mono text-xs tabular-nums"> {{ col.missing }} </td> <td ui-table-cell class="text-foreground font-mono text-xs tabular-nums">{{ col.distinct }}</td> <td ui-table-cell class="text-muted-foreground text-xs">{{ col.summary }}</td> <td ui-table-cell class="text-right"> <span ui-badge [variant]="statusVariant(col)" class="text-xs font-normal">{{ col.status }}</span> </td> </tr> } </tbody> </div> </div> </div> </div> } @else { <!-- 4 Detailed Column Deep-Dive Cards --> <div class="space-y-6"> <!-- 1. monthly_spend_usd (Numeric Float64 + 10-bar SVG Histogram) --> @if (activeTypeFilter() === 'all' || activeTypeFilter() === 'numeric') { <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-info/10 text-info flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="Hash" class="size-3.5" /> </div> <h3 ui-card-title class="font-mono text-base font-semibold">{{ spend.name }}</h3> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">{{ spend.type }}</span> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer" [attr.aria-label]="'Copy ' + spend.name" (click)="handleCopy(spend.name)" > @if (copiedColumn() === spend.name) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> <p ui-card-description class="text-xs"> Monthly billed subscription and add-on expenditure in USD. Right-skewed distribution with long tail. </p> </div> <!-- Meta Badges --> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="text-xs font-normal"> Missing: <span class="text-success text-success ml-1 font-semibold tabular-nums">{{ spend.missingPct }}</span> </span> <span ui-badge variant="outline" class="text-xs font-normal"> Distinct: <span class="text-foreground ml-1 font-semibold tabular-nums">{{ spend.distinctCount }}</span> </span> <span ui-badge variant="outline" class="text-xs font-normal"> Memory: <span class="text-foreground ml-1 font-semibold tabular-nums">{{ spend.memory }}</span> </span> </div> </div> </div> <div ui-card-content class="space-y-6 pt-1"> <!-- Summary Metrics Grid --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6"> @for (m of spendMetrics; track m.label) { <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground text-xs">{{ m.label }}</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ m.value }}</p> </div> } </div> <!-- 10-Bar SVG Histogram --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <div class="flex items-center gap-2"> <lucide-icon [img]="BarChart2" class="text-primary size-3.5" /> <span class="text-foreground font-medium">10-Bin Distribution Frequency Histogram</span> </div> @if (hoveredBin(); as b) { <span class="text-foreground font-mono tabular-nums"> {{ b.bin }}: <strong class="text-primary">{{ fmt(b.count) }} rows</strong> ({{ b.pct }}%) </span> } @else { <span class="text-muted-foreground">Hover bar to inspect bin interval</span> } </div> <!-- SVG Histogram Visual --> <div class="border-border bg-muted/20 w-full overflow-x-auto rounded-lg border p-4"> <div class="h-44 w-full max-w-[420px] min-w-full"> <svg class="h-full w-full overflow-visible" viewBox="0 0 520 140" preserveAspectRatio="none" role="img" aria-label="10-bar distribution histogram for monthly spend in USD" > <!-- Background Grid Lines --> <line x1="0" y1="20" x2="520" y2="20" stroke="currentColor" class="text-border/40" stroke-dasharray="3 3" /> <line x1="0" y1="65" x2="520" y2="65" stroke="currentColor" class="text-border/40" stroke-dasharray="3 3" /> <line x1="0" y1="110" x2="520" y2="110" stroke="currentColor" class="text-border" /> <!-- 10 Bars --> @for (bin of histogramBins; track $index; let idx = $index) { <g> <rect [attr.x]="idx * 52 + 6" [attr.y]="110 - bin.height" width="40" [attr.height]="bin.height" rx="3" [attr.class]="barClass(idx)" (mouseenter)="hoveredHistBin.set(idx)" (mouseleave)="hoveredHistBin.set(null)" /> @if (bin.pct >= 5) { <text [attr.x]="idx * 52 + 26" [attr.y]="104 - bin.height" text-anchor="middle" class="fill-foreground font-mono text-xs font-medium select-none" > {{ bin.pct }}% </text> } </g> } </svg> </div> <!-- Bin Axis Ticks --> <div class="text-muted-foreground mt-2 grid grid-cols-5 text-center font-mono text-xs tabular-nums"> <span class="text-left">$0.00</span> <span>$490</span> <span>$980</span> <span>$1,715</span> <span class="text-right">$2,450</span> </div> </div> </div> </div> </div> } <!-- 2. subscription_tier (Categorical + Horizontal Distribution Bars) --> @if (activeTypeFilter() === 'all' || activeTypeFilter() === 'categorical') { <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-chart-1/10 text-chart-1 flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="Tag" class="size-3.5" /> </div> <h3 ui-card-title class="font-mono text-base font-semibold">{{ tier.name }}</h3> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">{{ tier.type }}</span> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer" [attr.aria-label]="'Copy ' + tier.name" (click)="handleCopy(tier.name)" > @if (copiedColumn() === tier.name) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> <p ui-card-description class="text-xs"> Active customer plan classification tier. Clean categorical distribution across 4 discrete levels. </p> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="text-xs font-normal"> Missing: <span class="text-success text-success ml-1 font-semibold tabular-nums">{{ tier.missingPct }}</span> </span> <span ui-badge variant="outline" class="text-xs font-normal"> Distinct: <span class="text-foreground ml-1 font-semibold tabular-nums" >{{ tier.distinctCount }} values</span > </span> <span ui-badge variant="outline" class="text-xs font-normal"> Memory: <span class="text-foreground ml-1 font-semibold tabular-nums">{{ tier.memory }}</span> </span> </div> </div> </div> <div ui-card-content class="space-y-6 pt-1"> <!-- Segmented Composite Stacked Bar --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Category Proportions</span> <span class="text-muted-foreground font-mono tabular-nums">148,290 total instances</span> </div> <div class="border-border flex h-4 w-full overflow-hidden rounded-full border"> @for (t of subscriptionTiers; track t.name) { <div [class]="t.colorClass" [style.width.%]="t.pct" [title]="t.name + ': ' + t.pct + '% (' + fmt(t.count) + ' rows)'" ></div> } </div> </div> <!-- Detailed Breakdown Rows --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"> @for (t of subscriptionTiers; track t.name) { <div class="border-border bg-muted/20 flex flex-col justify-between rounded-lg border p-3.5"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <span class="text-foreground font-mono text-xs font-semibold">{{ t.name }}</span> <span ui-badge variant="outline" [class]="tierBadgeClass(t)">{{ t.pct.toFixed(1) }}%</span> </div> <p class="text-muted-foreground font-mono text-xs tabular-nums">{{ fmt(t.count) }} rows</p> </div> <div class="mt-3"> <div class="bg-muted relative h-1.5 w-full overflow-hidden rounded-full"> <div [class]="tierBarClass(t)" [style.width.%]="t.pct"></div> </div> </div> </div> } </div> </div> </div> } <!-- 3. account_age_days (Numeric Int32 + Quantiles Box Plot) --> @if (activeTypeFilter() === 'all' || activeTypeFilter() === 'numeric') { <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-info/10 text-info flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="Hash" class="size-3.5" /> </div> <h3 ui-card-title class="font-mono text-base font-semibold">{{ age.name }}</h3> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">{{ age.type }}</span> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer" [attr.aria-label]="'Copy ' + age.name" (click)="handleCopy(age.name)" > @if (copiedColumn() === age.name) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> <p ui-card-description class="text-xs"> Total account tenure in days since initial signup. Box plot shows median at 180d with Q1-Q3 interquartile band. </p> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="text-xs font-normal"> Missing: <span class="text-success text-success ml-1 font-semibold tabular-nums" >{{ age.missingPct }} (42 rows)</span > </span> <span ui-badge variant="outline" class="text-xs font-normal"> Distinct: <span class="text-foreground ml-1 font-semibold tabular-nums">{{ age.distinctCount }}</span> </span> <span ui-badge variant="outline" class="text-xs font-normal"> Memory: <span class="text-foreground ml-1 font-semibold tabular-nums">{{ age.memory }}</span> </span> </div> </div> </div> <div ui-card-content class="space-y-6 pt-1"> <!-- Quantile Stats Grid --> <div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-7"> <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span class="text-muted-foreground text-xs">Min (0%)</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ age.min }}</p> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span class="text-muted-foreground text-xs">Q1 (P25)</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ age.p25 }}</p> </div> <div class="border-primary/40 bg-primary/5 rounded-lg border p-2.5 text-center shadow-xs"> <span class="text-primary text-xs font-medium">Median (P50)</span> <p class="text-primary font-mono text-sm font-bold tabular-nums">{{ age.p50 }}</p> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span class="text-muted-foreground text-xs">Q3 (P75)</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ age.p75 }}</p> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span class="text-muted-foreground text-xs">P95</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ age.p95 }}</p> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span class="text-muted-foreground text-xs">P99</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ age.p99 }}</p> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span class="text-muted-foreground text-xs">Max</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ age.max }}</p> </div> </div> <!-- Box Plot SVG Representation --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Quantile Box & Whisker Plot</span> <span class="text-muted-foreground font-mono tabular-nums">IQR = 323 days (P75 - P25)</span> </div> <div class="border-border bg-muted/20 w-full overflow-x-auto rounded-lg border p-4"> <div class="h-28 w-full max-w-[420px] min-w-full"> <svg class="h-full w-full overflow-visible" viewBox="0 0 500 90" preserveAspectRatio="none" role="img" aria-label="Box plot visualization of account age quantiles" > <!-- Axis Baseline --> <line x1="30" y1="72" x2="470" y2="72" stroke="currentColor" class="text-border" /> <!-- Left Whisker: Min to Q1 --> <line x1="31" y1="36" x2="51" y2="36" stroke="currentColor" class="text-foreground" stroke-width="2" /> <!-- Min Cap --> <line x1="31" y1="24" x2="31" y2="48" stroke="currentColor" class="text-foreground" stroke-width="2" /> <!-- Right Whisker: Q3 to P99 --> <line x1="208" y1="36" x2="382" y2="36" stroke="currentColor" class="text-foreground" stroke-width="2" /> <!-- P99 Cap --> <line x1="382" y1="24" x2="382" y2="48" stroke="currentColor" class="text-foreground" stroke-width="2" /> <!-- Interquartile Box (Q1 to Q3) --> <rect x="51" y="18" width="157" height="36" rx="3" class="fill-primary/20 stroke-primary" stroke-width="2" /> <!-- Median (P50) Line --> <line x1="118" y1="18" x2="118" y2="54" class="stroke-primary" stroke-width="3" /> <!-- Outlier Points (P99 to Max: 780d, 820d, 850d) --> <circle cx="411" cy="36" r="3.5" class="fill-muted-foreground/60 stroke-foreground" /> <circle cx="431" cy="36" r="3.5" class="fill-muted-foreground/60 stroke-foreground" /> <circle cx="446" cy="36" r="3.5" class="fill-muted-foreground/60 stroke-foreground" /> <!-- Tick Labels on Axis --> <text x="31" y="86" text-anchor="middle" class="fill-muted-foreground font-mono text-xs"> 1d (Min) </text> <text x="51" y="86" text-anchor="middle" class="fill-muted-foreground font-mono text-xs"> 42d </text> <text x="118" y="86" text-anchor="middle" class="fill-primary font-mono text-xs font-bold"> 180d (Med) </text> <text x="208" y="86" text-anchor="middle" class="fill-muted-foreground font-mono text-xs"> 365d </text> <text x="382" y="86" text-anchor="middle" class="fill-muted-foreground font-mono text-xs"> 720d (P99) </text> <text x="446" y="86" text-anchor="middle" class="fill-muted-foreground font-mono text-xs"> 850d (Max) </text> </svg> </div> </div> </div> </div> </div> } <!-- 4. has_cancelled (Boolean) --> @if (activeTypeFilter() === 'all' || activeTypeFilter() === 'boolean') { <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-warning/10 text-warning flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="Binary" class="size-3.5" /> </div> <h3 ui-card-title class="font-mono text-base font-semibold">{{ cancelled.name }}</h3> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">{{ cancelled.type }}</span> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer" [attr.aria-label]="'Copy ' + cancelled.name" (click)="handleCopy(cancelled.name)" > @if (copiedColumn() === cancelled.name) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> <p ui-card-description class="text-xs"> Binary churn indicator flag. 14.2% positive cancellation rate against 85.8% retained base. </p> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="text-xs font-normal"> Missing: <span class="text-success text-success ml-1 font-semibold tabular-nums">{{ cancelled.missingPct }}</span> </span> <span ui-badge variant="outline" class="text-xs font-normal"> Distinct: <span class="text-foreground ml-1 font-semibold tabular-nums" >{{ cancelled.distinctCount }} values</span > </span> <span ui-badge variant="outline" class="text-xs font-normal"> Memory: <span class="text-foreground ml-1 font-semibold tabular-nums">{{ cancelled.memory }}</span> </span> </div> </div> </div> <div ui-card-content class="space-y-6 pt-1"> <!-- Boolean Split Bar --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Boolean Ratio Distribution</span> <span class="text-muted-foreground font-mono tabular-nums">Class Imbalance: 1 : 6.04</span> </div> <div class="border-border flex h-4 w-full overflow-hidden rounded-full border"> <div class="bg-success transition-colors" [style.width.%]="cancelled.falsePct" [title]="'False: ' + cancelled.falsePct + '% (' + fmt(cancelled.falseCount) + ' rows)'" ></div> <div class="bg-destructive transition-colors" [style.width.%]="cancelled.truePct" [title]="'True: ' + cancelled.truePct + '% (' + fmt(cancelled.trueCount) + ' rows)'" ></div> </div> </div> <!-- Value Breakdown Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <!-- False / Active --> <div class="border-border bg-muted/20 flex flex-col justify-between rounded-lg border p-4"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <span class="bg-success size-2 rounded-full"></span> <span class="text-foreground font-mono text-xs font-semibold">False (Active / Retained)</span> </div> <span ui-badge variant="outline" class="border-success/30 text-success text-success font-mono text-xs font-medium tabular-nums" > {{ cancelled.falsePct }}% </span> </div> <p class="text-muted-foreground font-mono text-xs tabular-nums"> {{ fmt(cancelled.falseCount) }} instances </p> </div> <div class="mt-3"> <ui-progress [value]="cancelled.falsePct" class="[&_[data-slot=progress-indicator]]:bg-success h-1.5" /> </div> </div> <!-- True / Cancelled --> <div class="border-border bg-muted/20 flex flex-col justify-between rounded-lg border p-4"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <span class="bg-destructive size-2 rounded-full"></span> <span class="text-foreground font-mono text-xs font-semibold" >True (Cancelled / Churned)</span > </div> <span ui-badge variant="outline" class="border-destructive/30 text-destructive text-destructive font-mono text-xs font-medium tabular-nums" > {{ cancelled.truePct }}% </span> </div> <p class="text-muted-foreground font-mono text-xs tabular-nums"> {{ fmt(cancelled.trueCount) }} instances </p> </div> <div class="mt-3"> <ui-progress [value]="cancelled.truePct" class="[&_[data-slot=progress-indicator]]:bg-destructive h-1.5" /> </div> </div> </div> </div> </div> } </div> } </div> `, }) export class UiColumnProfilingSummaryComponent { @Input() tableName = 'customer_churn_features_v2' @Input() totalRows = '148,290' @Input() totalColumns = 18 @Input() memoryFootprint = '24.2 MB' @Input() datasetVersion = 'v2.4.1' @Input('class') className?: string readonly activeTypeFilter = signal<ColumnProfilingTypeFilter>('all') readonly searchQuery = signal('') readonly viewMode = signal<ColumnProfilingViewMode>('cards') readonly isExporting = signal(false) readonly showExportSuccess = signal(false) readonly copiedColumn = signal<string | null>(null) readonly hoveredHistBin = signal<number | null>(null) readonly filteredColumns = computed(() => { const filter = this.activeTypeFilter() const q = this.searchQuery().toLowerCase() return allSchemaColumns.filter((col) => { const matchesCategory = filter === 'all' || col.category === filter const matchesSearch = col.name.toLowerCase().includes(q) || col.type.toLowerCase().includes(q) return matchesCategory && matchesSearch }) }) readonly hoveredBin = computed<ColumnProfilingHistogramBin | null>(() => { const idx = this.hoveredHistBin() return idx === null ? null : (histogramBins[idx] ?? null) }) protected readonly spend = monthlySpendStats protected readonly tier = subscriptionTierStats protected readonly age = accountAgeStats protected readonly cancelled = hasCancelledStats protected readonly histogramBins = histogramBins protected readonly subscriptionTiers = subscriptionTiers protected readonly spendMetrics = spendMetrics protected readonly typeFilters = typeFilters protected readonly AlertCircle = AlertCircle protected readonly BarChart2 = BarChart2 protected readonly Binary = Binary protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Copy = Copy protected readonly Database = Database protected readonly Download = Download protected readonly Hash = Hash protected readonly LayoutGrid = LayoutGrid protected readonly Search = Search protected readonly TableIcon = TableIcon protected readonly Tag = Tag get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } handleExportReport(): void { if (this.isExporting()) return this.isExporting.set(true) setTimeout(() => { this.isExporting.set(false) this.showExportSuccess.set(true) setTimeout(() => this.showExportSuccess.set(false), 3500) }, 600) } handleCopy(name: string): void { void navigator.clipboard?.writeText(name) this.copiedColumn.set(name) setTimeout(() => { this.copiedColumn.update((prev) => (prev === name ? null : prev)) }, 2000) } protected onSearch(event: Event): void { this.searchQuery.set((event.target as HTMLInputElement).value) } protected fmt(n: number): string { return n.toLocaleString() } protected filterClass(value: ColumnProfilingTypeFilter): string { return cn( 'cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors', this.activeTypeFilter() === value ? 'bg-primary text-primary-foreground shadow-xs' : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground', ) } protected viewClass(mode: ColumnProfilingViewMode): string { return cn( 'cursor-pointer rounded px-2 py-1 transition-colors', this.viewMode() === mode ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } protected barClass(idx: number): string { return cn( 'cursor-pointer transition-colors duration-150', this.hoveredHistBin() === idx ? 'fill-primary text-primary' : 'fill-primary/75 hover:fill-primary text-primary/75', ) } protected tierBadgeClass(t: ColumnProfilingTier): string { return cn('font-mono text-xs font-medium tabular-nums', t.badgeClass) } protected tierBarClass(t: ColumnProfilingTier): string { return cn('h-full rounded-full', t.colorClass) } protected statusVariant(col: ColumnProfilingSchemaColumn): 'default' | 'success' { return col.status.includes('Unique') ? 'default' : 'success' } }src/app/components/blocks/column-profiling-summary/index.ts0.3 kB
export { UiColumnProfilingSummaryComponent, type ColumnProfilingTypeFilter, type ColumnProfilingViewMode, type ColumnProfilingHistogramBin, type ColumnProfilingTier, type ColumnProfilingSchemaColumn, } from './column-profiling-summary.component'
Raw manifest:https://uipkge.dev/r/angular/column-profiling-summary.json