{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "charts-4",
  "title": "Charts 4: scorecard",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "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.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "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": [
    {
      "path": "registry/ballmac/components/blocks/charts-4/charts-4.tsx",
      "content": "// Ballmac UI: Charts 4. https://ui.ballmac.com/blocks/charts-4\n\"use client\"\n\nimport * as React from \"react\"\nimport { Target } from \"lucide-react\"\n\nimport { ChartBar } from \"@/components/ballmac/chart-bar\"\nimport { ChartRadar } from \"@/components/ballmac/chart-radar\"\nimport { ChartRadial } from \"@/components/ballmac/chart-radial\"\nimport { KpiItem, KpiRow } from \"@/components/ballmac/kpi-row\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Charts4Goal = { name: string; label: string; /** Percent of the target reached. */ value: number }\ntype Charts4Area = { area: string; current: number; target: number }\ntype Charts4Leader = { name: string; value: number }\n\ntype Charts4Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    title?: string\n    description?: string\n    /** The goals, each as the percent of its target reached. Up to four read well as rings. */\n    goals?: Charts4Goal[]\n    /** How the team scores on each area, against the score it is aiming for. 0 to 100. */\n    profile?: Charts4Area[]\n    /** The people or teams to rank. */\n    leaders?: Charts4Leader[]\n    /** Writes a leader's number. */\n    leaderFormatter?: (value: number) => string\n    /** What the leaders are ranked on: \"Closed deals\". */\n    leaderTitle?: string\n    locale?: string\n  }\n\nconst sampleGoals: Charts4Goal[] = [\n  { name: \"revenue\", label: \"Revenue\", value: 92 },\n  { name: \"retention\", label: \"Retention\", value: 78 },\n  { name: \"launches\", label: \"Launches\", value: 60 },\n]\nconst sampleProfile: Charts4Area[] = [\n  { area: \"Quality\", current: 82, target: 90 },\n  { area: \"Speed\", current: 68, target: 80 },\n  { area: \"Cost\", current: 74, target: 75 },\n  { area: \"Support\", current: 88, target: 85 },\n  { area: \"Docs\", current: 57, target: 80 },\n  { area: \"Security\", current: 79, target: 90 },\n]\nconst sampleLeaders: Charts4Leader[] = [\n  { name: \"Maya Jensen\", value: 412000 },\n  { name: \"Tomás Rivera\", value: 368000 },\n  { name: \"Aiko Tanaka\", value: 331000 },\n  { name: \"Priya Nair\", value: 296000 },\n  { name: \"Liam O'Connor\", value: 244000 },\n]\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6\">\n      <Skeleton className=\"h-24 rounded-xl\" />\n      <div className=\"grid gap-6 md:grid-cols-2\">\n        <Skeleton className=\"aspect-square rounded-xl\" />\n        <Skeleton className=\"aspect-square rounded-xl\" />\n      </div>\n      <Skeleton className=\"aspect-[2/1] rounded-xl\" />\n    </div>\n  )\n}\n\n/**\n * 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\n * 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.\n */\nfunction Charts4({\n  title = \"Quarterly scorecard\",\n  description = \"Goals, how the team scores and who is leading.\",\n  goals = sampleGoals,\n  profile = sampleProfile,\n  leaders = sampleLeaders,\n  leaderFormatter = (v) => `$${Math.round(v / 1000)}k`,\n  leaderTitle = \"Closed deals\",\n  locale = \"en\",\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Charts4Props) {\n  const id = React.useId()\n  const g = state === \"empty\" ? [] : goals\n  const met = g.filter((x) => x.value >= 100).length\n  const average = g.length ? g.reduce((sum, x) => sum + x.value, 0) / g.length : 0\n  const gaps = profile.map((a) => ({ ...a, gap: a.target - a.current })).sort((a, b) => b.gap - a.gap)\n  const widest = gaps[0]\n  const top = [...leaders].sort((a, b) => b.value - a.value)[0]\n  const pct = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 })\n\n  const content = !g.length ? null : (\n    <div className=\"grid gap-6\">\n      <KpiRow columns={4} className=\"w-full\">\n        <KpiItem label=\"Goals met\" value={`${met} of ${g.length}`} detail=\"At or over their target\" />\n        <KpiItem label=\"Average progress\" value={`${pct.format(average)}%`} detail=\"Across every goal\" />\n        <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\"} />\n        <KpiItem label=\"Leading\" value={top?.name ?? \"–\"} detail={top ? `${leaderFormatter(top.value)} ${leaderTitle.toLowerCase()}` : undefined} />\n      </KpiRow>\n\n      <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 [&>*]:min-w-0\">\n        <ChartRadial title=\"Goals\" description=\"Percent of each target\" data={g} centerLabel={false} valueFormatter={(v) => `${v}%`} valueLabel=\"of target\" categoryLabel=\"Goal\" locale={locale} />\n        <ChartRadar\n          title=\"Team profile\"\n          description=\"Score against the target, 0 to 100\"\n          data={profile}\n          xKey=\"area\"\n          categoryLabel=\"Area\"\n          domain={[0, 100]}\n          locale={locale}\n          series={[\n            { key: \"target\", label: \"Target\", color: \"var(--chart-3)\" },\n            { key: \"current\", label: \"Current\" },\n          ]}\n        />\n      </div>\n\n      <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} />\n    </div>\n  )\n\n  return (\n    <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)}>\n      <header className=\"mb-8\">\n        <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n          {title}\n        </h1>\n        <p className=\"text-muted-foreground mt-2 max-w-2xl text-pretty\">{description}</p>\n      </header>\n      <StateView\n        state={state === \"empty\" || !g.length ? \"empty\" : state === \"loading\" || state === \"error\" ? state : \"ready\"}\n        onRetry={onRetry}\n        label=\"Loading the scorecard\"\n        loading={<Skel />}\n        empty={{ icon: <Target />, title: emptyState?.title ?? \"No goals yet\", description: emptyState?.description ?? \"Set a goal and its progress appears here.\", action: emptyState?.action }}\n        error={{ title: errorState?.title ?? \"The scorecard could not be loaded\", description: errorState?.description ?? \"Check your connection and try again.\", ...errorState }}\n      >\n        {content}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Charts4, type Charts4Props, type Charts4Goal, type Charts4Area, type Charts4Leader }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/charts-4/charts-4.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "scorecard",
      "okr",
      "goals",
      "radar",
      "radial",
      "ranking",
      "charts",
      "dashboard",
      "team",
      "performance"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "<Charts4 title description goals={[{ name, label, value }]} profile={[{ area, current, target }]} leaders={[{ name, value }]} leaderFormatter leaderTitle state />. goals are percents of target (100 is met). profile scores run 0 to 100. The goals met, average progress, biggest gap and leader are worked out from the data.",
      "whenToUse": [
        "Quarterly or sprint reviews",
        "OKR and team performance pages"
      ],
      "whenNotToUse": [
        "Time series (use charts-1 or charts-3)",
        "Revenue (use charts-2)"
      ],
      "composesWith": [
        "app-shell-1",
        "goals-1",
        "leaderboard-1",
        "chart-frame"
      ],
      "a11y": [
        {
          "keys": "Goals",
          "action": "Each ring is named and read as 'value of target (share)' in the chart's summary"
        },
        {
          "keys": "Each chart",
          "action": "Has a Table view of the same numbers, a CSV download and a summary written from its data"
        },
        {
          "keys": "Biggest gap",
          "action": "Written as 'N points under target', not only a shape"
        }
      ],
      "customization": [
        "Pass your own goals, profile and leaders",
        "leaderFormatter writes the ranking's numbers",
        "Swap a chart for another chart-* component"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "charts-4-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeletons shaped like the figures and the three charts, under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No goals yet",
        "example": "charts-4-empty",
        "trigger": "state=\"empty\" (or goals={[]})",
        "note": "Says progress appears once a goal is set; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "charts-4-error",
        "trigger": "state=\"error\"",
        "note": "An alert with Try again (onRetry)."
      }
    ],
    "examples": [
      "charts-4-demo",
      "charts-4-loading",
      "charts-4-empty",
      "charts-4-error"
    ],
    "url": "https://ui.ballmac.com/components/charts-4"
  },
  "categories": [
    "blocks",
    "charts"
  ],
  "type": "registry:block"
}