
Overlapping ridgeline density plot comparing continuous distribution mountain curves.
components/tanstack-charts/area/ridgeline-density.tsx
import { type ChartPoint, binX, d3Curve, defineChart, normalize, ridgelineY, ruleY } from "@tanstack/charts";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo } from "react";
import { scaleLinear, scalePoint } from "d3-scale";
import { curveBasis } from "d3-shape";
import { Layers } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
interface EpisodeDatum {
season: number;
rating: number;
}
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
const seasonsList = [1, 2, 3, 4, 5];
const ratingBoundaries = [4, 5, 6, 7, 8, 9, 10];
function generateDemoData(): EpisodeDatum[] {
const results: EpisodeDatum[] = [];
seasonsList.forEach((s) => {
const meanRating = 9.2 - s * 0.4;
for (let ep = 0; ep < 25; ep++) {
const r = Math.max(4, Math.min(10, meanRating + (Math.random() - 0.45) * 1.8));
results.push({ season: s, rating: Number(r.toFixed(1)) });
}
});
return results;
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
if (points.length === 0) return { title: "", rows: [] };
const d = points[0].datum as { season: number; x: number; height: number };
if (!d) return { title: "", rows: [] };
return {
title: `Season ${d.season} (Rating ${d.x?.toFixed(1)})`,
rows: [{ label: "Density Height", value: `${(d.height * 100).toFixed(1)}%`, color: "var(--chart-1)" }],
};
}
export function RidgelineDensity() {
const data = useMemo(() => generateDemoData(), []);
const rows = useMemo(() => {
const bins = binX(data, {
value: "rating",
by: "season",
thresholds: ratingBoundaries,
outputs: { count: { reduce: "count" } },
});
return normalize(bins, {
value: "count",
by: "season",
basis: "max",
as: "height",
});
}, [data]);
const overlap = 0.75;
const definition = useMemo(
() =>
defineChart(
{
renderer: motionRenderer,
marks: [
ruleY(seasonsList, {
id: "season-guides",
stroke: "var(--border)",
strokeOpacity: 0.7,
}),
ridgelineY(rows, {
id: "rating-ridges",
x: "x",
y: "season",
height: "height",
key: (row: { season: number; x: number }) => `${row.season}:${row.x}`,
overlap,
color: "season",
fillOpacity: 0.45,
strokeWidth: 1.5,
curve: d3Curve(curveBasis),
}),
],
scales: {
x: {
scale: scaleLinear().domain([4, 10]),
grid: true,
axis: { label: "IMDb Rating Score" },
},
y: {
scale: scalePoint<number>().domain(seasonsList).padding(overlap),
reverse: true,
axis: {
ticks: {
values: seasonsList,
format: (s: number) => `Season ${s}`,
},
},
},
},
color: {
range: [
"var(--chart-1)",
"var(--chart-2)",
"var(--chart-3)",
"var(--chart-4)",
"var(--chart-5)",
],
},
margin: { top: 24, right: 24, bottom: 32, left: 65 },
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),
},
},
),
[rows],
);
return (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle>Overlapping Ridgeline Density Plot</CardTitle>
<CardDescription>
Stacked density ridge mountain plots comparing ratings distribution per season
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Overlapping Ridgeline Density Plot Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
Season 1 displays peak density concentrated at higher 8.8 rating{" "}
<Layers className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground leading-none">
Partial Y-axis overlapping ridgelines economize vertical space in multi-series KDE plots
</div>
</CardFooter>
</Card>
);
}
export default RidgelineDensity;