UIPackage
Menu

Framework

Change language

Boilerplate 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.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/column-profiling-summary.json
Named registry:npx shadcn-vue@latest add @uipkge/column-profiling-summaryInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
classHTMLAttributes['class']optional
tableNamestring'customer_churn_features_v2'optional
totalRowsstring'148optional
totalColumnsnumber18optional
memoryFootprintstring'24.2 MB'optional
datasetVersionstring'v2.4.1'optional

Files installed (1)

  • app/components/blocks/ColumnProfilingSummary.vue50.6 kB
    <script setup lang="ts">
    import { ref, computed, type HTMLAttributes } from 'vue'
    import {
      Database,
      Download,
      CheckCircle2,
      AlertCircle,
      Hash,
      Tag,
      Binary,
      Search,
      BarChart2,
      Copy,
      Check,
      Table as TableIcon,
      LayoutGrid,
    } from 'lucide-vue-next'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
    import { Progress } from '@/components/ui/progress'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    import { cn } from '@/lib/utils'
    
    interface Props {
      class?: HTMLAttributes['class']
      tableName?: string
      totalRows?: string
      totalColumns?: number
      memoryFootprint?: string
      datasetVersion?: string
    }
    
    const props = withDefaults(defineProps<Props>(), {
      tableName: 'customer_churn_features_v2',
      totalRows: '148,290',
      totalColumns: 18,
      memoryFootprint: '24.2 MB',
      datasetVersion: 'v2.4.1',
    })
    
    const activeTypeFilter = ref<'all' | 'numeric' | 'categorical' | 'boolean'>('all')
    const searchQuery = ref('')
    const viewMode = ref<'cards' | 'table'>('cards')
    const isExporting = ref(false)
    const showExportSuccess = ref(false)
    const copiedColumn = ref<string | null>(null)
    const hoveredHistBin = ref<number | null>(null)
    
    // 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 = [
      { 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 = [
      { 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 = [
      {
        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 filteredColumns = computed(() => {
      return allSchemaColumns.filter((col) => {
        const matchesCategory = activeTypeFilter.value === 'all' || col.category === activeTypeFilter.value
        const matchesSearch =
          col.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
          col.type.toLowerCase().includes(searchQuery.value.toLowerCase())
        return matchesCategory && matchesSearch
      })
    })
    
    function handleExportReport() {
      if (isExporting.value) return
      isExporting.value = true
      setTimeout(() => {
        isExporting.value = false
        showExportSuccess.value = true
        setTimeout(() => {
          showExportSuccess.value = false
        }, 3500)
      }, 600)
    }
    
    function handleCopy(name: string) {
      navigator.clipboard?.writeText(name)
      copiedColumn.value = name
      setTimeout(() => {
        if (copiedColumn.value === name) {
          copiedColumn.value = null
        }
      }, 2000)
    }
    </script>
    
    <template>
      <div data-slot="column-profiling-summary" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)">
        <!-- 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">
                <Database class="size-4" />
              </div>
              <h1 class="text-foreground text-2xl font-bold tracking-tight break-all sm:text-3xl">
                Table: {{ props.tableName }}
              </h1>
              <Badge variant="outline" class="gap-1.5 font-mono text-xs font-normal">
                <span class="bg-success size-1.5 rounded-full" />
                <span>Apache Parquet</span>
              </Badge>
              <Badge variant="secondary" class="font-mono text-xs font-normal">
                {{ props.datasetVersion }}
              </Badge>
            </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">{{ props.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">{{ props.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">{{ props.memoryFootprint }}</span>
              </div>
            </div>
    
            <!-- Export EDA Report Button -->
            <Button
              aria-label="Download attachment"
              size="sm"
              class="gap-1.5 shadow-xs"
              :disabled="isExporting"
              @click="handleExportReport"
            >
              <Download v-if="!isExporting" class="size-3.5" />
              <span v-else class="size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
              <span>{{ isExporting ? 'Generating HTML Report...' : 'Export EDA Report' }}</span>
            </Button>
          </div>
        </div>
    
        <!-- Export Success Banner -->
        <div
          v-if="showExportSuccess"
          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">
            <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 text-xs font-semibold hover:underline"
            @click="showExportSuccess = 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 -->
          <Card class="flex flex-col justify-between">
            <CardHeader 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">
                    <Hash class="text-info size-3.5" />
                  </div>
                  <CardTitle class="text-sm font-medium">Numeric Columns</CardTitle>
                </div>
                <Badge variant="outline" class="font-mono text-xs tabular-nums">55.6%</Badge>
              </div>
            </CardHeader>
            <CardContent 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>
            </CardContent>
          </Card>
    
          <!-- 2. Categorical Columns -->
          <Card class="flex flex-col justify-between">
            <CardHeader 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">
                    <Tag class="text-chart-1 size-3.5" />
                  </div>
                  <CardTitle class="text-sm font-medium">Categorical Columns</CardTitle>
                </div>
                <Badge variant="outline" class="font-mono text-xs tabular-nums">33.3%</Badge>
              </div>
            </CardHeader>
            <CardContent 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>
            </CardContent>
          </Card>
    
          <!-- 3. Missing Values % -->
          <Card class="flex flex-col justify-between">
            <CardHeader 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">
                    <AlertCircle class="text-success size-3.5" />
                  </div>
                  <CardTitle class="text-sm font-medium">Missing Values</CardTitle>
                </div>
                <Badge variant="success" class="text-xs">Clean</Badge>
              </div>
            </CardHeader>
            <CardContent 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>
            </CardContent>
          </Card>
    
          <!-- 4. Duplicate Rows -->
          <Card class="flex flex-col justify-between">
            <CardHeader 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">
                    <CheckCircle2 class="text-success size-3.5" />
                  </div>
                  <CardTitle class="text-sm font-medium">Duplicate Rows</CardTitle>
                </div>
                <Badge variant="success" class="text-xs">100% Unique</Badge>
              </div>
            </CardHeader>
            <CardContent 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>
            </CardContent>
          </Card>
        </div>
    
        <!-- Filter & Toolbar Controls -->
        <Card className="border-border bg-card shadow-xs">
          <CardContent 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">
                <button
                  type="button"
                  :class="[
                    'cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
                    activeTypeFilter === 'all'
                      ? 'bg-primary text-primary-foreground shadow-xs'
                      : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground',
                  ]"
                  @click="activeTypeFilter = 'all'"
                >
                  All Types (18)
                </button>
                <button
                  type="button"
                  :class="[
                    'cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
                    activeTypeFilter === 'numeric'
                      ? 'bg-primary text-primary-foreground shadow-xs'
                      : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground',
                  ]"
                  @click="activeTypeFilter = 'numeric'"
                >
                  Numeric (10)
                </button>
                <button
                  type="button"
                  :class="[
                    'cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
                    activeTypeFilter === 'categorical'
                      ? 'bg-primary text-primary-foreground shadow-xs'
                      : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground',
                  ]"
                  @click="activeTypeFilter = 'categorical'"
                >
                  Categorical (6)
                </button>
                <button
                  type="button"
                  :class="[
                    'cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
                    activeTypeFilter === 'boolean'
                      ? 'bg-primary text-primary-foreground shadow-xs'
                      : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground',
                  ]"
                  @click="activeTypeFilter = 'boolean'"
                >
                  Boolean (2)
                </button>
              </div>
    
              <!-- Search & View Mode Switcher -->
              <div class="flex items-center gap-2">
                <div class="relative flex items-center">
                  <Search class="text-muted-foreground pointer-events-none absolute left-2.5 size-3.5" />
                  <input
                    v-model="searchQuery"
                    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="[
                      'cursor-pointer rounded px-2 py-1 transition-colors',
                      viewMode === 'cards'
                        ? 'bg-background text-foreground shadow-xs'
                        : 'text-muted-foreground hover:text-foreground',
                    ]"
                    aria-label="Card deep dive view"
                    @click="viewMode = 'cards'"
                  >
                    <LayoutGrid class="size-3.5" />
                  </button>
                  <button
                    type="button"
                    :class="[
                      'cursor-pointer rounded px-2 py-1 transition-colors',
                      viewMode === 'table'
                        ? 'bg-background text-foreground shadow-xs'
                        : 'text-muted-foreground hover:text-foreground',
                    ]"
                    aria-label="Compact table view"
                    @click="viewMode = 'table'"
                  >
                    <TableIcon class="size-3.5" />
                  </button>
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
    
        <!-- Compact Table View -->
        <Card v-if="viewMode === 'table'">
          <CardHeader class="pb-3">
            <div class="flex items-center justify-between">
              <div>
                <CardTitle class="text-base font-semibold">Dataset Schema & Profiling Matrix</CardTitle>
                <CardDescription class="text-xs">
                  Tabular view of all columns with inferred types, missingness, cardinality, and distribution summary.
                </CardDescription>
              </div>
              <Badge variant="outline" class="font-mono text-xs tabular-nums">
                {{ filteredColumns.length }} of 18 columns
              </Badge>
            </div>
          </CardHeader>
          <CardContent class="p-0">
            <div class="overflow-x-auto">
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead class="min-w-[180px]">Column Name</TableHead>
                    <TableHead class="min-w-[140px]">Data Type</TableHead>
                    <TableHead class="min-w-[110px]">Missing Values</TableHead>
                    <TableHead class="min-w-[120px]">Distinct Values</TableHead>
                    <TableHead class="min-w-[280px]">Distribution / Quantiles</TableHead>
                    <TableHead class="text-right">Health Status</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  <TableRow v-for="col in filteredColumns" :key="col.name">
                    <TableCell 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"
                          :aria-label="`Copy column ${col.name}`"
                          @click="handleCopy(col.name)"
                        >
                          <Check v-if="copiedColumn === col.name" class="text-success size-3" />
                          <Copy v-else class="size-3" />
                        </button>
                      </div>
                    </TableCell>
                    <TableCell class="text-xs">
                      <Badge variant="secondary" class="font-mono text-xs font-normal">
                        {{ col.type }}
                      </Badge>
                    </TableCell>
                    <TableCell class="text-muted-foreground font-mono text-xs tabular-nums">
                      {{ col.missing }}
                    </TableCell>
                    <TableCell class="text-foreground font-mono text-xs tabular-nums">
                      {{ col.distinct }}
                    </TableCell>
                    <TableCell class="text-muted-foreground text-xs">
                      {{ col.summary }}
                    </TableCell>
                    <TableCell class="text-right">
                      <Badge :variant="col.status.includes('Unique') ? 'default' : 'success'" class="text-xs font-normal">
                        {{ col.status }}
                      </Badge>
                    </TableCell>
                  </TableRow>
                </TableBody>
              </Table>
            </div>
          </CardContent>
        </Card>
    
        <!-- 4 Detailed Column Deep-Dive Cards -->
        <div v-else class="space-y-6">
          <!-- 1. Column 1: monthly_spend_usd (Numeric Float64 + 10-bar SVG Histogram) -->
          <Card v-if="activeTypeFilter === 'all' || activeTypeFilter === 'numeric'" class="border-border bg-card shadow-xs">
            <CardHeader 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">
                      <Hash class="size-3.5" />
                    </div>
                    <CardTitle class="font-mono text-base font-semibold">
                      {{ monthlySpendStats.name }}
                    </CardTitle>
                    <Badge variant="secondary" class="font-mono text-xs font-normal">
                      {{ monthlySpendStats.type }}
                    </Badge>
                    <button
                      type="button"
                      class="text-muted-foreground hover:text-foreground cursor-pointer"
                      :aria-label="`Copy ${monthlySpendStats.name}`"
                      @click="handleCopy(monthlySpendStats.name)"
                    >
                      <Check v-if="copiedColumn === monthlySpendStats.name" class="text-success size-3" />
                      <Copy v-else class="size-3" />
                    </button>
                  </div>
                  <CardDescription class="text-xs">
                    Monthly billed subscription and add-on expenditure in USD. Right-skewed distribution with long tail.
                  </CardDescription>
                </div>
    
                <!-- Meta Badges -->
                <div class="flex flex-wrap items-center gap-2">
                  <Badge variant="outline" class="text-xs font-normal">
                    Missing:
                    <span class="text-success text-success ml-1 font-semibold tabular-nums">{{
                      monthlySpendStats.missingPct
                    }}</span>
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Distinct:
                    <span class="text-foreground ml-1 font-semibold tabular-nums">{{
                      monthlySpendStats.distinctCount
                    }}</span>
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Memory:
                    <span class="text-foreground ml-1 font-semibold tabular-nums">{{ monthlySpendStats.memory }}</span>
                  </Badge>
                </div>
              </div>
            </CardHeader>
    
            <CardContent 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">
                <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                  <span class="text-muted-foreground text-xs">Minimum</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ monthlySpendStats.min }}</p>
                </div>
                <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                  <span class="text-muted-foreground text-xs">Mean (Avg)</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ monthlySpendStats.mean }}</p>
                </div>
                <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                  <span class="text-muted-foreground text-xs">Median (P50)</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ monthlySpendStats.median }}</p>
                </div>
                <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                  <span class="text-muted-foreground text-xs">Maximum</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ monthlySpendStats.max }}</p>
                </div>
                <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                  <span class="text-muted-foreground text-xs">Std. Deviation</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ monthlySpendStats.stdDev }}</p>
                </div>
                <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                  <span class="text-muted-foreground text-xs">Interquartile (IQR)</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ monthlySpendStats.iqr }}</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">
                    <BarChart2 class="text-primary size-3.5" />
                    <span class="text-foreground font-medium">10-Bin Distribution Frequency Histogram</span>
                  </div>
                  <span v-if="hoveredHistBin !== null" class="text-foreground font-mono tabular-nums">
                    {{ histogramBins[hoveredHistBin].bin }}:
                    <strong class="text-primary">{{ histogramBins[hoveredHistBin].count.toLocaleString() }} rows</strong>
                    ({{ histogramBins[hoveredHistBin].pct }}%)
                  </span>
                  <span v-else 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 -->
                      <g v-for="(bin, idx) in histogramBins" :key="idx">
                        <!-- Bar rect -->
                        <rect
                          :x="idx * 52 + 6"
                          :y="110 - bin.height"
                          width="40"
                          :height="bin.height"
                          rx="3"
                          :class="[
                            'cursor-pointer transition-colors duration-150',
                            hoveredHistBin === idx
                              ? 'fill-primary text-primary'
                              : 'fill-primary/75 hover:fill-primary text-primary/75',
                          ]"
                          @mouseenter="hoveredHistBin = idx"
                          @mouseleave="hoveredHistBin = null"
                        />
    
                        <!-- Percentage Label on top of high bars -->
                        <text
                          v-if="bin.pct >= 5"
                          :x="idx * 52 + 26"
                          :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>
            </CardContent>
          </Card>
    
          <!-- 2. Column 2: subscription_tier (Categorical + Horizontal Distribution Bars) -->
          <Card
            v-if="activeTypeFilter === 'all' || activeTypeFilter === 'categorical'"
            class="border-border bg-card shadow-xs"
          >
            <CardHeader 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">
                      <Tag class="size-3.5" />
                    </div>
                    <CardTitle class="font-mono text-base font-semibold">
                      {{ subscriptionTierStats.name }}
                    </CardTitle>
                    <Badge variant="secondary" class="font-mono text-xs font-normal">
                      {{ subscriptionTierStats.type }}
                    </Badge>
                    <button
                      type="button"
                      class="text-muted-foreground hover:text-foreground cursor-pointer"
                      :aria-label="`Copy ${subscriptionTierStats.name}`"
                      @click="handleCopy(subscriptionTierStats.name)"
                    >
                      <Check v-if="copiedColumn === subscriptionTierStats.name" class="text-success size-3" />
                      <Copy v-else class="size-3" />
                    </button>
                  </div>
                  <CardDescription class="text-xs">
                    Active customer plan classification tier. Clean categorical distribution across 4 discrete levels.
                  </CardDescription>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <Badge variant="outline" class="text-xs font-normal">
                    Missing:
                    <span class="text-success text-success ml-1 font-semibold tabular-nums">{{
                      subscriptionTierStats.missingPct
                    }}</span>
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Distinct:
                    <span class="text-foreground ml-1 font-semibold tabular-nums"
                      >{{ subscriptionTierStats.distinctCount }} values</span
                    >
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Memory:
                    <span class="text-foreground ml-1 font-semibold tabular-nums">{{ subscriptionTierStats.memory }}</span>
                  </Badge>
                </div>
              </div>
            </CardHeader>
    
            <CardContent 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>
    
                <!-- Stacked bar -->
                <div class="border-border flex h-4 w-full overflow-hidden rounded-full border">
                  <div
                    v-for="tier in subscriptionTiers"
                    :key="tier.name"
                    :class="tier.colorClass"
                    :style="{ width: `${tier.pct}%` }"
                    :title="`${tier.name}: ${tier.pct}% (${tier.count.toLocaleString()} rows)`"
                  />
                </div>
              </div>
    
              <!-- Detailed Breakdown Rows -->
              <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
                <div
                  v-for="tier in subscriptionTiers"
                  :key="tier.name"
                  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">{{ tier.name }}</span>
                      <Badge variant="outline" :class="cn('font-mono text-xs font-medium tabular-nums', tier.badgeClass)">
                        {{ tier.pct.toFixed(1) }}%
                      </Badge>
                    </div>
                    <p class="text-muted-foreground font-mono text-xs tabular-nums">
                      {{ tier.count.toLocaleString() }} rows
                    </p>
                  </div>
    
                  <div class="mt-3">
                    <div class="bg-muted relative h-1.5 w-full overflow-hidden rounded-full">
                      <div :class="cn('h-full rounded-full', tier.colorClass)" :style="{ width: `${tier.pct}%` }" />
                    </div>
                  </div>
                </div>
              </div>
            </CardContent>
          </Card>
    
          <!-- 3. Column 3: account_age_days (Numeric Int32 + Quantiles Box Plot) -->
          <Card v-if="activeTypeFilter === 'all' || activeTypeFilter === 'numeric'" class="border-border bg-card shadow-xs">
            <CardHeader 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">
                      <Hash class="size-3.5" />
                    </div>
                    <CardTitle class="font-mono text-base font-semibold">
                      {{ accountAgeStats.name }}
                    </CardTitle>
                    <Badge variant="secondary" class="font-mono text-xs font-normal">
                      {{ accountAgeStats.type }}
                    </Badge>
                    <button
                      type="button"
                      class="text-muted-foreground hover:text-foreground cursor-pointer"
                      :aria-label="`Copy ${accountAgeStats.name}`"
                      @click="handleCopy(accountAgeStats.name)"
                    >
                      <Check v-if="copiedColumn === accountAgeStats.name" class="text-success size-3" />
                      <Copy v-else class="size-3" />
                    </button>
                  </div>
                  <CardDescription class="text-xs">
                    Total account tenure in days since initial signup. Box plot shows median at 180d with Q1-Q3
                    interquartile band.
                  </CardDescription>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <Badge variant="outline" class="text-xs font-normal">
                    Missing:
                    <span class="text-success text-success ml-1 font-semibold tabular-nums"
                      >{{ accountAgeStats.missingPct }} (42 rows)</span
                    >
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Distinct:
                    <span class="text-foreground ml-1 font-semibold tabular-nums">{{ accountAgeStats.distinctCount }}</span>
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Memory:
                    <span class="text-foreground ml-1 font-semibold tabular-nums">{{ accountAgeStats.memory }}</span>
                  </Badge>
                </div>
              </div>
            </CardHeader>
    
            <CardContent 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">{{ accountAgeStats.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">{{ accountAgeStats.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">{{ accountAgeStats.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">{{ accountAgeStats.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">{{ accountAgeStats.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">{{ accountAgeStats.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">{{ accountAgeStats.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"
                    >
                      <!-- Whisker line (Min 1d to Whisker Max 720d) -->
                      <!-- Scale: 0d -> x=30, 900d -> x=470 (width=440, ratio=440/900 = 0.4888) -->
                      <!-- Min 1d: x=30.5 -->
                      <!-- P25 42d: x=50.5 -->
                      <!-- P50 180d: x=118 -->
                      <!-- P75 365d: x=208.4 -->
                      <!-- P99 720d: x=382 -->
                      <!-- Max 850d: x=445.5 -->
    
                      <!-- 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>
            </CardContent>
          </Card>
    
          <!-- 4. Column 4: has_cancelled (Boolean) -->
          <Card v-if="activeTypeFilter === 'all' || activeTypeFilter === 'boolean'" class="border-border bg-card shadow-xs">
            <CardHeader 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">
                      <Binary class="size-3.5" />
                    </div>
                    <CardTitle class="font-mono text-base font-semibold">
                      {{ hasCancelledStats.name }}
                    </CardTitle>
                    <Badge variant="secondary" class="font-mono text-xs font-normal">
                      {{ hasCancelledStats.type }}
                    </Badge>
                    <button
                      type="button"
                      class="text-muted-foreground hover:text-foreground cursor-pointer"
                      :aria-label="`Copy ${hasCancelledStats.name}`"
                      @click="handleCopy(hasCancelledStats.name)"
                    >
                      <Check v-if="copiedColumn === hasCancelledStats.name" class="text-success size-3" />
                      <Copy v-else class="size-3" />
                    </button>
                  </div>
                  <CardDescription class="text-xs">
                    Binary churn indicator flag. 14.2% positive cancellation rate against 85.8% retained base.
                  </CardDescription>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <Badge variant="outline" class="text-xs font-normal">
                    Missing:
                    <span class="text-success text-success ml-1 font-semibold tabular-nums">{{
                      hasCancelledStats.missingPct
                    }}</span>
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Distinct:
                    <span class="text-foreground ml-1 font-semibold tabular-nums"
                      >{{ hasCancelledStats.distinctCount }} values</span
                    >
                  </Badge>
                  <Badge variant="outline" class="text-xs font-normal">
                    Memory:
                    <span class="text-foreground ml-1 font-semibold tabular-nums">{{ hasCancelledStats.memory }}</span>
                  </Badge>
                </div>
              </div>
            </CardHeader>
    
            <CardContent 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>
    
                <!-- Two-Tone Split Bar -->
                <div class="border-border flex h-4 w-full overflow-hidden rounded-full border">
                  <div
                    class="bg-success transition-colors"
                    :style="{ width: `${hasCancelledStats.falsePct}%` }"
                    :title="`False: ${hasCancelledStats.falsePct}% (${hasCancelledStats.falseCount.toLocaleString()} rows)`"
                  />
                  <div
                    class="bg-destructive transition-colors"
                    :style="{ width: `${hasCancelledStats.truePct}%` }"
                    :title="`True: ${hasCancelledStats.truePct}% (${hasCancelledStats.trueCount.toLocaleString()} rows)`"
                  />
                </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 class="text-foreground font-mono text-xs font-semibold">False (Active / Retained)</span>
                      </div>
                      <Badge
                        variant="outline"
                        class="border-success/30 text-success text-success font-mono text-xs font-medium tabular-nums"
                      >
                        {{ hasCancelledStats.falsePct }}%
                      </Badge>
                    </div>
                    <p class="text-muted-foreground font-mono text-xs tabular-nums">
                      {{ hasCancelledStats.falseCount.toLocaleString() }} instances
                    </p>
                  </div>
                  <div class="mt-3">
                    <Progress
                      :model-value="hasCancelledStats.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 class="text-foreground font-mono text-xs font-semibold">True (Cancelled / Churned)</span>
                      </div>
                      <Badge
                        variant="outline"
                        class="border-destructive/30 text-destructive text-destructive font-mono text-xs font-medium tabular-nums"
                      >
                        {{ hasCancelledStats.truePct }}%
                      </Badge>
                    </div>
                    <p class="text-muted-foreground font-mono text-xs tabular-nums">
                      {{ hasCancelledStats.trueCount.toLocaleString() }} instances
                    </p>
                  </div>
                  <div class="mt-3">
                    <Progress
                      :model-value="hasCancelledStats.truePct"
                      class="[&_[data-slot=progress-indicator]]:bg-destructive h-1.5"
                    />
                  </div>
                </div>
              </div>
            </CardContent>
          </Card>
        </div>
      </div>
    </template>
    

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