
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.
blocks/charts/pie-chart-01/index.tsx
"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>
)
}
