Components
Loading preview...
A composable, customizable contribution graph component inspired by GitHub's activity heatmap. Features composable subcomponents (Grid, Tooltip, Legend), CSS variable theming, configurable cell sizes, interactive tooltips with custom render functions, and full dark mode support.
npx shadcn@latest add https://21st.dev/r/bankkroll/contribution-graph// Custom labels demo
import {
ContributionGraph,
ContributionGraphGrid,
ContributionGraphTooltip,
ContributionGraphLegend,
type ContributionData,
} from "@/components/ui/contribution-graph";
function generateSampleData(): ContributionData[] {
const data: ContributionData[] = [];
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
const current = new Date(startDate);
while (current <= endDate) {
const dayOfWeek = current.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const baseChance = isWeekend ? 0.3 : 0.6;
if (Math.random() < baseChance) {
data.push({
date: current.toISOString().split("T")[0],
count: Math.floor(Math.random() * 12) + 1,
});
}
current.setDate(current.getDate() + 1);
}
return data;
}
const sampleData = generateSampleData();
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
startDate.setDate(startDate.getDate() + 1);
// Calculate comprehensive stats
const totalContributions = sampleData.reduce((sum, d) => sum + d.count, 0);
const activeDays = sampleData.length;
const longestStreak = (() => {
let maxStreak = 0;
let currentStreak = 0;
const dateSet = new Set(sampleData.map((d) => d.date));
const current = new Date(startDate);
while (current <= endDate) {
const dateStr = current.toISOString().split("T")[0];
if (dateSet.has(dateStr)) {
currentStreak++;
maxStreak = Math.max(maxStreak, currentStreak);
} else {
currentStreak = 0;
}
current.setDate(current.getDate() + 1);
}
return maxStreak;
})();
const currentStreak = (() => {
let streak = 0;
const dateSet = new Set(sampleData.map((d) => d.date));
const current = new Date(endDate);
while (current >= startDate) {
const dateStr = current.toISOString().split("T")[0];
if (dateSet.has(dateStr)) {
streak++;
current.setDate(current.getDate() - 1);
} else {
break;
}
}
return streak;
})();
export default function DemoCustomLabels() {
return (
<div className="w-full max-w-4xl p-6 space-y-4">
{/* Stats Header */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="rounded-lg border bg-card p-3">
<div className="text-2xl font-bold text-foreground tabular-nums">
{totalContributions.toLocaleString()}
</div>
<div className="text-xs text-muted-foreground">Total contributions</div>
</div>
<div className="rounded-lg border bg-card p-3">
<div className="text-2xl font-bold text-foreground tabular-nums">
{activeDays}
</div>
<div className="text-xs text-muted-foreground">Active days</div>
</div>
<div className="rounded-lg border bg-card p-3">
<div className="flex items-baseline gap-1">
<span className="text-2xl font-bold text-foreground tabular-nums">
{longestStreak}
</span>
<span className="text-xs text-muted-foreground">days</span>
</div>
<div className="text-xs text-muted-foreground">Longest streak</div>
</div>
<div className="rounded-lg border bg-card p-3">
<div className="flex items-baseline gap-1">
<span className="text-2xl font-bold text-foreground tabular-nums">
{currentStreak}
</span>
<span className="text-xs text-muted-foreground">days</span>
</div>
<div className="text-xs text-muted-foreground">Current streak</div>
</div>
</div>
{/* Graph */}
<ContributionGraph
data={sampleData}
startDate={startDate.toISOString().split("T")[0]}
endDate={endDate.toISOString().split("T")[0]}
formatCount={(count) => {
if (count === 0) return "No contributions";
if (count === 1) return "1 contribution";
return `${count} contributions`;
}}
formatDate={(date) => {
const d = new Date(date);
const today = new Date();
const diffDays = Math.floor(
(today.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)
);
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Yesterday";
return d.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
}}
>
<ContributionGraphGrid />
<ContributionGraphTooltip />
<div className="mt-3 flex items-center justify-between">
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">
{Math.round((activeDays / 365) * 100)}%
</span>{" "}
of days active in the last year
</div>
<ContributionGraphLegend />
</div>
</ContributionGraph>
</div>
);
}