radial-chart-13
Fine segmented budget ring for dashboards: a thin ring in blue and neutral ink with a center total and a full text breakdown.
pnpm dlx shadcn add @01orbital/radial-chart-13"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"
import { Separator } from "@/components/ui/separator"
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(--art-accent)" },
{
key: "engineering",
label: "Engineering",
amount: 38000,
color: "var(--muted-foreground)",
},
{
key: "research",
label: "Research",
amount: 20000,
color: "var(--foreground)",
},
] 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 RadialChart13() {
return (
<Card className="@container/budget 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}
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-56"
>
<RadialBarChart
accessibilityLayer={false}
data={DATA}
startAngle={90}
endAngle={-270}
innerRadius="90%"
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={1.5}
/>
))}
<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 tabular-nums"
>
{TOTAL_LABEL}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 24}
fontSize={10}
className="fill-muted-foreground font-mono uppercase"
>
{COPY.unit}
</tspan>
</text>
)
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="grid gap-3">
<Separator />
<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 @[18rem]/budget:grid-cols-[auto_minmax(0,1fr)_auto]"
>
<span
aria-hidden="true"
className="h-px w-3 shrink-0"
style={{ backgroundColor: item.color }}
/>
<span className="min-w-0 wrap-anywhere">{item.label}</span>
<span className="col-start-2 tabular-nums @[18rem]/budget: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: