
A radar chart with custom formatted value labels on outer vertices built with TanStack Charts.
components/tanstack-charts/radar/radar-label-custom.tsx
import { type ChartPoint, defineChart } from "@tanstack/charts";
import { motion } from "@tanstack/charts/motion";
import { angleGrid, focusGroupAngle, polar, radialArea, radialGrid, radialText } from "@tanstack/charts/polar";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo } from "react";
import { scaleLinear, scalePoint } from "d3-scale";
import { curveLinearClosed } from "d3-shape";
import { TrendingUp } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
function generateDemoData() {
return [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
];
}
const twoSeries = ["desktop", "mobile"] as const;
const chartColors = ["var(--chart-1)", "var(--chart-2)"];
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
function titleCase(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
return {
title: String(points[0]?.xValue ?? ""),
rows: points.map((point) => ({
label: titleCase(
String(point.group ?? point.markId.replace(/-?(bars|lines|areas|slices|values|radar)$/u, "")),
),
value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString("en-US"),
color: point.color,
})),
};
}
export function RadarLabelCustom() {
const data = useMemo(() => generateDemoData(), []);
const definition = useMemo(() => {
const months = data.map((row) => row.month);
const radiusMax = 320;
const gridValues = Array.from({ length: 4 }, (_, index) => Math.round((radiusMax * (index + 1)) / 4));
const guides = [
radialGrid({
values: gridValues,
shape: "polygon",
stroke: "var(--border)",
strokeOpacity: 1,
}),
angleGrid({
values: months,
labels: false,
labelOffset: 10,
labelFill: "var(--muted-foreground)",
labelFontSize: 12,
stroke: "var(--border)",
strokeOpacity: 1,
}),
];
return defineChart(
({ height }) => ({
scales: {
x: null,
y: null,
},
marks: [
polar({
radiusRatio: height < 220 ? 0.64 : 0.76,
scales: {
angle: { scale: scalePoint<string>().domain(months), wrap: true },
radius: { scale: scaleLinear().domain([0, radiusMax]) },
},
guides,
marks: [
radialArea(data, {
id: "desktop-radar",
className: "ts-chart__radar",
angle: "month",
radius: "desktop",
key: "month",
z: () => "desktop",
curve: curveLinearClosed,
fill: chartColors[0],
fillOpacity: 0.6,
stroke: chartColors[0],
strokeOpacity: 1,
strokeWidth: 1,
}),
radialArea(data, {
id: "mobile-radar",
className: "ts-chart__radar",
angle: "month",
radius: "mobile",
key: "month",
z: () => "mobile",
curve: curveLinearClosed,
fill: chartColors[1],
fillOpacity: 1,
stroke: chartColors[1],
strokeOpacity: 1,
strokeWidth: 1,
}),
radialText(data, {
id: "radar-values",
angle: "month",
radius: () => radiusMax,
key: "month",
z: () => "desktop",
text: (row) => `${row.desktop}/${row.mobile ?? 0}`,
anchor: "outside",
radiusOffset: 15,
dy: -7,
fill: "var(--foreground)",
fontSize: 12,
fontWeight: 500,
}),
radialText(data, {
id: "radar-months",
angle: "month",
radius: () => radiusMax,
key: "month",
z: () => "desktop",
text: "month",
anchor: "outside",
radiusOffset: 15,
dy: 10,
fill: "var(--muted-foreground)",
fontSize: 12,
}),
],
}),
],
color: { domain: twoSeries, range: chartColors },
margin: 0,
}),
{
focus: focusGroupAngle,
tooltip: {
use: tooltip,
motion: { type: "spring", stiffness: 170, damping: 18 },
className: "font-sans shadow-none rounded-md bg-popover text-popover-foreground border p-2 text-xs",
anchor: "group-center",
placement: "auto",
sort: "color-domain",
content: (points) => shadcnTooltipContent(points),
},
},
);
}, [data]);
return (
<Card className="flex w-full max-w-md flex-col">
<CardHeader className="items-center pb-4">
<CardTitle>API SLA Compliance Profile</CardTitle>
<CardDescription>SLA compliance score across core API service endpoints</CardDescription>
</CardHeader>
<CardContent className="pb-0">
<div className="mx-auto flex aspect-square max-h-[250px] w-full items-center justify-center">
<Chart
definition={definition}
renderer={motionRenderer}
initialWidth={250}
height={250}
ariaLabel="API SLA Compliance Profile Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col gap-2 pt-4 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
API reliability score improved by 5.2% this quarter{" "}
<TrendingUp className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</CardFooter>
</Card>
);
}
export default RadarLabelCustom;