
An expanded stacked area chart illustrating traffic distribution ratios normalized to 100% built with TanStack Charts.
components/tanstack-charts/area/area-stack-expanded.tsx
import { areaY, defineChart, lineY } from "@tanstack/charts";
import { d3Curve } from "@tanstack/charts/d3/shape";
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 { curveNatural } from "d3-shape";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
const chartData = [
{ month: "January", desktop: 186, mobile: 80, other: 45 },
{ month: "February", desktop: 305, mobile: 200, other: 100 },
{ month: "March", desktop: 237, mobile: 120, other: 150 },
{ month: "April", desktop: 73, mobile: 190, other: 50 },
{ month: "May", desktop: 209, mobile: 130, other: 100 },
{ month: "June", desktop: 214, mobile: 140, other: 160 },
].map((item) => {
const total = item.desktop + item.mobile + item.other;
const pctOther = item.other / total;
const pctMobile = item.mobile / total;
const pctDesktop = item.desktop / total;
return {
...item,
y0Other: 0,
y1Other: pctOther,
y0Mobile: pctOther,
y1Mobile: pctOther + pctMobile,
y0Desktop: pctOther + pctMobile,
y1Desktop: pctOther + pctMobile + pctDesktop,
};
});
export function AreaStackExpanded() {
const definition = useMemo(() => {
return defineChart({
renderer: motionRenderer,
motion: {
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
},
focus: "group-x",
tooltip: {
use: tooltip,
motion: { type: "spring", stiffness: 170, damping: 18 },
className: "font-sans! shadow-none! capitalize!",
content: (points) => {
const datum = points[0]?.datum as (typeof chartData)[number];
if (!datum) return { rows: [] };
const total = datum.desktop + datum.mobile + datum.other;
return {
title: String(points[0].xValue),
rows: [
{
label: "Desktop",
value: `${datum.desktop} (${Math.round((datum.desktop / total) * 100)}%)`,
color: "var(--chart-1)",
},
{
label: "Mobile",
value: `${datum.mobile} (${Math.round((datum.mobile / total) * 100)}%)`,
color: "var(--chart-2)",
},
{
label: "Other",
value: `${datum.other} (${Math.round((datum.other / total) * 100)}%)`,
color: "var(--chart-3)",
},
],
};
},
},
marks: [
areaY(chartData, {
id: "other",
x: (d) => d.month.slice(0, 3),
y: (d) => d.y1Other,
fill: "var(--chart-3)",
fillOpacity: 0.15,
curve: d3Curve(curveNatural),
}),
lineY(chartData, {
id: "other-line",
x: (d) => d.month.slice(0, 3),
y: (d) => d.y1Other,
stroke: "var(--chart-3)",
strokeWidth: 2,
curve: d3Curve(curveNatural),
points: false,
}),
areaY(chartData, {
id: "mobile",
x: (d) => d.month.slice(0, 3),
y: (d) => d.y1Mobile,
fill: "var(--chart-2)",
fillOpacity: 0.4,
curve: d3Curve(curveNatural),
}),
lineY(chartData, {
id: "mobile-line",
x: (d) => d.month.slice(0, 3),
y: (d) => d.y1Mobile,
stroke: "var(--chart-2)",
strokeWidth: 2,
curve: d3Curve(curveNatural),
points: false,
}),
areaY(chartData, {
id: "desktop",
x: (d) => d.month.slice(0, 3),
y: (d) => d.y1Desktop,
fill: "var(--chart-1)",
fillOpacity: 0.4,
curve: d3Curve(curveNatural),
}),
lineY(chartData, {
id: "desktop-line",
x: (d) => d.month.slice(0, 3),
y: (d) => d.y1Desktop,
stroke: "var(--chart-1)",
strokeWidth: 2,
curve: d3Curve(curveNatural),
points: false,
}),
],
scales: {
x: {
scale: scalePoint().domain(chartData.map((d) => d.month.slice(0, 3))),
},
y: {
scale: scaleLinear().domain([0, 1]),
grid: true,
},
},
});
}, []);
return (
<Card className="gap-4">
<CardHeader>
<CardTitle className="text-lg">Normalized Revenue Share</CardTitle>
<CardDescription>
Relative revenue distribution between Enterprise and SMB account tiers
</CardDescription>
</CardHeader>
<CardContent>
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Expanded stacked area chart illustrating traffic distribution ratios normalized to 100%"
/>
</CardContent>
</Card>
);
}