
An interactive bar chart with tabbed metric selection built with TanStack Charts.
components/tanstack-charts/bar/bar-chart-interactive.tsx
import { barY, defineChart } from "@tanstack/charts";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo, useState } from "react";
import { extent, max } from "d3-array";
import { scaleLinear, scaleTime } from "d3-scale";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { ChartConfig } from "@/components/ui/chart";
// Generate 3 months of daily visitor data (April 1 to June 30, 2024)
const startTime = new Date(2024, 3, 1).getTime();
const DAY_MS = 24 * 60 * 60 * 1000;
const chartData = Array.from({ length: 91 }, (_, i) => {
const date = new Date(startTime + i * DAY_MS);
const dayOfWeek = date.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const baseFactor = isWeekend ? 0.65 : 1.0;
const desktop = Math.round((180 + Math.sin(i * 0.15) * 70 + Math.cos(i * 0.05) * 50) * baseFactor + (i % 7) * 12);
const mobile = Math.round(
(130 + Math.cos(i * 0.12) * 55 + Math.sin(i * 0.08) * 35) * (isWeekend ? 1.15 : 0.85) + (i % 5) * 10,
);
return {
date,
desktop: Math.max(50, desktop),
mobile: Math.max(40, mobile),
};
});
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig;
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 140, damping: 16, mass: 0.8 },
});
const formatTooltipDate = (date: unknown) => {
if (!(date instanceof Date)) return "";
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
type MetricTab = "desktop" | "mobile";
export const BarChartInteractive = () => {
const [activeTab, setActiveTab] = useState<MetricTab>("desktop");
const totals = useMemo(() => {
const totalDesktop = chartData.reduce((acc, curr) => acc + curr.desktop, 0);
const totalMobile = chartData.reduce((acc, curr) => acc + curr.mobile, 0);
return {
desktop: totalDesktop,
mobile: totalMobile,
};
}, []);
const definition = useMemo(() => {
const xDomain = extent(chartData, (d) => d.date) as [Date, Date];
const activeColor = chartConfig[activeTab].color;
const yMax = max(chartData, (d) => d[activeTab]) ?? 0;
const marks = [
barY(chartData, {
id: "visitor-bar", // Stable mark ID for morph animations
key: (d) => d.date.toISOString(), // Stable item keys
x: (d) => d.date,
y: (d) => d[activeTab],
fill: activeColor,
radius: 2,
}),
];
return defineChart(
{
renderer: motionRenderer,
motion: {
transition: { type: "spring", stiffness: 140, damping: 16, mass: 0.8 },
},
marks,
scales: {
x: {
scale: scaleTime().domain(xDomain),
},
y: {
scale: scaleLinear()
.domain([0, Math.ceil(yMax / 50) * 50])
.nice(),
grid: true,
},
},
},
{
focus: "group-x",
tooltip: {
use: tooltip,
motion: { type: "spring", stiffness: 140, damping: 16 },
className: "font-sans! shadow-none! capitalize!",
content: (points) => {
const datum = points[0]?.datum as (typeof chartData)[number];
if (!datum) return { rows: [] };
return {
title: formatTooltipDate(points[0].xValue),
rows: [
{
label: chartConfig[activeTab].label,
value: Number(datum[activeTab]).toLocaleString("en-US"),
color: activeColor,
},
],
};
},
},
},
);
}, [activeTab]);
return (
<Card className="gap-0 py-4 sm:py-0">
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
{/* Title Context */}
<div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 sm:pt-6 sm:pb-6">
<CardTitle className="text-lg">User Activity Highlights</CardTitle>
<CardDescription>Showing total visitors for the last 3 months</CardDescription>
</div>
{/* Tab Controls */}
<div className="flex">
{(["desktop", "mobile"] as const).map((key) => {
return (
<button
key={key}
data-active={activeTab === key}
className="data-[active=true]:bg-muted/50 hover:bg-muted/30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left transition-colors even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6"
onClick={() => setActiveTab(key)}>
<span className="text-muted-foreground text-xs tracking-wider uppercase">
{chartConfig[key].label}
</span>
<span className="text-lg leading-none font-bold sm:text-2xl">
{totals[key].toLocaleString("en-US")}
</span>
</button>
);
})}
</div>
</CardHeader>
<CardContent className="px-2 pt-4 sm:p-6 sm:pt-8">
{/* Active Legend Indicator */}
<div className="text-muted-foreground mb-4 flex items-center justify-end gap-2 text-xs font-medium">
<span
className="h-2.5 w-2.5 rounded-full transition-colors duration-300"
style={{ backgroundColor: chartConfig[activeTab].color }}
/>
<span>{chartConfig[activeTab].label} Visitors</span>
</div>
<div className="h-[280px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={280}
ariaLabel={`User activity bar chart displaying ${activeTab} data`}
/>
</div>
</CardContent>
</Card>
);
};
export default BarChartInteractive;