Menu

Change language

GitHub repo

Column Profiling Summary

blockdevops

Pandas Profiling and ydata style exploratory data analysis (EDA) column profiling summary with dataset metrics, distribution histograms, quantile box plots, and categorical frequency breakdowns.

Installation

$npx uipkge-ng@latest add column-profiling-summary
Manifest:https://uipkge.dev/r/angular/column-profiling-summary.jsonInstalls to:src/app/components/blocks/column-profiling-summary/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ColumnProfilingHistogramBin
interface ColumnProfilingHistogramBin {
  bin: string
  count: number
  pct: number
  height: number
}
ColumnProfilingTier
interface ColumnProfilingTier {
  name: string
  count: number
  pct: number
  colorClass: string
  badgeClass: string
}
ColumnProfilingSchemaColumn
interface 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 &amp; 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 &amp; 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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/column-profiling-summary.json