Metrics Grid
blockanalyticsSix-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
$pnpm dlx uipkge-ng@latest add metrics-grid$npx uipkge-ng@latest add metrics-grid$yarn dlx uipkge-ng@latest add metrics-grid$bunx uipkge-ng@latest add metrics-gridManifest:
https://uipkge.dev/r/angular/metrics-grid.jsonInstalls to:src/app/components/blocks/metrics-grid/Variants
Loading interactive previews…
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
export { UiMetricsGridComponent } from './metrics-grid.component'
Raw manifest:https://uipkge.dev/r/angular/metrics-grid.json