
Interactive tabbed bar dashboard switching desktop and mobile traffic.
components/tanstack-charts/bar/active-bar-dashboard.tsx
import { barY, crosshair, 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 { scaleBand, scaleLinear } from "d3-scale";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { ChartConfig } from "@/components/ui/chart";
interface DashboardRow {
id: string;
day: string;
desktop: number;
mobile: number;
}
type MetricType = "desktop" | "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: 170, damping: 18, mass: 1 },
});
function generateDemoData(): DashboardRow[] {
const results: DashboardRow[] = [];
for (let i = 1; i <= 30; i++) {
const id = i < 10 ? `0${i}` : `${i}`;
results.push({
id,
day: `Jun ${i}`,
desktop: Math.floor(120 + Math.sin(i * 0.4) * 80 + Math.random() * 40),
mobile: Math.floor(90 + Math.cos(i * 0.3) * 60 + Math.random() * 30),
});
}
return results;
}
export function ActiveBarDashboard() {
const [activeTab, setActiveTab] = useState<MetricType>("desktop");
const data = useMemo(() => generateDemoData(), []);
const totals = useMemo(() => {
const totalDesktop = data.reduce((acc, curr) => acc + curr.desktop, 0);
const totalMobile = data.reduce((acc, curr) => acc + curr.mobile, 0);
return {
desktop: totalDesktop,
mobile: totalMobile,
};
}, [data]);
const yMax = useMemo(() => {
const max = Math.max(...data.map((d) => d[activeTab]));
return Math.ceil((max * 1.15) / 50) * 50;
}, [data, activeTab]);
const tickIds = ["01", "07", "14", "21", "28"];
const definition = useMemo(
() =>
defineChart(
{
renderer: motionRenderer,
motion: {
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
},
marks: [
barY(data, {
id: "daily-visitors",
x: "id",
y: activeTab,
key: "id",
fill: "url(#visitor-bars-gradient)",
radius: 4,
}),
crosshair<string, number>({
id: "active-bar-ring",
x: {
band: {
fill: "transparent",
stroke: chartConfig[activeTab].color,
strokeOpacity: 0.9,
strokeWidth: 1.5,
radius: 5,
},
},
y: false,
}),
],
scales: {
x: {
scale: scaleBand<string>()
.domain(data.map((d) => d.id))
.paddingInner(0.2),
axis: {
line: false,
ticks: {
values: tickIds,
format: (v: string) => data.find((d) => d.id === v)?.day ?? "",
},
},
},
y: {
scale: scaleLinear().domain([0, yMax]).nice(),
grid: true,
axis: false,
},
},
gradients: [
{
id: "visitor-bars-gradient",
x1: 0,
y1: 1,
x2: 0,
y2: 0,
stops: [
{ offset: 0, color: chartConfig[activeTab].color, opacity: 0.35 },
{ offset: 1, color: chartConfig[activeTab].color, opacity: 0.95 },
],
},
],
margin: { top: 20, right: 12, bottom: 32, left: 12 },
theme: {
foreground: "var(--muted-foreground)",
grid: "var(--border)",
background: "transparent",
},
},
{
keyboard: true,
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 DashboardRow;
if (!datum) return { rows: [] };
return {
title: datum.day,
rows: [
{
label: chartConfig[activeTab].label,
value: `${datum[activeTab].toLocaleString("en-US")} visitors`,
color: chartConfig[activeTab].color,
},
],
};
},
},
},
),
[data, activeTab, yMax],
);
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">Active Bar Visitor Dashboard</CardTitle>
<CardDescription>
Interactive daily visitor traffic with focus ring cursor (Last 30 Days)
</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">
{/* Dynamic 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={`Active bar visitor dashboard chart displaying ${activeTab} data`}
/>
</div>
</CardContent>
</Card>
);
}
export default ActiveBarDashboard;