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 shadcn@latest add https://uipkge.dev/r/lit/metrics-grid.json
Manifest:https://uipkge.dev/r/lit/metrics-grid.jsonInstalls to:src/uip/blocks/metrics-grid/

Variants

Loading interactive previews…

Schema

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

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-metrics-grid': UipMetricsGrid
  }
}

npm dependencies

Theming

CSS custom properties referenced in this item. Override any of them in your :root or per-element to retheme.

--chart-1--chart-3--chart-4--chart-5

Files installed (1)

  • src/uip/blocks/metrics-grid/metrics-grid.ts9 kB
    import { LitElement, css, html } from 'lit'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/charts/pie-chart'
    import '../../components/charts/bar-chart'
    
    // 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. Tile shapes are spelled out inline as siblings.
    
    const totalRuns = 770
    const runsByStatus = [
      { name: 'Successful', value: 638 },
      { name: 'Partial', value: 0 },
      { name: 'Error / Skipped', value: 132 },
    ]
    
    const totalProcessed = 638
    const dispositionSplit = [
      { name: 'Non-disputable', value: 168 },
      { name: 'Disputable', value: 470 },
    ]
    
    const totalClassified = 638
    const classification = [
      { name: 'Type A', value: 525 },
      { name: 'Type B', value: 113 },
    ]
    
    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)
    
    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)
    
    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: any) => `${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: any) => `${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] },
        },
      ],
    }
    
    /**
     * <uip-metrics-grid> — the registry MetricsGrid block as a web component.
     *
     * Datasets, option objects, and classes are React's verbatim. Tiles are
     * spelled out inline (different chart types per tile are the point of the
     * layout). No props, no events. Chart data/options are properties bound from
     * the module constants above.
     */
    export class UipMetricsGrid extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'metrics-grid')
      }
    
      render() {
        return html`
          <div data-slot="metrics-grid" class="space-y-4">
            <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">
              <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="size-2 rounded-full" style="background:var(--chart-1)"></span>
                    Successful
                  </span>
                  <span class="inline-flex items-center gap-1.5">
                    <span class="size-2 rounded-full" style="background:var(--chart-4)"></span>
                    Error / Skipped
                  </span>
                </div>
                <uip-pie-chart .data=${runsByStatus} .option=${pieOption} height="220"></uip-pie-chart>
              </article>
    
              <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="size-2 rounded-full" style="background:var(--chart-1)"></span>
                    Non-disputable
                  </span>
                  <span class="inline-flex items-center gap-1.5">
                    <span class="size-2 rounded-full" style="background:var(--chart-3)"></span>
                    Disputable
                  </span>
                </div>
                <uip-pie-chart .data=${dispositionSplit} .option=${pieOption} height="220"></uip-pie-chart>
              </article>
    
              <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="size-2 rounded-full" style="background:var(--chart-1)"></span>
                    Type A
                  </span>
                  <span class="inline-flex items-center gap-1.5">
                    <span class="size-2 rounded-full" style="background:var(--chart-3)"></span>
                    Type B
                  </span>
                </div>
                <uip-pie-chart .data=${classification} .option=${pieOption} height="220"></uip-pie-chart>
              </article>
    
              <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>
                <uip-bar-chart
                  .data=${skipReasons}
                  x-field="reason"
                  y-field="count"
                  .option=${horizontalBarOption}
                  height="220"
                ></uip-bar-chart>
              </article>
    
              <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>
                <uip-bar-chart
                  .data=${errorReasons}
                  x-field="reason"
                  y-field="count"
                  .option=${horizontalBarOption}
                  height="220"
                ></uip-bar-chart>
              </article>
    
              <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">${(openShare * 100).toFixed(2)}% 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="size-2 rounded-full" style="background:var(--chart-1)"></span>
                    Open
                  </span>
                  <span class="inline-flex items-center gap-1.5">
                    <span class="size-2 rounded-full" style="background:var(--chart-5)"></span>
                    Pending
                  </span>
                  <span class="inline-flex items-center gap-1.5">
                    <span class="size-2 rounded-full" style="background:var(--chart-3)"></span>
                    Closed
                  </span>
                </div>
                <uip-pie-chart .data=${workflowStatus} .option=${donutOption} donut height="220"></uip-pie-chart>
              </article>
            </div>
          </div>
        `
      }
    }
    
    customElements.get('uip-metrics-grid') || customElements.define('uip-metrics-grid', UipMetricsGrid)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-metrics-grid': UipMetricsGrid
      }
    }
    

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