components/tanstack-charts/area/treemap.tsx
import { type ChartPoint, defineChart } from "@tanstack/charts";
import { treemap } from "@tanstack/charts/hierarchy/treemap";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo } from "react";
import { Layout } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
interface FlareNode {
name: string;
size: number;
}
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
function generateDemoData(): FlareNode[] {
return [
{ name: "flare.analytics.cluster.AgglomerativeCluster", size: 3938 },
{ name: "flare.analytics.cluster.CommunityStructure", size: 3812 },
{ name: "flare.analytics.cluster.HierarchicalCluster", size: 6714 },
{ name: "flare.analytics.graph.BetweennessCentrality", size: 3534 },
{ name: "flare.analytics.graph.LinkDistance", size: 5731 },
{ name: "flare.animate.Easing", size: 17010 },
{ name: "flare.animate.FunctionSequence", size: 5842 },
{ name: "flare.animate.Tween", size: 6006 },
{ name: "flare.data.DataField", size: 1759 },
{ name: "flare.data.DataSchema", size: 2165 },
{ name: "flare.display.LineSprite", size: 1732 },
{ name: "flare.display.RectSprite", size: 3623 },
{ name: "flare.physics.DragForce", size: 1082 },
{ name: "flare.physics.GravityForce", size: 1336 },
{ name: "flare.physics.SpringForce", size: 1681 },
{ name: "flare.query.AggregateExpression", size: 1616 },
{ name: "flare.util.MathUtil", size: 8223 },
{ name: "flare.util.StringUtil", size: 11234 },
];
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
if (points.length === 0) return { title: "", rows: [] };
const d = points[0].datum as { path?: string; value?: number; name?: string };
if (!d) return { title: "", rows: [] };
const nodeName = d.name || d.path || "Class Node";
return {
title: nodeName.split(".").pop() || nodeName,
rows: [
{ label: "File Size", value: `${(d.value || 0).toLocaleString()} Bytes`, color: "var(--chart-1)" },
{ label: "Package", value: nodeName.split(".").slice(0, -1).join("."), color: "var(--foreground)" },
],
};
}
export function Treemap() {
const data = useMemo(() => generateDemoData(), []);
const definition = useMemo(
() =>
defineChart(
{
renderer: motionRenderer,
marks: [
treemap(data, {
id: "treemap-cells",
path: "name",
delimiter: ".",
value: "size",
ratio: 4 / 3,
round: true,
color: (node) => node.ancestorIds.at(-1) ?? node.id,
inset: 1,
stroke: "var(--background)",
strokeWidth: 1.5,
label: (node) => node.id.split(".").pop() ?? "",
labelFill: "var(--primary-foreground)",
labelFontSize: 9,
labelFontWeight: 600,
}),
],
color: {
range: [
"var(--chart-1)",
"var(--chart-2)",
"var(--chart-3)",
"var(--chart-4)",
"var(--chart-5)",
],
},
scales: {
x: null,
y: null,
},
guides: false,
margin: 4,
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],
);
return (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle>Squarified Hierarchical Treemap</CardTitle>
<CardDescription>
Space-filling treemap tiling nested package structure proportional to file sizes
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Squarified Hierarchical Treemap Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
Animate and Util packages occupy largest rectangular area{" "}
<Layout className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground leading-none">
Squarified layout optimizes aspect ratios for easy visual area comparison
</div>
</CardFooter>
</Card>
);
}
export default Treemap;