pie-chart-02
Fine donut chart for dashboards: a thin genre ring with blue and neutral segments, a center total and a complete text breakdown.
pnpm dlx shadcn add @01orbital/pie-chart-02"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"
import { Separator } from "@/components/ui/separator"
// 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(--art-accent)",
},
{ genre: "jazz", label: "Jazz", minutes: 4380, fill: "var(--foreground)" },
{
genre: "classical",
label: "Classical",
minutes: 3410,
fill: "var(--muted-foreground)",
},
{
genre: "electronic",
label: "Electronic",
minutes: 2760,
fill: "color-mix(in oklab, var(--foreground) 75%, var(--card))",
},
{
genre: "folk",
label: "Folk",
minutes: 1730,
fill: "color-mix(in oklab, var(--foreground) 60%, var(--card))",
},
]
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 PieChart02() {
return (
<Card className="mx-auto w-full max-w-[26rem] gap-8 [--card-spacing:24px]">
<CardHeader>
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{COPY.description}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
config={CHART_CONFIG}
className="mx-auto aspect-square w-full max-w-56"
>
<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="88%"
outerRadius="94%"
stroke="var(--card)"
strokeWidth={2}
>
<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}
fontSize={30}
className="fill-foreground tabular-nums"
>
{TOTAL.toLocaleString("en-US")}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 22}
className="fill-muted-foreground font-mono uppercase"
fontSize={10}
>
{COPY.total}
</tspan>
</text>
)
}}
/>
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
<CardFooter className="grid gap-3">
<Separator />
<ul aria-label={COPY.breakdown} className="grid w-full gap-2 text-sm">
{GENRES.map((genre) => (
<li
key={genre.genre}
className="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-1 min-[18rem]:grid-cols-[auto_minmax(0,1fr)_auto_auto]"
>
<span
aria-hidden="true"
className="h-px w-3 shrink-0"
style={{ backgroundColor: genre.fill }}
/>
<span className="min-w-0 wrap-anywhere">{genre.label}</span>
<span className="col-start-2 tabular-nums min-[18rem]:col-start-auto">
{genre.minutes.toLocaleString("en-US")}
</span>
<span className="col-start-2 font-mono text-xs text-muted-foreground tabular-nums min-[18rem]:col-start-auto">
{Math.round((genre.minutes / TOTAL) * 100)}%
</span>
</li>
))}
</ul>
</CardFooter>
</Card>
)
}
Dependencies
npm: recharts@3.8.0. The CLI installs them for you.
Registry pieces it installs with it: