# Charts 4: scorecard

> A quarterly scorecard: goals met and average progress, concentric rings for the goals, a radar of the team against its targets and a ranked bar chart, with a table view and CSV download on every chart.

A shadcn/ui block from the [@ballmac](https://ui.ballmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ballmac/charts-4).

- Page, with a live preview: https://shadcn.exchange/r/ballmac/charts-4
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/charts-4-light-b2c6353a87e154ea.png
- Registry item: https://ui.ballmac.com/r/charts-4.json
- Categories: blocks, charts
- Listed: 2026-10-18, source last changed 2026-10-18

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/ballmac/charts-4.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `utils`, `https://ui.ballmac.com/r/chart-bar.json`, `https://ui.ballmac.com/r/chart-radar.json`, `https://ui.ballmac.com/r/chart-radial.json`, `https://ui.ballmac.com/r/kpi-row.json`, `https://ui.ballmac.com/r/skeleton.json`, `https://ui.ballmac.com/r/state-view.json`

## Files

### `registry/ballmac/components/blocks/charts-4/charts-4.tsx`

```tsx
// Ballmac UI: Charts 4. https://ui.ballmac.com/blocks/charts-4
"use client"

import * as React from "react"
import { Target } from "lucide-react"

import { ChartBar } from "@/components/ballmac/chart-bar"
import { ChartRadar } from "@/components/ballmac/chart-radar"
import { ChartRadial } from "@/components/ballmac/chart-radial"
import { KpiItem, KpiRow } from "@/components/ballmac/kpi-row"
import { Skeleton } from "@/components/ballmac/skeleton"
import { frameProps, StateView, type StateProps } from "@/components/ballmac/state-view"
import { cn } from "@/lib/utils"

type Charts4Goal = { name: string; label: string; /** Percent of the target reached. */ value: number }
type Charts4Area = { area: string; current: number; target: number }
type Charts4Leader = { name: string; value: number }

type Charts4Props = Omit<React.ComponentProps<"section">, "title"> &
  StateProps & {
    title?: string
    description?: string
    /** The goals, each as the percent of its target reached. Up to four read well as rings. */
    goals?: Charts4Goal[]
    /** How the team scores on each area, against the score it is aiming for. 0 to 100. */
    profile?: Charts4Area[]
    /** The people or teams to rank. */
    leaders?: Charts4Leader[]
    /** Writes a leader's number. */
    leaderFormatter?: (value: number) => string
    /** What the leaders are ranked on: "Closed deals". */
    leaderTitle?: string
    locale?: string
  }

const sampleGoals: Charts4Goal[] = [
  { name: "revenue", label: "Revenue", value: 92 },
  { name: "retention", label: "Retention", value: 78 },
  { name: "launches", label: "Launches", value: 60 },
]
const sampleProfile: Charts4Area[] = [
  { area: "Quality", current: 82, target: 90 },
  { area: "Speed", current: 68, target: 80 },
  { area: "Cost", current: 74, target: 75 },
  { area: "Support", current: 88, target: 85 },
  { area: "Docs", current: 57, target: 80 },
  { area: "Security", current: 79, target: 90 },
]
const sampleLeaders: Charts4Leader[] = [
  { name: "Maya Jensen", value: 412000 },
  { name: "Tomás Rivera", value: 368000 },
  { name: "Aiko Tanaka", value: 331000 },
  { name: "Priya Nair", value: 296000 },
  { name: "Liam O'Connor", value: 244000 },
]

function Skel() {
  return (
    <div aria-hidden="true" className="grid gap-6">
      <Skeleton className="h-24 rounded-xl" />
      <div className="grid gap-6 md:grid-cols-2">
        <Skeleton className="aspect-square rounded-xl" />
        <Skeleton className="aspect-square rounded-xl" />
      </div>
      <Skeleton className="aspect-[2/1] rounded-xl" />
    </div>
  )
}

/**
 * A quarterly scorecard: how many goals are met and how far along the rest are, concentric rings for the goals, a radar of the team against its targets
 * and a ranked bar chart. The headline figures are worked out from the data, and every chart has a table view and a CSV download.
 */
function Charts4({
  title = "Quarterly scorecard",
  description = "Goals, how the team scores and who is leading.",
  goals = sampleGoals,
  profile = sampleProfile,
  leaders = sampleLeaders,
  leaderFormatter = (v) => `$${Math.round(v / 1000)}k`,
  leaderTitle = "Closed deals",
  locale = "en",
  state = "ready",
  onRetry,
  emptyState,
  errorState,
  className,
  ...props
}: Charts4Props) {
  const id = React.useId()
  const g = state === "empty" ? [] : goals
  const met = g.filter((x) => x.value >= 100).length
  const average = g.length ? g.reduce((sum, x) => sum + x.value, 0) / g.length : 0
  const gaps = profile.map((a) => ({ ...a, gap: a.target - a.current })).sort((a, b) => b.gap - a.gap)
  const widest = gaps[0]
  const top = [...leaders].sort((a, b) => b.value - a.value)[0]
  const pct = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 })

  const content = !g.length ? null : (
    <div className="grid gap-6">
      <KpiRow columns={4} className="w-full">
        <KpiItem label="Goals met" value={`${met} of ${g.length}`} detail="At or over their target" />
        <KpiItem label="Average progress" value={`${pct.format(average)}%`} detail="Across every goal" />
        <KpiItem label="Biggest gap" value={widest && widest.gap > 0 ? widest.area : "None"} detail={widest && widest.gap > 0 ? `${widest.gap} points under target` : "Every area is on target"} />
        <KpiItem label="Leading" value={top?.name ?? "–"} detail={top ? `${leaderFormatter(top.value)} ${leaderTitle.toLowerCase()}` : undefined} />
      </KpiRow>

      <div className="grid grid-cols-1 gap-6 md:grid-cols-2 [&>*]:min-w-0">
        <ChartRadial title="Goals" description="Percent of each target" data={g} centerLabel={false} valueFormatter={(v) => `${v}%`} valueLabel="of target" categoryLabel="Goal" locale={locale} />
        <ChartRadar
          title="Team profile"
          description="Score against the target, 0 to 100"
          data={profile}
          xKey="area"
          categoryLabel="Area"
          domain={[0, 100]}
          locale={locale}
          series={[
            { key: "target", label: "Target", color: "var(--chart-3)" },
            { key: "current", label: "Current" },
          ]}
        />
      </div>

      <ChartBar title={leaderTitle} description="This quarter, highest first" data={leaders} xKey="name" categoryLabel="Name" layout="bars" sort="descending" labels valueAxis={false} grid="none" highlight={top?.name} valueFormatter={leaderFormatter} locale={locale} aspect="panorama" series={[{ key: "value", label: leaderTitle }]} radius={8} />
    </div>
  )

  return (
    <section data-slot="charts-4" data-state={state} aria-labelledby={`${id}-title`} className={cn("mx-auto w-full max-w-5xl px-4 py-10 sm:px-6", className)} {...frameProps(props)}>
      <header className="mb-8">
        <h1 id={`${id}-title`} className="text-3xl font-semibold tracking-[-0.035em] text-balance">
          {title}
        </h1>
        <p className="text-muted-foreground mt-2 max-w-2xl text-pretty">{description}</p>
      </header>
      <StateView
        state={state === "empty" || !g.length ? "empty" : state === "loading" || state === "error" ? state : "ready"}
        onRetry={onRetry}
        label="Loading the scorecard"
        loading={<Skel />}
        empty={{ icon: <Target />, title: emptyState?.title ?? "No goals yet", description: emptyState?.description ?? "Set a goal and its progress appears here.", action: emptyState?.action }}
        error={{ title: errorState?.title ?? "The scorecard could not be loaded", description: errorState?.description ?? "Check your connection and try again.", ...errorState }}
      >
        {content}
      </StateView>
    </section>
  )
}

export { Charts4, type Charts4Props, type Charts4Goal, type Charts4Area, type Charts4Leader }
```
