# Radial Chart Interactive

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

Radial Chart Interactive is an open-source chart from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/charts/radial-chart-interactive

**React**

**Example**

```tsx title="components/charts/radial-chart-interactive.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)
  valuesRef.current = values

  React.useEffect(() => {
    const from = valuesRef.current
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setValues(target)
      return
    }
    const start = performance.now()
    let frame = 0
    const step = () => {
      const progress = Math.min(1, (performance.now() - start) / FILL_DURATION)
      const eased =
        progress < 0.5 ? 4 * progress ** 3 : 1 - (-2 * progress + 2) ** 3 / 2
      setValues(
        target.map((value, index) => {
          const origin = from[index] ?? value
          return origin + (value - origin) * eased
        })
      )
      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}
          value={[String(selected)]}
          onValueChange={(value: string[]) => {
            if (value[0] !== undefined) setSelected(Number(value[0]))
          }}
          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 aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:hover:bg-background aria-pressed: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>
  )
}
```

An activity summary with a strip of seven day buttons, each showing mini rings, and large Move, Exercise and Stand rings that fill to the selected day's values. Totals and goal progress for each ring update next to it. Use it as a pattern for daily goals that readers browse day by day.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/radial-chart-interactive
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add charts radial-chart-interactive
```

### Manual

1. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

2. Copy the source into `components/charts/radial-chart-interactive.tsx`:

```tsx title="components/charts/radial-chart-interactive.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)
  valuesRef.current = values

  React.useEffect(() => {
    const from = valuesRef.current
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setValues(target)
      return
    }
    const start = performance.now()
    let frame = 0
    const step = () => {
      const progress = Math.min(1, (performance.now() - start) / FILL_DURATION)
      const eased =
        progress < 0.5 ? 4 * progress ** 3 : 1 - (-2 * progress + 2) ** 3 / 2
      setValues(
        target.map((value, index) => {
          const origin = from[index] ?? value
          return origin + (value - origin) * eased
        })
      )
      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}
          value={[String(selected)]}
          onValueChange={(value: string[]) => {
            if (value[0] !== undefined) setSelected(Number(value[0]))
          }}
          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 aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:hover:bg-background aria-pressed: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>
  )
}
```

3. Update the import paths to match your project.

## Usage

```tsx
import { RadialChartInteractive } from "@/components/charts/radial-chart-interactive"
```

```tsx
<RadialChartInteractive />
```

Replace `WEEK` with your own days and values, one number per ring in the same order as `RINGS`. Edit `RINGS` to change labels, units, goals and colors. Set the initially selected day in the `selected` state.

## Notes

The rings are plain SVG rather than a Recharts chart, so there is no tooltip. Values beyond the goal show a full ring, while the text still reports the real percentage. Day buttons are fully keyboard accessible, the totals are announced through a live region, and the fill animation respects reduced motion.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/charts-radial--interactive.light.webp)

```tsx title="uniwind/components/charts/radial-chart-interactive.tsx"
import { cn } from "cn"
import * as React from "react"
import { View } from "react-native"
import Svg, { Circle, G } from "react-native-svg"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { type ChartColor, ChartPalette } from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

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

const RINGS: {
  key: Ring
  label: string
  unit: string
  goal: number
  color: ChartColor
  text: string
}[] = [
  {
    key: "move",
    label: "Move",
    unit: "kcal",
    goal: 600,
    color: "red-500",
    text: "text-red-500",
  },
  {
    key: "exercise",
    label: "Exercise",
    unit: "min",
    goal: 30,
    color: "green-500",
    text: "text-green-500",
  },
  {
    key: "stand",
    label: "Stand",
    unit: "hr",
    goal: 12,
    color: "cyan-500",
    text: "text-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 NO_ACTIVITY = RINGS.map(() => 0)

function Rings({
  values,
  size,
  bar,
}: {
  values: number[]
  size: number
  bar: number
}) {
  return (
    <View
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants"
    >
      <ChartPalette colors={RINGS.map((ring) => ring.color)}>
        {(palette) => (
          <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
            <G rotation={-90} origin={`${size / 2}, ${size / 2}`}>
              {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)
                )
                const color = palette[ring.color]
                return (
                  <G key={ring.key}>
                    <Circle
                      cx={size / 2}
                      cy={size / 2}
                      r={radius}
                      fill="none"
                      strokeWidth={bar}
                      stroke={color}
                      strokeOpacity={0.22}
                    />
                    {progress > 0.001 ? (
                      <Circle
                        cx={size / 2}
                        cy={size / 2}
                        r={radius}
                        fill="none"
                        strokeWidth={bar}
                        strokeLinecap="round"
                        stroke={color}
                        strokeDasharray={`${progress * circumference} ${circumference}`}
                      />
                    ) : null}
                  </G>
                )
              })}
            </G>
          </Svg>
        )}
      </ChartPalette>
    </View>
  )
}

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

  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-lg font-semibold">Activity</Text>
        <Text className="text-sm text-muted-foreground">
          This week · pick a day
        </Text>
      </CardHeader>
      <CardContent className="gap-6">
        <ToggleGroup
          spacing={1}
          value={[String(selected)]}
          onValueChange={(value) => {
            if (value[0] !== undefined) setSelected(Number(value[0]))
          }}
          accessibilityLabel="Day"
          className="w-full self-stretch"
        >
          {WEEK.map((entry, index) => (
            <ToggleGroupItem
              key={entry.day}
              value={String(index)}
              accessibilityLabel={entry.day}
              className={(state) =>
                cn(
                  "h-auto min-w-0 flex-1 shrink flex-col gap-1.5 rounded-xl px-0 py-2",
                  state.pressed && "bg-background active:opacity-80"
                )
              }
            >
              <Text
                className={cn(
                  "text-xs font-semibold",
                  selected === index
                    ? "text-foreground"
                    : "text-muted-foreground"
                )}
              >
                {entry.day.slice(0, 3)}
              </Text>
              <Rings values={entry.values} size={40} bar={4} />
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <View className="flex-row flex-wrap items-center justify-center gap-8">
          <Rings values={values} size={260} bar={24} />
          <View className="min-w-44 gap-4" accessibilityLiveRegion="polite">
            {RINGS.map((ring, index) => {
              const percent = Math.round(
                ((values[index] ?? 0) / ring.goal) * 100
              )
              return (
                <View key={ring.key} accessible className="gap-0.5">
                  <Text className="text-base font-semibold">{ring.label}</Text>
                  <View className="flex-row items-baseline gap-0.5">
                    <Text
                      className={cn(
                        "text-3xl font-semibold tracking-tight tabular-nums",
                        ring.text
                      )}
                    >
                      {`${Math.round(values[index] ?? 0)}/${ring.goal}`}
                    </Text>
                    <Text
                      className={cn(
                        "text-base font-semibold uppercase",
                        ring.text
                      )}
                    >
                      {ring.unit}
                    </Text>
                  </View>
                  <Text className="text-xs text-muted-foreground">
                    {`${percent}%${percent >= 100 ? " · goal met" : ""}`}
                  </Text>
                </View>
              )
            })}
          </View>
        </View>
      </CardContent>
    </Card>
  )
}
```

```swift title="swift/Charts/RadialChartInteractive.swift"
import SwiftUI

private let rings: [(key: String, label: String, unit: String, goal: Double, color: Color)] = [
  ("move", "Move", "kcal", 600, Palette.red500),
  ("exercise", "Exercise", "min", 30, Palette.green500),
  ("stand", "Stand", "hr", 12, Palette.cyan500),
]

private let week: [(day: String, values: [Double])] = [
  ("Monday", [520, 34, 11]),
  ("Tuesday", [610, 42, 12]),
  ("Wednesday", [380, 18, 9]),
  ("Thursday", [640, 45, 12]),
  ("Friday", [470, 28, 10]),
  ("Saturday", [720, 62, 12]),
  ("Sunday", [486, 24, 10]),
]

private struct Rings: View {
  let values: [Double]
  let size: CGFloat
  let bar: CGFloat

  var body: some View {
    ZStack {
      ForEach(rings.indices, id: \.self) { index in
        let ring = rings[index]
        let radius = size / 2 - bar / 2 - CGFloat(index) * (bar + max(1, bar * 0.12))
        let progress = max(0, min(1, (values[index]) / ring.goal))
        Circle()
          .stroke(ring.color.opacity(0.22), lineWidth: bar)
          .frame(width: radius * 2, height: radius * 2)
        Circle()
          .trim(from: 0, to: progress)
          .stroke(ring.color, style: StrokeStyle(lineWidth: bar, lineCap: .round))
          .rotationEffect(.degrees(-90))
          .frame(width: radius * 2, height: radius * 2)
          .opacity(progress > 0.001 ? 1 : 0)
      }
    }
    .frame(width: size, height: size)
    .accessibilityHidden(true)
  }
}

public struct RadialChartInteractive: View {
  @State private var selected = 6
  @State private var values: [Double] = [0, 0, 0]
  @Environment(\.colorScheme) private var colorScheme
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init() {}

  init(selected: Int, filled: Bool) {
    _selected = State(initialValue: selected)
    _values = State(initialValue: filled ? week[selected].values : [0, 0, 0])
  }

  public var body: some View {
    let target = week[selected].values
    Card(.filled, size: .sm) {
      VStack(alignment: .leading, spacing: 2) {
        Text("Activity")
          .textStyle(.lg, weight: .semibold)
        Text("This week · pick a day")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .padding(.horizontal, 16)
      CardContent {
        VStack(spacing: 24) {
          HStack(spacing: 4) {
            ForEach(week.indices, id: \.self) { index in
              day(index)
            }
          }
          .hapticFeedback(.selection, trigger: selected)
          .accessibilityElement(children: .contain)
          .accessibilityLabel("Day")
          ViewThatFits(in: .horizontal) {
            HStack(spacing: 32) {
              Rings(values: values, size: 260, bar: 24)
              stats(target)
            }
            VStack(spacing: 24) {
              Rings(values: values, size: 260, bar: 24)
              stats(target)
            }
          }
          .frame(maxWidth: .infinity)
        }
      }
    }
    .onAppear { fill(target) }
    .onChange(of: selected) { _, _ in fill(week[selected].values) }
  }

  private func fill(_ target: [Double]) {
    guard values != target else { return }
    withAnimation(reduceMotion ? nil : .timingCurve(0.65, 0, 0.35, 1, duration: 0.75)) {
      values = target
    }
  }

  private func day(_ index: Int) -> some View {
    let active = index == selected
    return Button {
      selected = index
    } label: {
      VStack(spacing: 6) {
        Text(verbatim: String(week[index].day.prefix(3)))
          .textStyle(.xs, weight: .semibold)
          .foregroundStyle(active ? EsuissColor.foreground : EsuissColor.mutedForeground)
        Rings(values: week[index].values, size: 40, bar: 4)
      }
      .padding(.vertical, 8)
      .frame(maxWidth: .infinity)
      .background(active ? EsuissColor.background : .clear, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
      .contentShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
    }
    .buttonStyle(.plain)
    .accessibilityLabel(Text(LocalizedStringKey(week[index].day)))
    .accessibilityAddTraits(active ? .isSelected : [])
  }

  private func stats(_ target: [Double]) -> some View {
    VStack(alignment: .leading, spacing: 16) {
      ForEach(rings.indices, id: \.self) { index in
        let ring = rings[index]
        let percent = Int((target[index] / ring.goal * 100).rounded())
        VStack(alignment: .leading, spacing: 2) {
          Text(LocalizedStringKey(ring.label))
            .textStyle(.base, weight: .semibold)
          HStack(alignment: .firstTextBaseline, spacing: 2) {
            Text(verbatim: "\(Int(target[index]))/\(Int(ring.goal))")
              .textStyle(.xl3, weight: .semibold)
              .monospacedDigit()
              .contentTransition(.numericText(value: target[index]))
            Text(LocalizedStringKey(ring.unit.uppercased()))
              .textStyle(.base, weight: .semibold)
          }
          .foregroundStyle(tint(ring.color))
          Text(verbatim: "\(percent)%\(percent >= 100 ? " · goal met" : "")")
            .textStyle(.xs)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
        .accessibilityElement(children: .combine)
      }
    }
    .frame(minWidth: 176, alignment: .leading)
    .animation(reduceMotion ? nil : .default, value: selected)
  }

  private func tint(_ color: Color) -> Color {
    guard colorScheme != .dark else { return color }
    if #available(iOS 18, *) {
      return color.mix(with: EsuissColor.foreground, by: 0.45)
    }
    return color
  }
}
```

```kt title="kotlin/charts/RadialChartInteractive.kt"
package com.esuiss.ui.charts

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.formReducedMotion
import com.esuiss.ui.components.rememberHaptics
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlin.math.roundToInt
import kotlinx.coroutines.launch

private class RadialInteractiveActivityRing(val label: String, val unit: String, val goal: Double, val color: Colors.() -> Color)

private val activityRings = listOf(
    RadialInteractiveActivityRing("Move", "kcal", 600.0) { Palette.red500 },
    RadialInteractiveActivityRing("Exercise", "min", 30.0) { Palette.green500 },
    RadialInteractiveActivityRing("Stand", "hr", 12.0) { Palette.cyan500 },
)

private class RadialInteractiveActivityDay(val day: String, val values: List<Double>)

private val week = listOf(
    RadialInteractiveActivityDay("Monday", listOf(520.0, 34.0, 11.0)),
    RadialInteractiveActivityDay("Tuesday", listOf(610.0, 42.0, 12.0)),
    RadialInteractiveActivityDay("Wednesday", listOf(380.0, 18.0, 9.0)),
    RadialInteractiveActivityDay("Thursday", listOf(640.0, 45.0, 12.0)),
    RadialInteractiveActivityDay("Friday", listOf(470.0, 28.0, 10.0)),
    RadialInteractiveActivityDay("Saturday", listOf(720.0, 62.0, 12.0)),
    RadialInteractiveActivityDay("Sunday", listOf(486.0, 24.0, 10.0)),
)

private val FillEasing = CubicBezierEasing(0.65f, 0f, 0.35f, 1f)

@Composable
private fun Rings(values: List<Double>, size: Int, bar: Int, modifier: Modifier = Modifier) {
    val colors = EsuissTheme.colors
    RingsCanvas(
        values = activityRings.mapIndexed { index, ring -> (values[index] / ring.goal).toFloat() },
        colors = activityRings.map { it.color(colors) },
        size = size.dp,
        bar = bar.dp,
        gap = maxOf(1f, bar * 0.12f).dp,
        modifier = modifier.clearAndSetSemantics {},
    )
}

@Composable
fun RadialChartInteractive(modifier: Modifier = Modifier) {
    val colors = EsuissTheme.colors
    val reduced = formReducedMotion()
    val haptics = rememberHaptics()
    var selected by remember { mutableIntStateOf(6) }
    val target = week[selected].values
    val animated = remember { activityRings.map { Animatable(0f) } }
    LaunchedEffect(selected) {
        animated.forEachIndexed { index, value ->
            launch {
                if (reduced) value.snapTo(target[index].toFloat()) else value.animateTo(target[index].toFloat(), tween(750, easing = FillEasing))
            }
        }
    }
    val values = animated.map { it.value.toDouble() }
    Card(modifier, variant = CardVariant.Filled, size = CardSize.Sm) {
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                BasicText("Activity", style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                BasicText("This week · pick a day", style = Typography.sm.copy(color = colors.mutedForeground))
            }
        }
        CardContent(Modifier.padding(top = 20.dp)) {
            Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
                Row(
                    Modifier
                        .fillMaxWidth()
                        .selectableGroup()
                        .semantics { contentDescription = "Day" },
                    horizontalArrangement = Arrangement.spacedBy(4.dp),
                ) {
                    week.forEachIndexed { index, day ->
                        val active = index == selected
                        val shape = RoundedCornerShape(Radius.xl)
                        Column(
                            Modifier
                                .weight(1f)
                                .clip(shape)
                                .background(if (active) colors.background else Color.Transparent, shape)
                                .selectable(
                                    selected = active,
                                    interactionSource = remember { MutableInteractionSource() },
                                    indication = null,
                                    role = Role.Tab,
                                ) {
                                    if (!active) haptics.selection()
                                    selected = index
                                }
                                .semantics { contentDescription = day.day }
                                .padding(vertical = 8.dp),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.spacedBy(6.dp),
                        ) {
                            BasicText(
                                day.day.take(3),
                                Modifier.clearAndSetSemantics {},
                                style = Typography.xs.copy(
                                    color = if (active) colors.foreground else colors.mutedForeground,
                                    fontWeight = FontWeight.SemiBold,
                                ),
                            )
                            Rings(day.values, size = 40, bar = 4)
                        }
                    }
                }
                BoxWithConstraints(Modifier.fillMaxWidth()) {
                    if (maxWidth >= 468.dp) {
                        Row(
                            Modifier.align(Alignment.Center),
                            horizontalArrangement = Arrangement.spacedBy(32.dp),
                            verticalAlignment = Alignment.CenterVertically,
                        ) {
                            Rings(values, size = 260, bar = 24)
                            RingStats(values, target)
                        }
                    } else {
                        Column(
                            Modifier.fillMaxWidth(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.spacedBy(24.dp),
                        ) {
                            Rings(values, size = 260, bar = 24)
                            RingStats(values, target)
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun RingStats(values: List<Double>, target: List<Double>) {
    val colors = EsuissTheme.colors
    Column(
        Modifier
            .widthIn(min = 176.dp)
            .semantics { liveRegion = LiveRegionMode.Polite },
        verticalArrangement = Arrangement.spacedBy(16.dp),
    ) {
        activityRings.forEachIndexed { index, ring ->
            val percent = (target[index] / ring.goal * 100).roundToInt()
            val color = ring.color(colors)
            val tint = if (colors.isDark) color else lerp(color, colors.foreground, 0.45f)
            Column(Modifier.semantics(mergeDescendants = true) {}, verticalArrangement = Arrangement.spacedBy(2.dp)) {
                BasicText(ring.label, style = Typography.base.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
                    BasicText(
                        "${values[index].roundToInt()}/${ring.goal.roundToInt()}",
                        Modifier.alignByBaseline(),
                        style = Typography.xl3.copy(color = tint, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                    )
                    BasicText(
                        ring.unit.uppercase(),
                        Modifier.alignByBaseline(),
                        style = Typography.base.copy(color = tint, fontWeight = FontWeight.SemiBold),
                    )
                }
                BasicText(
                    "$percent%${if (percent >= 100) " · goal met" else ""}",
                    style = Typography.xs.copy(color = colors.mutedForeground),
                )
            }
        }
    }
}
```

A `Card` with a `ToggleGroup` of seven days, each showing small rings, above a large ring view and per-ring totals. The rings are drawn with `Svg` circles colored through `ChartPalette`; `RINGS` sets each label, unit, goal and color, and `WEEK` holds three values per day. Picking a day updates the large rings and the totals, which show the value, goal and percentage, with a note when a goal is met.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/radial-chart-interactive
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add radial-chart-interactive
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add radial-chart-interactive --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add radial-chart-interactive --platform kotlin
```

The CLI also adds what it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn react-native-svg
```

2. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/charts/radial-chart-interactive.tsx`:

- `uniwind/components/charts/radial-chart-interactive.tsx`: shown above

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn react-native-svg
```

2. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/charts/radial-chart-interactive.tsx`:

```tsx title="nativewind/components/charts/radial-chart-interactive.tsx"
import { cn } from "cn"
import * as React from "react"
import { View } from "react-native"
import Svg, { Circle, G } from "react-native-svg"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { type ChartColor, ChartPalette } from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

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

const RINGS: {
  key: Ring
  label: string
  unit: string
  goal: number
  color: ChartColor
  text: string
}[] = [
  {
    key: "move",
    label: "Move",
    unit: "kcal",
    goal: 600,
    color: "red-500",
    text: "text-red-500",
  },
  {
    key: "exercise",
    label: "Exercise",
    unit: "min",
    goal: 30,
    color: "green-500",
    text: "text-green-500",
  },
  {
    key: "stand",
    label: "Stand",
    unit: "hr",
    goal: 12,
    color: "cyan-500",
    text: "text-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 NO_ACTIVITY = RINGS.map(() => 0)

function Rings({
  values,
  size,
  bar,
}: {
  values: number[]
  size: number
  bar: number
}) {
  return (
    <View
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants"
    >
      <ChartPalette colors={RINGS.map((ring) => ring.color)}>
        {(palette) => (
          <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
            <G rotation={-90} origin={`${size / 2}, ${size / 2}`}>
              {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)
                )
                const color = palette[ring.color]
                return (
                  <G key={ring.key}>
                    <Circle
                      cx={size / 2}
                      cy={size / 2}
                      r={radius}
                      fill="none"
                      strokeWidth={bar}
                      stroke={color}
                      strokeOpacity={0.22}
                    />
                    {progress > 0.001 ? (
                      <Circle
                        cx={size / 2}
                        cy={size / 2}
                        r={radius}
                        fill="none"
                        strokeWidth={bar}
                        strokeLinecap="round"
                        stroke={color}
                        strokeDasharray={`${progress * circumference} ${circumference}`}
                      />
                    ) : null}
                  </G>
                )
              })}
            </G>
          </Svg>
        )}
      </ChartPalette>
    </View>
  )
}

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

  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-lg font-semibold">Activity</Text>
        <Text className="text-sm text-muted-foreground">
          This week · pick a day
        </Text>
      </CardHeader>
      <CardContent className="gap-6">
        <ToggleGroup
          spacing={1}
          value={[String(selected)]}
          onValueChange={(value) => {
            if (value[0] !== undefined) setSelected(Number(value[0]))
          }}
          accessibilityLabel="Day"
          className="w-full self-stretch"
        >
          {WEEK.map((entry, index) => (
            <ToggleGroupItem
              key={entry.day}
              value={String(index)}
              accessibilityLabel={entry.day}
              className={(state) =>
                cn(
                  "h-auto min-w-0 flex-1 shrink flex-col gap-1.5 rounded-xl px-0 py-2",
                  state.pressed && "bg-background active:opacity-80"
                )
              }
            >
              <Text
                className={cn(
                  "text-xs font-semibold",
                  selected === index
                    ? "text-foreground"
                    : "text-muted-foreground"
                )}
              >
                {entry.day.slice(0, 3)}
              </Text>
              <Rings values={entry.values} size={40} bar={4} />
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <View className="flex-row flex-wrap items-center justify-center gap-8">
          <Rings values={values} size={260} bar={24} />
          <View className="min-w-44 gap-4" accessibilityLiveRegion="polite">
            {RINGS.map((ring, index) => {
              const percent = Math.round(
                ((values[index] ?? 0) / ring.goal) * 100
              )
              return (
                <View key={ring.key} accessible className="gap-0.5">
                  <Text className="text-base font-semibold">{ring.label}</Text>
                  <View className="flex-row items-baseline gap-0.5">
                    <Text
                      className={cn(
                        "text-3xl font-semibold tracking-tight tabular-nums",
                        ring.text
                      )}
                    >
                      {`${Math.round(values[index] ?? 0)}/${ring.goal}`}
                    </Text>
                    <Text
                      className={cn(
                        "text-base font-semibold uppercase",
                        ring.text
                      )}
                    >
                      {ring.unit}
                    </Text>
                  </View>
                  <Text className="text-xs text-muted-foreground">
                    {`${percent}%${percent >= 100 ? " · goal met" : ""}`}
                  </Text>
                </View>
              )
            })}
          </View>
        </View>
      </CardContent>
    </Card>
  )
}
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install react-native-svg
```

2. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/charts/radial-chart-interactive.tsx`:

```tsx title="stylesheet/components/charts/radial-chart-interactive.tsx"
import * as React from "react"
import { StyleSheet, View } from "react-native"
import Svg, { Circle, G } from "react-native-svg"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { type ChartColor, ChartPalette } from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Tone = keyof ThemeColors | keyof typeof palette

function tone(colors: ThemeColors, key: Tone) {
  return { ...colors, ...palette }[key]
}

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

const RINGS: {
  key: Ring
  label: string
  unit: string
  goal: number
  color: ChartColor
  swatch: Tone
}[] = [
  {
    key: "move",
    label: "Move",
    unit: "kcal",
    goal: 600,
    color: "red-500",
    swatch: "red500",
  },
  {
    key: "exercise",
    label: "Exercise",
    unit: "min",
    goal: 30,
    color: "green-500",
    swatch: "green500",
  },
  {
    key: "stand",
    label: "Stand",
    unit: "hr",
    goal: 12,
    color: "cyan-500",
    swatch: "cyan500",
  },
]

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 NO_ACTIVITY = RINGS.map(() => 0)

const styles = StyleSheet.create({
  header: { gap: 2 },
  title: { ...fontSize.lg, fontWeight: "600" },
  caption: { ...fontSize.sm },
  content: { gap: 24 },
  days: { width: "100%", alignSelf: "stretch" },
  day: {
    height: "auto",
    minWidth: 0,
    flex: 1,
    flexShrink: 1,
    flexDirection: "column",
    gap: 6,
    borderRadius: 12,
    paddingHorizontal: 0,
    paddingVertical: 8,
  },
  dayLabel: { ...fontSize.xs, fontWeight: "600" },
  summary: {
    flexDirection: "row",
    flexWrap: "wrap",
    alignItems: "center",
    justifyContent: "center",
    gap: 32,
  },
  rings: { minWidth: 176, gap: 16 },
  ring: { gap: 2 },
  ringLabel: { ...fontSize.base, fontWeight: "600" },
  reading: { flexDirection: "row", alignItems: "baseline", gap: 2 },
  value: {
    ...fontSize["3xl"],
    fontWeight: "600",
    letterSpacing: -0.75,
    fontVariant: ["tabular-nums"],
  },
  unit: { ...fontSize.base, fontWeight: "600", textTransform: "uppercase" },
  percent: { ...fontSize.xs },
})

function Rings({
  values,
  size,
  bar,
}: {
  values: number[]
  size: number
  bar: number
}) {
  return (
    <View
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants"
    >
      <ChartPalette colors={RINGS.map((ring) => ring.color)}>
        {(palette) => (
          <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
            <G rotation={-90} origin={`${size / 2}, ${size / 2}`}>
              {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)
                )
                const color = palette[ring.color]
                return (
                  <G key={ring.key}>
                    <Circle
                      cx={size / 2}
                      cy={size / 2}
                      r={radius}
                      fill="none"
                      strokeWidth={bar}
                      stroke={color}
                      strokeOpacity={0.22}
                    />
                    {progress > 0.001 ? (
                      <Circle
                        cx={size / 2}
                        cy={size / 2}
                        r={radius}
                        fill="none"
                        strokeWidth={bar}
                        strokeLinecap="round"
                        stroke={color}
                        strokeDasharray={`${progress * circumference} ${circumference}`}
                      />
                    ) : null}
                  </G>
                )
              })}
            </G>
          </Svg>
        )}
      </ChartPalette>
    </View>
  )
}

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

  return (
    <Card variant="filled" size="sm">
      <CardHeader style={styles.header}>
        <Text style={styles.title}>Activity</Text>
        <Text style={[styles.caption, { color: colors.mutedForeground }]}>
          This week · pick a day
        </Text>
      </CardHeader>
      <CardContent style={styles.content}>
        <ToggleGroup
          spacing={1}
          value={[String(selected)]}
          onValueChange={(value) => {
            if (value[0] !== undefined) setSelected(Number(value[0]))
          }}
          accessibilityLabel="Day"
          style={styles.days}
        >
          {WEEK.map((entry, index) => (
            <ToggleGroupItem
              key={entry.day}
              value={String(index)}
              accessibilityLabel={entry.day}
              style={(state) => [
                styles.day,
                state.pressed && { backgroundColor: colors.background },
              ]}
            >
              <Text
                style={[
                  styles.dayLabel,
                  {
                    color:
                      selected === index
                        ? colors.foreground
                        : colors.mutedForeground,
                  },
                ]}
              >
                {entry.day.slice(0, 3)}
              </Text>
              <Rings values={entry.values} size={40} bar={4} />
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <View style={styles.summary}>
          <Rings values={values} size={260} bar={24} />
          <View style={styles.rings} accessibilityLiveRegion="polite">
            {RINGS.map((ring, index) => {
              const percent = Math.round(
                ((values[index] ?? 0) / ring.goal) * 100
              )
              return (
                <View key={ring.key} accessible style={styles.ring}>
                  <Text style={styles.ringLabel}>{ring.label}</Text>
                  <View style={styles.reading}>
                    <Text
                      style={[
                        styles.value,
                        { color: tone(colors, ring.swatch) },
                      ]}
                    >
                      {`${Math.round(values[index] ?? 0)}/${ring.goal}`}
                    </Text>
                    <Text
                      style={[
                        styles.unit,
                        { color: tone(colors, ring.swatch) },
                      ]}
                    >
                      {ring.unit}
                    </Text>
                  </View>
                  <Text
                    style={[styles.percent, { color: colors.mutedForeground }]}
                  >
                    {`${percent}%${percent >= 100 ? " · goal met" : ""}`}
                  </Text>
                </View>
              )
            })}
          </View>
        </View>
      </CardContent>
    </Card>
  )
}
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

3. Copy the source and every file it uses:

- `swift/Charts/RadialChartInteractive.swift`: shown above

- `swift/Components/Card.swift`: see [Card](https://ui.suiss.app/docs/components/card.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

4. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/charts/RadialChartInteractive.kt`: shown above

- `kotlin/charts/RadialChartRings.kt`: see [Radial Chart Rings](https://ui.suiss.app/docs/charts/radial-chart-rings.md)

- `kotlin/components/Card.kt`: see [Card](https://ui.suiss.app/docs/components/card.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { RadialChartInteractive } from "@/components/charts/radial-chart-interactive"
```

```tsx
<RadialChartInteractive />
```

Replace `WEEK` with your daily values and adjust the goals, units and colors in `RINGS`. The component takes no props.

