Activity Graph
A high-performance metric visualizer featuring staggered bar animations and a spring-physics tooltip. Built with Framer Motion, it provides dynamic, real-time feedback for audit data, using an architectural B/W aesthetic and reactive hover states to ensure precise data readability.
framer-motion
Activity Graph
Live Metrics+12.4%
Installation
Install the required dependencies:
npm install framer-motionlib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
components/ui/activity-graph.tsx
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
type GraphProps = {
className?: string;
bars?: number[];
};
export function ActivityGraph({
className,
bars = [40, 70, 45, 90, 65, 80, 50, 95, 60, 85],
}: GraphProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
return (
<div
className={cn(
className,
"p-6 rounded-4xl border w-sm border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-900/30",
)}
>
<h4 className="text-[10px] flex items-center gap-1 font-black uppercase tracking-widest text-zinc-400 mb-6">
Activity Graph
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
</h4>
<div className="relative flex items-end justify-between h-20 gap-1 group/graph">
<AnimatePresence>
{hoveredIndex !== null && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.9 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
left: `${(hoveredIndex / (bars.length - 1)) * 100}%`,
}}
exit={{ opacity: 0, y: 10, scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="absolute -top-8 z-20 pointer-events-none"
style={{ x: "-50%" }} // This ensures the tooltip itself is centered on its own anchor point
>
<div className="px-3 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-black rounded-lg text-[10px] font-black shadow-xl whitespace-nowrap">
VALUE: {bars[hoveredIndex]}%{/* Tooltip Arrow */}
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-zinc-900 dark:bg-white rotate-45" />
</div>
</motion.div>
)}
</AnimatePresence>
{bars.map((height, i) => (
<motion.div
key={i}
initial={{ height: 0 }}
animate={{ height: `${height}%` }}
onMouseEnter={() => setHoveredIndex(i)}
onMouseLeave={() => setHoveredIndex(null)}
transition={{ delay: i * 0.05, duration: 0.8, ease: "circOut" }}
className="flex-1 bg-zinc-900 dark:bg-white rounded-t-sm opacity-20 hover:opacity-100 transition-opacity cursor-crosshair relative"
/>
))}
</div>
<div className="mt-6 flex justify-between items-center">
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-tighter">
Live Metrics
</span>
<span className="text-xs font-black dark:text-white">+12.4%</span>
</div>
</div>
);
}