Ring Chart

Concentric ring chart for multi-metric progress and comparison visualization

Installation

pnpm dlx shadcn@latest add "https://vritti.thesatyajit.com/r/ring-chart"

Dependencies

pnpm add @visx/responsive @visx/shape d3-shape motion @number-flow/react

Examples

Fitness

"use client"
import RingChart, { Ring, RingCenter } from "@/components/vritti/ring-chart"

const data = [
  { label: "Steps",      value:  8420, maxValue: 10000, color: "var(--chart-1)" },
  { label: "Calories",   value:  1840, maxValue:  2200, color: "var(--chart-2)" },
  { label: "Active Min", value:    38, maxValue:    60, color: "var(--chart-3)" },
  { label: "Sleep (h)",  value:   7.2, maxValue:     8, color: "var(--chart-4)" },
]

export function RingChartFitnessDemo() {
  return (
    <div className="flex justify-center p-8">
      <RingChart data={data} size={280}>
        {data.map((item, index) => (
          <Ring index={index} key={item.label} />
        ))}
        <RingCenter
          defaultLabel="Today"
          formatOptions={{ maximumFractionDigits: 1 }}
        />
      </RingChart>
    </div>
  )
}

Legend

"use client"
import { useState } from "react"
import RingChart, {
  Legend,
  LegendItem,
  LegendLabel,
  LegendMarker,
  LegendProgress,
  LegendValue,
  Ring,
  RingCenter,
  type LegendItemData,
  type RingData,
} from "@/components/vritti/ring-chart"

const sessionsData: RingData[] = [
  { label: "Organic", value: 4250, maxValue: 5000, color: "#0ea5e9" },
  { label: "Paid", value: 3120, maxValue: 5000, color: "#a855f7" },
  { label: "Email", value: 2100, maxValue: 5000, color: "#f59e0b" },
  { label: "Social", value: 1580, maxValue: 5000, color: "#10b981" },
  { label: "Referral", value: 1050, maxValue: 5000, color: "#ef4444" },
  { label: "Direct", value: 747, maxValue: 5000, color: "#6366f1" },
]

const legendItems: LegendItemData[] = sessionsData.map((d) => ({
  label: d.label,
  value: d.value,
  maxValue: d.maxValue,
  color: d.color ?? "",
}))

export function RingChartLegendDemo() {
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)

  return (
    <div className="flex flex-col items-center justify-center gap-8 p-4 lg:flex-row lg:gap-12">
      <RingChart
        data={sessionsData}
        hoveredIndex={hoveredIndex}
        onHoverChange={setHoveredIndex}
        size={240}
      >
        {sessionsData.map((item, index) => (
          <Ring index={index} key={item.label} />
        ))}
        <RingCenter defaultLabel="Total Sessions" />
      </RingChart>

      <Legend
        hoveredIndex={hoveredIndex}
        items={legendItems}
        onHoverChange={setHoveredIndex}
        title="Sessions by Channel"
      >
        <LegendItem className="grid grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-1">
          <LegendMarker />
          <LegendLabel />
          <LegendValue showPercentage />
          <div className="col-span-full">
            <LegendProgress />
          </div>
        </LegendItem>
      </Legend>
    </div>
  )
}

Progress

"use client"
import RingChart, { Ring } from "@/components/vritti/ring-chart"

const data = [
  { label: "Q1 Target", value: 75, maxValue: 100, color: "hsl(217, 91%, 60%)" },
  { label: "Q2 Target", value: 50, maxValue: 100, color: "hsl(280, 87%, 65%)" },
  { label: "Q3 Target", value: 90, maxValue: 100, color: "hsl(142, 71%, 45%)" },
]

export function RingChartProgressDemo() {
  return (
    <div className="flex justify-center p-8">
      <RingChart data={data} size={260} strokeWidth={16} ringGap={8}>
        {data.map((item, index) => (
          <Ring index={index} key={item.label} />
        ))}
      </RingChart>
    </div>
  )
}