science-release-map / src /components /WeeklyHeatmap.tsx
evijit's picture
evijit HF Staff
hover text showing repo names (#9)
66f319d verified
import React, { useMemo, useCallback, useEffect, useState } from "react";
import { Tooltip } from "@mui/material";
import { getWeekDateRange } from "../utils/weeklyCalendar";
import { getHeatmapColorIntensity } from "../utils/heatmapColors";
type WeeklyActivity = {
date: string;
count: number;
level: number;
items?: string[];
};
type WeeklyHeatmapProps = {
data: WeeklyActivity[];
color: string;
};
const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
// Track theme changes
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
// Initial check
checkTheme();
// Watch for theme changes
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
// Memoize the grouped data to avoid recalculation
const groupedData = useMemo(() => {
return data.reduce((acc, activity) => {
const date = new Date(activity.date);
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!acc[yearMonth]) {
acc[yearMonth] = [];
}
acc[yearMonth].push(activity);
return acc;
}, {} as Record<string, WeeklyActivity[]>);
}, [data]);
// Memoize sorted months
const sortedMonths = useMemo(() => Object.keys(groupedData).sort(), [groupedData]);
// Memoize the color intensity function with theme awareness
const getColorIntensity = useCallback((level: number) => {
return getHeatmapColorIntensity(level, color, isDark) || undefined;
}, [color, isDark]);
// Get the exact same empty dot color as ActivityCalendar
const emptyDotColor = useMemo(() => {
return isDark ? '#374151' : '#d1d5db';
}, [isDark]);
// Get month names
const getMonthName = (yearMonth: string) => {
const [year, month] = yearMonth.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1);
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
};
return (
<div className="w-full">
<div className="flex flex-wrap gap-4 justify-center">
{sortedMonths.map((yearMonth) => {
const monthData = groupedData[yearMonth];
return (
<div key={yearMonth} className="flex flex-col items-center">
<div className="text-xs mb-2 text-gray-600 dark:text-gray-400">
{getMonthName(yearMonth)}
</div>
<div className="flex gap-1">
{monthData.map((activity, index) => {
const itemsText = activity.items && activity.items.length > 0
? activity.items.join(', ')
: 'No releases';
const tooltipTitle = activity.count > 0
? `${activity.count} new repos in week of ${getWeekDateRange(activity.date)}: ${itemsText}`
: `No repos in week of ${getWeekDateRange(activity.date)}`;
return (
<Tooltip
key={`${yearMonth}-${index}`}
title={tooltipTitle}
arrow
>
<div
className="w-3 h-3 rounded-sm cursor-pointer transition-opacity hover:opacity-80"
style={{
backgroundColor: activity.level === 0 ? emptyDotColor : getColorIntensity(activity.level),
}}
/>
</Tooltip>
);
})}
</div>
</div>
);
})}
</div>
{/* Legend */}
<div className="flex items-center justify-center mt-4 text-xs text-gray-600 dark:text-gray-400">
<span className="mr-2">Less</span>
<div className="flex gap-1">
{[0, 1, 2, 3, 4].map((level) => (
<div
key={level}
className="w-2.5 h-2.5 rounded-sm"
style={{
backgroundColor: level === 0 ? emptyDotColor : getColorIntensity(level),
}}
/>
))}
</div>
<span className="ml-2">More</span>
</div>
</div>
);
};
export default React.memo(WeeklyHeatmap);