Pie Chart

Pie and donut chart with smooth hover animations and composable center content

Installation

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

Dependencies

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

Examples

Custom Center

"use client"
import { useState } from "react"
import PieChart, { PieCenter, PieSlice, type PieData } from "@/components/vritti/pie-chart"

const salesData: PieData[] = [
  { label: "Electronics", value: 4250, color: "#0ea5e9" },
  { label: "Clothing", value: 3120, color: "#a855f7" },
  { label: "Food", value: 2100, color: "#f59e0b" },
  { label: "Home", value: 1580, color: "#10b981" },
  { label: "Other", value: 1050, color: "#ef4444" },
]

const total = salesData.reduce((sum, d) => sum + d.value, 0)

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

  return (
    <div className="flex justify-center p-4">
      <PieChart
        data={salesData}
        hoveredIndex={hoveredIndex}
        innerRadius={80}
        onHoverChange={setHoveredIndex}
        size={300}
      >
        {salesData.map((item, index) => (
          <PieSlice index={index} key={item.label} />
        ))}
        <PieCenter>
          {({ value, label, isHovered, data }) => (
            <div className="text-center">
              <div
                className="font-bold text-3xl"
                style={{ color: isHovered ? data.color : undefined }}
              >
                {value.toLocaleString()}
              </div>
              <div className="text-muted-foreground text-sm">{label}</div>
              {isHovered && (
                <div className="mt-1 text-muted-foreground text-xs">
                  {((data.value / total) * 100).toFixed(1)}% of total
                </div>
              )}
            </div>
          )}
        </PieCenter>
      </PieChart>
    </div>
  )
}

Donut

"use client"
import PieChart, { PieCenter, PieSlice } from "@/components/vritti/pie-chart"

const data = [
  { label: "React", value: 400, color: "var(--chart-1)" },
  { label: "Vue", value: 200, color: "var(--chart-2)" },
  { label: "Angular", value: 150, color: "var(--chart-3)" },
  { label: "Svelte", value: 100, color: "var(--chart-4)" },
]

export function PieChartDonutDemo() {
  return (
    <div className="flex justify-center p-8">
      <PieChart cornerRadius={4} data={data} innerRadius={80} padAngle={0.04} size={300}>
        {data.map((item, index) => (
          <PieSlice index={index} key={item.label} />
        ))}
        <PieCenter defaultLabel="Total" />
      </PieChart>
    </div>
  )
}

Gradient

"use client"
import PieChart, { PieData, PieSlice, RadialGradient } from "@/components/vritti/pie-chart"

const gradientData: PieData[] = [
  { label: "Segment A", value: 40 },
  { label: "Segment B", value: 30 },
  { label: "Segment C", value: 30 },
]

export function PieChartGradientDemo() {
  return (
    <div className="flex justify-center p-4">
      <PieChart data={gradientData} size={280}>
        <RadialGradient
          from="#0ea5e9"
          fromOffset="0%"
          id="pie-gradient-1"
          to="#06b6d4"
          toOffset="100%"
        />
        <RadialGradient
          from="#a855f7"
          fromOffset="0%"
          id="pie-gradient-2"
          to="#ec4899"
          toOffset="100%"
        />
        <RadialGradient
          from="#f59e0b"
          fromOffset="0%"
          id="pie-gradient-3"
          to="#ef4444"
          toOffset="100%"
        />
        <PieSlice fill="url(#pie-gradient-1)" index={0} />
        <PieSlice fill="url(#pie-gradient-2)" index={1} />
        <PieSlice fill="url(#pie-gradient-3)" index={2} />
      </PieChart>
    </div>
  )
}

Hover

"use client"
import { useState } from "react"
import PieChart, {
  PieSlice,
  type PieSliceHoverEffect,
  type PieData,
} from "@/components/vritti/pie-chart"

const salesData: PieData[] = [
  { label: "Electronics", value: 4250, color: "#0ea5e9" },
  { label: "Clothing", value: 3120, color: "#a855f7" },
  { label: "Food", value: 2100, color: "#f59e0b" },
  { label: "Home", value: 1580, color: "#10b981" },
  { label: "Other", value: 1050, color: "#ef4444" },
]

export function PieChartHoverDemo() {
  const [hoverEffect, setHoverEffect] = useState<PieSliceHoverEffect>("translate")

  return (
    <div className="flex flex-col items-center gap-6 p-4">
      <div className="flex items-center gap-3">
        <span className="text-muted-foreground text-sm">Hover Effect:</span>
        <div className="flex gap-2">
          {(["translate", "grow", "none"] as PieSliceHoverEffect[]).map((effect) => (
            <button
              key={effect}
              type="button"
              onClick={() => setHoverEffect(effect)}
              className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
                hoverEffect === effect
                  ? "bg-primary text-primary-foreground"
                  : "bg-muted text-muted-foreground hover:bg-muted/80"
              }`}
            >
              {effect.charAt(0).toUpperCase() + effect.slice(1)}
            </button>
          ))}
        </div>
      </div>

      <PieChart data={salesData} size={280}>
        {salesData.map((item, index) => (
          <PieSlice hoverEffect={hoverEffect} index={index} key={item.label} />
        ))}
      </PieChart>
    </div>
  )
}

Interactive

"use client"
import PieChart, { PieSlice, PieCenter } from "@/components/vritti/pie-chart"

const data = [
  { label: "Marketing", value: 4200, color: "#0ea5e9" },
  { label: "Engineering", value: 8500, color: "#a855f7" },
  { label: "Sales", value: 3100, color: "#f59e0b" },
  { label: "Design", value: 2800, color: "#10b981" },
  { label: "Operations", value: 1900, color: "#ef4444" },
]

export function PieChartInteractiveDemo() {
  return (
    <div className="flex justify-center p-4">
      <PieChart
        data={data}
        size={300}
        innerRadius={70}
        padAngle={0.04}
        cornerRadius={4}
        hoverOffset={12}
      >
        {data.map((item, index) => (
          <PieSlice index={index} key={item.label} hoverEffect="grow" />
        ))}
        <PieCenter
          defaultLabel="Budget"
          formatOptions={{ style: "currency", currency: "USD", notation: "compact" }}
        />
      </PieChart>
    </div>
  )
}

Legend

"use client"
import { useState } from "react"
import PieChart, {
  Legend,
  LegendItem,
  LegendLabel,
  LegendMarker,
  LegendValue,
  PieSlice,
  type LegendItemData,
  type PieData,
} from "@/components/vritti/pie-chart"

const salesData: PieData[] = [
  { label: "Electronics", value: 4250, color: "#0ea5e9" },
  { label: "Clothing", value: 3120, color: "#a855f7" },
  { label: "Food", value: 2100, color: "#f59e0b" },
  { label: "Home", value: 1580, color: "#10b981" },
  { label: "Other", value: 1050, color: "#ef4444" },
]

const total = salesData.reduce((sum, d) => sum + d.value, 0)

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

export function PieChartLegendDemo() {
  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">
      <PieChart
        data={salesData}
        hoveredIndex={hoveredIndex}
        onHoverChange={setHoveredIndex}
        size={280}
      >
        {salesData.map((item, index) => (
          <PieSlice index={index} key={item.label} />
        ))}
      </PieChart>

      <Legend
        hoveredIndex={hoveredIndex}
        items={legendItems}
        onHoverChange={setHoveredIndex}
        title="Sales by Category"
      >
        <LegendItem className="flex items-center gap-3">
          <LegendMarker />
          <LegendLabel className="flex-1 text-sm font-medium" />
          <LegendValue showPercentage />
        </LegendItem>
      </Legend>
    </div>
  )
}

Nested

"use client"
import PieChart, { PieSlice, PieCenter } from "@/components/vritti/pie-chart"

const outerData = [
  { label: "Chrome", value: 520, color: "#4285F4" },
  { label: "Safari", value: 190, color: "#FF9500" },
  { label: "Firefox", value: 130, color: "#FF6611" },
  { label: "Edge", value: 110, color: "#0078D4" },
  { label: "Other", value: 50, color: "#999999" },
]

const innerData = [
  { label: "Desktop", value: 620, color: "var(--chart-1)" },
  { label: "Mobile", value: 310, color: "var(--chart-2)" },
  { label: "Tablet", value: 70, color: "var(--chart-3)" },
]

export function PieChartNestedDemo() {
  return (
    <div className="flex flex-col items-center gap-8 p-4">
      <div className="flex items-center gap-12">
        <div className="flex flex-col items-center gap-2">
          <span className="text-sm font-medium text-muted-foreground">By Browser</span>
          <PieChart data={outerData} size={220} innerRadius={60} padAngle={0.03} cornerRadius={3}>
            {outerData.map((item, index) => (
              <PieSlice index={index} key={item.label} />
            ))}
          </PieChart>
        </div>
        <div className="flex flex-col items-center gap-2">
          <span className="text-sm font-medium text-muted-foreground">By Device</span>
          <PieChart data={innerData} size={220} innerRadius={60} padAngle={0.03} cornerRadius={3}>
            {innerData.map((item, index) => (
              <PieSlice index={index} key={item.label} />
            ))}
            <PieCenter defaultLabel="Total" />
          </PieChart>
        </div>
      </div>
    </div>
  )
}

Pattern

"use client"
import PieChart, { PatternLines, PieData, PieSlice } from "@/components/vritti/pie-chart"

const patternData: PieData[] = [
  { label: "Category A", value: 35 },
  { label: "Category B", value: 25 },
  { label: "Category C", value: 20 },
  { label: "Category D", value: 20 },
]

export function PieChartPatternDemo() {
  return (
    <div className="flex justify-center p-4">
      <PieChart data={patternData} size={280}>
        <PatternLines
          height={6}
          id="pie-pattern-1"
          orientation={["diagonal"]}
          stroke="var(--chart-1)"
          strokeWidth={1}
          width={6}
        />
        <PatternLines
          height={6}
          id="pie-pattern-2"
          orientation={["horizontal"]}
          stroke="var(--chart-2)"
          strokeWidth={1}
          width={6}
        />
        <PatternLines
          height={6}
          id="pie-pattern-3"
          orientation={["vertical"]}
          stroke="var(--chart-3)"
          strokeWidth={1}
          width={6}
        />
        <PatternLines
          height={8}
          id="pie-pattern-4"
          orientation={["diagonalRightToLeft"]}
          stroke="var(--chart-4)"
          strokeWidth={1}
          width={8}
        />
        <PieSlice fill="url(#pie-pattern-1)" index={0} />
        <PieSlice fill="url(#pie-pattern-2)" index={1} />
        <PieSlice fill="url(#pie-pattern-3)" index={2} />
        <PieSlice fill="url(#pie-pattern-4)" index={3} />
      </PieChart>
    </div>
  )
}

Semi

"use client"
import PieChart, { PieSlice, PieCenter } from "@/components/vritti/pie-chart"

const data = [
  { label: "Completed", value: 72, color: "var(--chart-1)" },
  { label: "In Progress", value: 18, color: "var(--chart-2)" },
  { label: "Remaining", value: 10, color: "var(--chart-3)" },
]

export function PieChartSemiDemo() {
  return (
    <div className="flex justify-center p-4">
      <PieChart
        data={data}
        size={300}
        innerRadius={80}
        padAngle={0.03}
        cornerRadius={4}
        startAngle={-Math.PI / 2}
        endAngle={Math.PI / 2}
      >
        {data.map((item, index) => (
          <PieSlice index={index} key={item.label} />
        ))}
        <PieCenter defaultLabel="Progress" />
      </PieChart>
    </div>
  )
}