
Bilateral horizontal bar chart displaying demographic population breakdowns.
components/tanstack-charts/bar/population-pyramid.tsx
import { type ChartPoint, barX, defineChart, groupBy, stack } 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 { scaleBand, scaleLinear } from "d3-scale";
import { Users } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
interface PopulationDatum {
ageGroup: string;
sex: "MALE" | "FEMALE";
count: number;
}
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
const ageGroups = ["0-14", "15-29", "30-44", "45-59", "60-74", "75+"];
const pyramidSexes: ("MALE" | "FEMALE")[] = ["MALE", "FEMALE"];
function generateDemoData(): PopulationDatum[] {
const results: PopulationDatum[] = [];
const maleCounts = [65, 82, 95, 88, 54, 32];
const femaleCounts = [62, 80, 98, 92, 58, 38];
ageGroups.forEach((age, idx) => {
results.push({ ageGroup: age, sex: "MALE", count: maleCounts[idx] });
results.push({ ageGroup: age, sex: "FEMALE", count: femaleCounts[idx] });
});
return results;
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
if (points.length === 0) return { title: "", rows: [] };
const d = points[0].datum as PopulationDatum;
if (!d) return { title: "", rows: [] };
return {
title: `Age Group ${d.ageGroup}`,
rows: [
{
label: d.sex === "MALE" ? "Male Population" : "Female Population",
value: `${d.count}k`,
color: d.sex === "MALE" ? "var(--chart-1)" : "var(--chart-2)",
},
],
};
}
export function PopulationPyramid() {
const rawData = useMemo(() => generateDemoData(), []);
const counts = useMemo(() => {
return groupBy(rawData, {
by: {
ageGroup: "ageGroup",
sex: "sex",
},
outputs: { count: { value: "count", reduce: "sum" } },
});
}, [rawData]);
const definition = useMemo(
() =>
defineChart(
{
renderer: motionRenderer,
marks: [
barX(counts, {
id: "population-bars",
x: (row: PopulationDatum) => (row.sex === "MALE" ? -row.count : row.count),
y: "ageGroup",
z: "sex",
color: "sex",
layout: stack({ offset: "diverging", order: pyramidSexes }),
key: (row: PopulationDatum) => `${row.ageGroup}:${row.sex}`,
inset: 1,
radius: 3,
}),
],
scales: {
x: {
scale: scaleLinear().domain([-120, 120]),
grid: true,
axis: {
ticks: {
count: 7,
format: (value: number) => `${Math.abs(value)}k`,
},
label: "Population (Thousands)",
},
},
y: {
scale: () => scaleBand<string>().domain(ageGroups).paddingInner(0.15).paddingOuter(0.05),
},
},
color: {
domain: pyramidSexes,
range: ["var(--chart-1)", "var(--chart-2)"],
},
margin: { top: 24, right: 24, bottom: 32, left: 55 },
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),
},
},
),
[counts],
);
return (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle>Diverging Population Demographic Pyramid</CardTitle>
<CardDescription>
Bilateral horizontal bar chart displaying male vs female population distributions across age
brackets
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Diverging Population Demographic Pyramid Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
Peak working-age demographic concentrated in 30-44 bracket{" "}
<Users className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground leading-none">
Negative male X values mirror positive female X values around zero center axis
</div>
</CardFooter>
</Card>
);
}
export default PopulationPyramid;