Charts

Donut Chart with Center Total

Card with a donut chart of minutes by genre, the total printed in the center, and a footer list giving each genre's swatch, minutes and share. Client component on recharts 3.8.

Free, MIT licenseOpen in Browse
"use client"

import { Label, Pie, PieChart } from "recharts"

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from "@/components/ui/chart"

// The copy: change text here, not in the markup.
const COPY = {
  title: "Listening by genre",
  description: "Minutes listened in October",
  total: "minutes",
  breakdown: "Minutes by genre",
} as const

const GENRES = [
  { genre: "ambient", label: "Ambient", minutes: 6120, fill: "var(--chart-1)" },
  { genre: "jazz", label: "Jazz", minutes: 4380, fill: "var(--chart-2)" },
  {
    genre: "classical",
    label: "Classical",
    minutes: 3410,
    fill: "var(--chart-3)",
  },
  {
    genre: "electronic",
    label: "Electronic",
    minutes: 2760,
    fill: "var(--chart-4)",
  },
  { genre: "folk", label: "Folk", minutes: 1730, fill: "var(--chart-5)" },
]

const CHART_CONFIG = {
  minutes: { label: "Minutes" },
  ...Object.fromEntries(
    GENRES.map((genre) => [
      genre.genre,
      { label: genre.label, color: genre.fill },
    ])
  ),
} satisfies ChartConfig

const TOTAL = GENRES.reduce((sum, genre) => sum + genre.minutes, 0)

export default function PieChart01() {
  return (
    <Card className="mx-auto w-full max-w-md">
      <CardHeader>
        <CardTitle>
          <h2>{COPY.title}</h2>
        </CardTitle>
        <CardDescription>{COPY.description}</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={CHART_CONFIG}
          className="mx-auto aspect-square max-h-64"
        >
          <PieChart accessibilityLayer>
            <ChartTooltip
              cursor={false}
              content={<ChartTooltipContent hideLabel />}
            />
            {/* A card-colored gap parts the slices; the list below holds the values. */}
            <Pie
              data={GENRES}
              dataKey="minutes"
              nameKey="genre"
              innerRadius={64}
              stroke="var(--card)"
              strokeWidth={3}
            >
              <Label
                content={({ viewBox }) => {
                  if (!viewBox || !("cx" in viewBox)) return null
                  return (
                    <text
                      x={viewBox.cx}
                      y={viewBox.cy}
                      textAnchor="middle"
                      dominantBaseline="middle"
                    >
                      <tspan
                        x={viewBox.cx}
                        y={viewBox.cy}
                        className="fill-foreground text-2xl font-medium tabular-nums"
                      >
                        {TOTAL.toLocaleString("en-US")}
                      </tspan>
                      <tspan
                        x={viewBox.cx}
                        y={(viewBox.cy ?? 0) + 22}
                        className="fill-muted-foreground"
                      >
                        {COPY.total}
                      </tspan>
                    </text>
                  )
                }}
              />
            </Pie>
          </PieChart>
        </ChartContainer>
      </CardContent>
      <CardFooter>
        <ul aria-label={COPY.breakdown} className="grid w-full gap-2 text-sm">
          {GENRES.map((genre) => (
            <li key={genre.genre} className="flex items-center gap-2">
              <span
                aria-hidden="true"
                className="size-2.5 shrink-0 rounded-[2px]"
                style={{ backgroundColor: genre.fill }}
              />
              {genre.label}
              <span className="ml-auto tabular-nums">
                {genre.minutes.toLocaleString("en-US")}
              </span>
              <span className="w-10 text-right text-muted-foreground tabular-nums">
                {Math.round((genre.minutes / TOTAL) * 100)}%
              </span>
            </li>
          ))}
        </ul>
      </CardFooter>
    </Card>
  )
}

Installation

pnpm dlx shadcn add @01orbital/pie-chart-01
npx shadcn add @01orbital/pie-chart-01
yarn shadcn add @01orbital/pie-chart-01
bunx --bun shadcn add @01orbital/pie-chart-01

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/blocks/charts/pie-chart-01/index.tsx, as source you own and edit.

Usage

import PieChart01 from "@/components/blocks/charts/pie-chart-01"

export default function Example() {
  return <PieChart01 />
}

Dependencies

npm: recharts@3.8.0. The CLI installs them for you.

Registry pieces it installs with it:

  • card
  • chart