Menu

Change language

GitHub repo

Metrics Grid

blockanalytics

Six-tile KPI status grid. Each tile pairs a stat header (count / percentage) with a small pie or horizontal bar chart that breaks the metric down. Different tiles use different chart types so each KPI reads at a glance.

Installation

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

Variants

Loading interactive previews…

Files installed (2)

  • src/app/components/blocks/metrics-grid/metrics-grid.component.ts9.1 kB
    // Six-tile KPI grid. Each tile renders a labeled KPI header (title +
    // big number / percentage / count) above a small chart -- pie or
    // horizontal bar -- that gives the breakdown.
    //
    // Per the registry's primitive-vs-block rule: tile shapes are spelled
    // out inline as siblings. Different tiles use different chart types
    // (donut, full pie, horizontal bar) -- those differences are the whole
    // point of the layout, so they don't hide behind a wrapper.
    import { Component, ChangeDetectionStrategy } from '@angular/core'
    import { UiPieChartComponent } from '@/ui/charts/pie-chart/pie-chart.component'
    import { UiBarChartComponent } from '@/ui/charts/bar-chart/bar-chart.component'
    
    // --- KPI 1: status pie (e.g. successful / partially / error) ---
    const totalRuns = 770
    const runsByStatus = [
      { name: 'Successful', value: 638 },
      { name: 'Partial', value: 0 },
      { name: 'Error / Skipped', value: 132 },
    ]
    
    // --- KPI 2: disposition split ---
    const totalProcessed = 638
    const dispositionSplit = [
      { name: 'Non-disputable', value: 168 },
      { name: 'Disputable', value: 470 },
    ]
    
    // --- KPI 3: classification breakdown ---
    const totalClassified = 638
    const classification = [
      { name: 'Type A', value: 525 },
      { name: 'Type B', value: 113 },
    ]
    
    // --- KPI 4: top reasons (horizontal bar) ---
    const skipReasons = [
      { reason: 'CREDIT_NOTE', count: 11 },
      { reason: 'CUSTOMS_INSPECT', count: 5 },
      { reason: 'NEGATIVE_COST', count: 8 },
    ]
    const totalSkipped = skipReasons.reduce((a, r) => a + r.count, 0)
    
    // --- KPI 5: error reasons (horizontal bar) ---
    const errorReasons = [
      { reason: 'INVALID_CUTOFF', count: 3 },
      { reason: 'NO_SHIPMENT_DATA', count: 20 },
      { reason: 'SELF_BILLING', count: 42 },
      { reason: 'MISSING_OBLIGATORY_FIELD', count: 2 },
      { reason: 'MISSING_OBLIGATORY_TIME', count: 4 },
      { reason: 'UNABLE_TO_CLASSIFY', count: 40 },
    ]
    const totalErrors = errorReasons.reduce((a, r) => a + r.count, 0)
    
    // --- KPI 6: workflow status ---
    const openShare = 0.7602
    const workflowStatus = [
      { name: 'Open', value: 485 },
      { name: 'Pending', value: 151 },
      { name: 'Closed', value: 2 },
    ]
    
    const pieOption = {
      legend: { show: false },
      series: [
        {
          radius: ['0%', '70%'],
          label: {
            show: true,
            formatter: (p: { value: number; percent: number }) => `${p.value}\n(${p.percent}%)`,
            fontSize: 11,
            color: '#fff',
            fontWeight: 600,
          },
          labelLine: { show: false },
        },
      ],
    }
    
    const donutOption = {
      legend: { show: false },
      series: [
        {
          radius: ['40%', '70%'],
          label: {
            show: true,
            formatter: (p: { value: number; percent: number }) => `${p.value}\n(${p.percent}%)`,
            fontSize: 11,
            color: '#fff',
            fontWeight: 600,
          },
          labelLine: { show: false },
        },
      ],
    }
    
    const horizontalBarOption = {
      legend: { show: false },
      xAxis: { type: 'value' as const, axisLabel: { fontSize: 10 } },
      yAxis: { type: 'category' as const, axisLabel: { fontSize: 10 } },
      grid: { left: 16, right: 16, top: 8, bottom: 24, containLabel: true },
      series: [
        {
          type: 'bar' as const,
          barMaxWidth: 18,
          itemStyle: { borderRadius: [0, 3, 3, 0] },
        },
      ],
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-metrics-grid, [ui-metrics-grid]',
      standalone: true,
      imports: [UiPieChartComponent, UiBarChartComponent],
      host: {
        'data-slot': 'metrics-grid',
        class: 'block space-y-4',
      },
      template: `
        <h2 class="text-primary text-sm font-semibold tracking-tight">Results Overview</h2>
    
        <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
          <!-- Tile 1: Runs by status (pie) -->
          <article class="bg-card border-border rounded-lg border p-4">
            <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Runs by status</h3>
            <div class="text-primary mt-1 mb-2 text-2xl font-semibold">{{ totalRuns }} runs</div>
            <div class="text-muted-foreground mb-3 flex items-center gap-3 text-xs">
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-1 size-2 rounded-full"></span>
                Successful
              </span>
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-4 size-2 rounded-full"></span>
                Error / Skipped
              </span>
            </div>
            <ui-pie-chart [data]="runsByStatus" [option]="pieOption" height="220" />
          </article>
    
          <!-- Tile 2: Disposition split (pie) -->
          <article class="bg-card border-border rounded-lg border p-4">
            <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Disposition split</h3>
            <div class="text-primary mt-1 mb-2 text-2xl font-semibold">{{ totalProcessed }} processed</div>
            <div class="text-muted-foreground mb-3 flex items-center gap-3 text-xs">
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-1 size-2 rounded-full"></span>
                Non-disputable
              </span>
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-3 size-2 rounded-full"></span>
                Disputable
              </span>
            </div>
            <ui-pie-chart [data]="dispositionSplit" [option]="pieOption" height="220" />
          </article>
    
          <!-- Tile 3: Classification (pie) -->
          <article class="bg-card border-border rounded-lg border p-4">
            <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Classification</h3>
            <div class="text-primary mt-1 mb-2 text-2xl font-semibold">{{ totalClassified }} classified</div>
            <div class="text-muted-foreground mb-3 flex items-center gap-3 text-xs">
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-1 size-2 rounded-full"></span>
                Type A
              </span>
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-3 size-2 rounded-full"></span>
                Type B
              </span>
            </div>
            <ui-pie-chart [data]="classification" [option]="pieOption" height="220" />
          </article>
    
          <!-- Tile 4: Skip reasons (horizontal bar) -->
          <article class="bg-card border-border rounded-lg border p-4">
            <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Top skip reasons</h3>
            <div class="text-primary mt-1 mb-2 text-2xl font-semibold">{{ totalSkipped }} skipped</div>
            <div class="text-muted-foreground mb-3 text-xs">By trigger</div>
            <ui-bar-chart [data]="skipReasons" xField="reason" yField="count" [option]="horizontalBarOption" height="220" />
          </article>
    
          <!-- Tile 5: Error reasons (horizontal bar) -->
          <article class="bg-card border-border rounded-lg border p-4">
            <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Top error reasons</h3>
            <div class="text-primary mt-1 mb-2 text-2xl font-semibold">{{ totalErrors }} errors</div>
            <div class="text-muted-foreground mb-3 text-xs">By failure mode</div>
            <ui-bar-chart
              [data]="errorReasons"
              xField="reason"
              yField="count"
              [option]="horizontalBarOption"
              height="220"
            />
          </article>
    
          <!-- Tile 6: Workflow status (donut + highlight) -->
          <article class="bg-card border-border rounded-lg border p-4">
            <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Workflow status</h3>
            <div class="text-primary mt-1 mb-2 text-2xl font-semibold">{{ openPercent }}% open</div>
            <div class="text-muted-foreground mb-3 flex items-center gap-3 text-xs">
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-1 size-2 rounded-full"></span>
                Open
              </span>
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-5 size-2 rounded-full"></span>
                Pending
              </span>
              <span class="inline-flex items-center gap-1.5">
                <span class="bg-chart-3 size-2 rounded-full"></span>
                Closed
              </span>
            </div>
            <!-- React passes donut too; its radius is overridden by donutOption, so the ring comes from the option alone. -->
            <ui-pie-chart [data]="workflowStatus" [option]="donutOption" height="220" />
          </article>
        </div>
      `,
    })
    export class UiMetricsGridComponent {
      protected readonly totalRuns = totalRuns
      protected readonly runsByStatus = runsByStatus
      protected readonly totalProcessed = totalProcessed
      protected readonly dispositionSplit = dispositionSplit
      protected readonly totalClassified = totalClassified
      protected readonly classification = classification
      protected readonly skipReasons = skipReasons
      protected readonly totalSkipped = totalSkipped
      protected readonly errorReasons = errorReasons
      protected readonly totalErrors = totalErrors
      protected readonly openPercent = (openShare * 100).toFixed(2)
      protected readonly workflowStatus = workflowStatus
      protected readonly pieOption = pieOption
      protected readonly donutOption = donutOption
      protected readonly horizontalBarOption = horizontalBarOption
    }
    
  • src/app/components/blocks/metrics-grid/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/metrics-grid.json