
Hollow inner radius donut chart displaying center KPI metric.
components/tanstack-charts/pie/center-donut.tsx
import { type ChartPoint, defineChart } from "@tanstack/charts";
import { motion } from "@tanstack/charts/motion";
import { pie, polar, radialArc, radialText } from "@tanstack/charts/polar";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo } from "react";
import { scaleLinear } from "d3-scale";
import { Donut } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
interface DonutSegment {
category: string;
value: number;
}
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
const tau = Math.PI * 2;
const categories = ["Desktop", "Mobile", "Tablet"];
const colors = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)"];
function generateDemoData(): DonutSegment[] {
return [
{ category: "Desktop", value: 55 },
{ category: "Mobile", value: 35 },
{ category: "Tablet", value: 10 },
];
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
if (points.length === 0) return { title: "", rows: [] };
const d = points[0].datum as DonutSegment;
if (!d || !d.category) return { title: "", rows: [] };
return {
title: d.category,
rows: [{ label: "Device Share", value: `${d.value}%`, color: "var(--chart-1)" }],
};
}
export function CenterDonut() {
const data = useMemo(() => generateDemoData(), []);
const totalValue = useMemo(() => data.reduce((acc, curr) => acc + curr.value, 0), [data]);
const definition = useMemo(() => {
const arcs = pie(data, { value: "value" });
const center = [{ id: "total", angle: 0, radius: 0, text: `${totalValue}%` }];
return defineChart(
{
renderer: motionRenderer,
scales: {
x: null,
y: null,
},
marks: [
polar({
radiusRatio: 0.75,
scales: {
angle: { scale: scaleLinear().domain([0, tau]) },
radius: { scale: scaleLinear().domain([0, 1]) },
},
marks: [
radialArc(arcs, {
id: "segment-slices",
key: "category",
innerRadius: ({ radius }) => radius * 0.65,
color: "category",
}),
radialText(center, {
id: "center-total",
angle: "angle",
radius: "radius",
key: "id",
text: "text",
fill: "var(--foreground)",
fontSize: 24,
fontWeight: 700,
}),
],
}),
],
color: { domain: categories, range: colors },
margin: { top: 20, right: 20, bottom: 20, left: 20 },
theme: {
foreground: "var(--muted-foreground)",
grid: "var(--border)",
background: "transparent",
},
},
{
keyboard: true,
tooltip: {
use: tooltip,
className: "font-sans shadow-none rounded-md bg-popover text-popover-foreground border p-2 text-xs",
anchor: "pointer",
placement: "auto",
content: (points) => shadcnTooltipContent(points),
},
},
);
}, [data, totalValue]);
return (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle>Donut Chart with Central KPI Radial Text Metric</CardTitle>
<CardDescription>
Hollow inner radius donut chart displaying total aggregate KPI metric in center origin
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Donut Chart with Central KPI Radial Text Metric Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
Total aggregated metric volume centered in hole <Donut className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground leading-none">
`radialText` positioned at origin (radius: 0) anchors central KPI typography
</div>
</CardFooter>
</Card>
);
}
export default CenterDonut;