radial-chart-05
Segmented radial chart for budget allocation: a full ring split into three stacked segments, with the total at its center and a text breakdown below.
pnpm dlx shadcn add @01orbital/radial-chart-05"use client"
import {
Label,
PolarAngleAxis,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
const COPY = {
title: "Project budget",
description: "Allocation by team",
unit: "total",
} as const
// Amounts in USD. Each series occupies its share of one complete ring.
const ALLOCATIONS = [
{ key: "design", label: "Design", amount: 42000, color: "var(--chart-2)" },
{
key: "engineering",
label: "Engineering",
amount: 38000,
color: "var(--chart-3)",
},
{
key: "research",
label: "Research",
amount: 20000,
color: "var(--primary)",
},
] as const
const TOTAL = ALLOCATIONS.reduce((sum, item) => sum + item.amount, 0)
const DATA = [
Object.fromEntries(ALLOCATIONS.map((item) => [item.key, item.amount])),
]
const TOTAL_LABEL = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 0,
}).format(TOTAL)
const CHART_CONFIG = Object.fromEntries(
ALLOCATIONS.map((item) => [
item.key,
{ label: item.label, color: item.color },
])
) satisfies ChartConfig
/** A segmented budget ring with a center total and a full text breakdown. */
export default function RadialChart05() {
return (
<Card className="mx-auto w-full max-w-sm">
<CardHeader>
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{COPY.description}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
config={CHART_CONFIG}
role="img"
aria-label={`Project budget: ${TOTAL.toLocaleString("en-US")} dollars. ${ALLOCATIONS.map((item) => `${item.label}: ${item.amount.toLocaleString("en-US")} dollars`).join(". ")}.`}
className="mx-auto aspect-square w-full max-w-64"
>
<RadialBarChart
accessibilityLayer={false}
data={DATA}
startAngle={90}
endAngle={-270}
innerRadius="70%"
outerRadius="100%"
>
<PolarAngleAxis type="number" domain={[0, TOTAL]} tick={false} />
{ALLOCATIONS.map((item) => (
<RadialBar
key={item.key}
dataKey={item.key}
stackId="budget"
fill={`var(--color-${item.key})`}
stroke="var(--card)"
strokeWidth={3}
/>
))}
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<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 font-medium tabular-nums"
>
{TOTAL_LABEL}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 24}
fontSize={12}
className="fill-muted-foreground"
>
{COPY.unit}
</tspan>
</text>
)
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter>
<ul role="list" className="grid w-full gap-3 text-sm">
{ALLOCATIONS.map((item) => (
<li
key={item.key}
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]"
>
<span
aria-hidden="true"
className="size-2.5 shrink-0 rounded-[2px]"
style={{ backgroundColor: item.color }}
/>
<span className="min-w-0 wrap-anywhere">{item.label}</span>
<span className="col-start-2 font-medium tabular-nums min-[18rem]:col-start-auto">
{item.amount.toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
})}
</span>
</li>
))}
</ul>
</CardFooter>
</Card>
)
}
Dependencies
npm: recharts@3.8.0. The CLI installs them for you.
Registry pieces it installs with it: