Menu

Change language

GitHub repo

Stats — Band with Sparklines

blockmarketing

Metric band pairing each headline figure with an inline sparkline and its period, so a number arrives with the shape of the trend behind it.

Installation

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

Variants

Loading interactive previews…

Schema

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

SparklineMetric
interface SparklineMetric {
  value: string
  unit: string
  label: string
  period: string
  series: number[]
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/stats-band-sparklines/stats-band-sparklines.component.ts3.7 kB
    // Metric band pairing each headline figure with an inline sparkline and its period,
    // so a number arrives with the shape of the trend behind it.
    // Port of the Vue/React block 1:1 — inline SVG, no chart dependency.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    interface SparklineMetric {
      value: string
      unit: string
      label: string
      period: string
      series: number[]
    }
    
    const metrics: SparklineMetric[] = [
      {
        value: '4.2',
        unit: 'days',
        label: 'Median time to close',
        period: 'last 12 months',
        series: [9.4, 9.1, 8.7, 8.2, 7.4, 6.8, 6.1, 5.6, 5.1, 4.8, 4.4, 4.2],
      },
      {
        value: '61',
        unit: '/mo',
        label: 'Ad-hoc data requests',
        period: 'last 12 months',
        series: [212, 198, 184, 160, 141, 122, 108, 96, 84, 74, 66, 61],
      },
      {
        value: '340',
        unit: '',
        label: 'Certified metrics live',
        period: 'since launch',
        series: [0, 12, 38, 74, 112, 148, 181, 214, 248, 281, 312, 340],
      },
      {
        value: '99.98',
        unit: '%',
        label: 'Query availability',
        period: 'rolling 90 days',
        series: [99.9, 99.95, 99.92, 99.97, 99.99, 99.96, 99.98, 99.99, 99.97, 99.98, 99.99, 99.98],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-stats-band-sparklines, [ui-stats-band-sparklines]',
      standalone: true,
      // Vue renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [UiBadgeComponent, UiSeparatorComponent],
      template: `
        <section data-slot="stats-band-sparklines" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-12">
            <span ui-badge variant="secondary">Measured</span>
    
            <dl class="mt-6 grid gap-8 sm:grid-cols-2 lg:grid-cols-4 lg:gap-0">
              @for (metric of allMetrics; track metric.label; let idx = $index) {
                <div class="relative lg:px-6 lg:first:pl-0">
                  <dd class="font-display text-3xl font-bold tracking-tight">
                    {{ metric.value }}<span class="text-muted-foreground text-lg font-semibold">{{ metric.unit }}</span>
                  </dd>
                  <dt class="mt-1 text-sm font-medium">{{ metric.label }}</dt>
    
                  <svg
                    class="text-primary/70 mt-3 h-7 w-full"
                    viewBox="0 0 100 28"
                    preserveAspectRatio="none"
                    aria-hidden="true"
                  >
                    <polyline [attr.points]="points(metric.series)" fill="none" stroke="currentColor" stroke-width="1.5" />
                  </svg>
    
                  <p class="text-muted-foreground mt-1 font-mono text-xs">{{ metric.period }}</p>
    
                  @if (idx < allMetrics.length - 1) {
                    <div ui-separator orientation="vertical" class="absolute top-0 right-0 hidden h-full lg:block"></div>
                  }
                </div>
              }
            </dl>
          </div>
        </section>
      `,
    })
    export class UiStatsBandSparklinesComponent {
      protected readonly allMetrics = metrics
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('border-border bg-card border-y', this.className)
      }
    
      /** Normalise a series into a 100x28 viewBox so every sparkline shares a baseline. */
      points(series: number[]): string {
        const min = Math.min(...series)
        const max = Math.max(...series)
        const span = max - min || 1
        return series
          .map((value, index) => {
            const x = (index / (series.length - 1)) * 100
            const y = 26 - ((value - min) / span) * 24
            return `${x.toFixed(1)},${y.toFixed(1)}`
          })
          .join(' ')
      }
    }
    
  • src/app/components/blocks/stats-band-sparklines/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/stats-band-sparklines.json