# Radial Chart Interactive

> A weekly activity view where picking a day animates three large rings and updates each ring's totals.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/radial-chart-interactive).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/radial-chart-interactive
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/radial-chart-interactive-light-a08b8a3f9eee92ca.png
- Registry item: https://suiss.app/ui/r/new-york-v4/radial-chart-interactive.json
- Categories: charts, charts-radial
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/radial-chart-interactive.json
```

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

## Dependencies

- Registry items: `@esuiss/card`, `@esuiss/toggle-group`

## Files

### `bases/radix/components/charts/radial-chart-interactive.tsx`

```tsx
"use client"

import * as React from "react"

import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Ring = "move" | "exercise" | "stand"

const RINGS: {
  key: Ring
  label: string
  unit: string
  goal: number
  color: string
}[] = [
  {
    key: "move",
    label: "Move",
    unit: "kcal",
    goal: 600,
    color: "var(--color-red-500)",
  },
  {
    key: "exercise",
    label: "Exercise",
    unit: "min",
    goal: 30,
    color: "var(--color-green-500)",
  },
  {
    key: "stand",
    label: "Stand",
    unit: "hr",
    goal: 12,
    color: "var(--color-cyan-500)",
  },
]

const WEEK = [
  { day: "Monday", values: [520, 34, 11] },
  { day: "Tuesday", values: [610, 42, 12] },
  { day: "Wednesday", values: [380, 18, 9] },
  { day: "Thursday", values: [640, 45, 12] },
  { day: "Friday", values: [470, 28, 10] },
  { day: "Saturday", values: [720, 62, 12] },
  { day: "Sunday", values: [486, 24, 10] },
]

const FILL_DURATION = 750

const NO_ACTIVITY = RINGS.map(() => 0)

function useMorph(target: number[]) {
  const [values, setValues] = React.useState(() => target.map(() => 0))
  const valuesRef = React.useRef(values)

  React.useEffect(() => {
    const from = valuesRef.current
    const reduced = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches
    const start = performance.now()
    let frame = 0
    const step = () => {
      const progress = reduced
        ? 1
        : Math.min(1, (performance.now() - start) / FILL_DURATION)
      const eased =
        progress < 0.5 ? 4 * progress ** 3 : 1 - (-2 * progress + 2) ** 3 / 2
      const next =
        progress < 1
          ? target.map((value, index) => {
              const origin = from[index] ?? value
              return origin + (value - origin) * eased
            })
          : target
      valuesRef.current = next
      setValues(next)
      if (progress < 1) frame = requestAnimationFrame(step)
    }
    frame = requestAnimationFrame(step)
    return () => cancelAnimationFrame(frame)
  }, [target])

  return values
}

function Rings({
  values,
  size,
  bar,
  className,
}: {
  values: number[]
  size: number
  bar: number
  className?: string
}) {
  return (
    <svg
      aria-hidden
      viewBox={`0 0 ${size} ${size}`}
      className={className}
      style={{ rotate: "-90deg" }}
    >
      {RINGS.map((ring, index) => {
        const radius =
          size / 2 - bar / 2 - index * (bar + Math.max(1, bar * 0.12))
        const circumference = 2 * Math.PI * radius
        const progress = Math.max(
          0,
          Math.min(1, (values[index] ?? 0) / ring.goal)
        )
        return (
          <g key={ring.key}>
            <circle
              cx={size / 2}
              cy={size / 2}
              r={radius}
              fill="none"
              strokeWidth={bar}
              stroke={`color-mix(in oklab, ${ring.color} 22%, transparent)`}
            />
            {progress > 0.001 && (
              <circle
                cx={size / 2}
                cy={size / 2}
                r={radius}
                fill="none"
                strokeWidth={bar}
                strokeLinecap="round"
                stroke={ring.color}
                strokeDasharray={`${progress * circumference} ${circumference}`}
              />
            )}
          </g>
        )
      })}
    </svg>
  )
}

export function RadialChartInteractive() {
  const [selected, setSelected] = React.useState(6)
  const target = WEEK[selected]?.values ?? NO_ACTIVITY
  const values = useMorph(target)

  return (
    <Card variant="filled" size="sm" className="[--card-spacing:--spacing(5)]">
      <CardHeader className="gap-0.5">
        <p className="text-lg font-semibold">Activity</p>
        <p className="text-sm text-muted-foreground">This week · pick a day</p>
      </CardHeader>
      <CardContent className="flex flex-col gap-6">
        <ToggleGroup
          spacing={1}
          type="single"
          value={String(selected)}
          onValueChange={(value: string) => {
            if (value) setSelected(Number(value))
          }}
          aria-label="Day"
          className="grid w-full grid-cols-7"
        >
          {WEEK.map((entry, index) => (
            <ToggleGroupItem
              key={entry.day}
              value={String(index)}
              aria-label={entry.day}
              className="h-auto flex-col gap-1.5 rounded-xl px-0 py-2 text-xs font-semibold text-muted-foreground data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:hover:bg-background data-[state=on]:hover:text-foreground"
            >
              {entry.day.slice(0, 3)}
              <Rings
                values={entry.values}
                size={40}
                bar={4}
                className="size-10"
              />
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <div className="flex flex-wrap items-center justify-center gap-8">
          <Rings
            values={values}
            size={260}
            bar={24}
            className="size-65 max-w-full"
          />
          <p className="sr-only" aria-live="polite">
            {WEEK[selected]?.day}:{" "}
            {RINGS.map(
              (ring, index) =>
                `${ring.label} ${target[index] ?? 0} of ${ring.goal} ${ring.unit}`
            ).join(", ")}
          </p>
          <dl className="flex min-w-44 flex-col gap-4">
            {RINGS.map((ring, index) => {
              const percent = Math.round(
                ((target[index] ?? 0) / ring.goal) * 100
              )
              return (
                <div key={ring.key} className="flex flex-col gap-0.5">
                  <dt className="text-base font-semibold">{ring.label}</dt>
                  <dd
                    className="flex items-baseline gap-0.5 text-[color-mix(in_oklab,var(--ring),var(--foreground)_45%)] dark:text-(--ring)"
                    style={{ "--ring": ring.color } as React.CSSProperties}
                  >
                    <span className="text-3xl font-semibold tracking-tight tabular-nums">
                      {Math.round(values[index] ?? 0)}/{ring.goal}
                    </span>
                    <span className="text-base font-semibold uppercase">
                      {ring.unit}
                    </span>
                  </dd>
                  <dd className="text-xs text-muted-foreground">
                    {percent}%{percent >= 100 ? " · goal met" : ""}
                  </dd>
                </div>
              )
            })}
          </dl>
        </div>
      </CardContent>
    </Card>
  )
}
```
