Avijit Ghosh
commited on
Commit
·
f6efde3
1
Parent(s):
e603004
Fix weekly heatmap to show proper weekly aggregation
Browse files- Updated weeklyCalendar.ts to create true weekly data points
- Created custom WeeklyHeatmap component for proper weekly visualization
- Weekly view now shows 4-5 dots per month representing weeks
- Each dot aggregates all activity for that week
- Improved tooltips and month labels for better UX
- Maintains daily view for detailed activity tracking
- src/components/Heatmap.tsx +24 -22
- src/components/Navbar.tsx +10 -0
- src/components/ThemeToggle.tsx +2 -0
- src/components/WeeklyHeatmap.tsx +96 -0
- src/contexts/ThemeContext.tsx +2 -0
- src/pages/_app.tsx +6 -1
- src/utils/weeklyCalendar.ts +25 -32
src/components/Heatmap.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import React from "react";
|
|
| 2 |
import ActivityCalendar from "react-activity-calendar";
|
| 3 |
import { Tooltip, Avatar } from "@mui/material";
|
| 4 |
import Link from "next/link";
|
| 5 |
-
import { aggregateToWeeklyData
|
|
|
|
| 6 |
|
| 7 |
type ViewMode = 'daily' | 'weekly';
|
| 8 |
|
|
@@ -52,27 +53,28 @@ const Heatmap: React.FC<HeatmapProps> = ({
|
|
| 52 |
</div>
|
| 53 |
)}
|
| 54 |
<div className="w-full overflow-x-auto flex justify-center">
|
| 55 |
-
|
| 56 |
-
data={processedData}
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
);
|
|
|
|
| 2 |
import ActivityCalendar from "react-activity-calendar";
|
| 3 |
import { Tooltip, Avatar } from "@mui/material";
|
| 4 |
import Link from "next/link";
|
| 5 |
+
import { aggregateToWeeklyData } from "../utils/weeklyCalendar";
|
| 6 |
+
import WeeklyHeatmap from "./WeeklyHeatmap";
|
| 7 |
|
| 8 |
type ViewMode = 'daily' | 'weekly';
|
| 9 |
|
|
|
|
| 53 |
</div>
|
| 54 |
)}
|
| 55 |
<div className="w-full overflow-x-auto flex justify-center">
|
| 56 |
+
{viewMode === 'weekly' ? (
|
| 57 |
+
<WeeklyHeatmap data={processedData} color={color} />
|
| 58 |
+
) : (
|
| 59 |
+
<ActivityCalendar
|
| 60 |
+
data={processedData}
|
| 61 |
+
theme={{
|
| 62 |
+
dark: ["#161b22", color],
|
| 63 |
+
light: ["#e0e0e0", color],
|
| 64 |
+
}}
|
| 65 |
+
blockSize={11}
|
| 66 |
+
blockMargin={2}
|
| 67 |
+
hideTotalCount
|
| 68 |
+
renderBlock={(block, activity) => (
|
| 69 |
+
<Tooltip
|
| 70 |
+
title={`${activity.count} new repos on ${activity.date}`}
|
| 71 |
+
arrow
|
| 72 |
+
>
|
| 73 |
+
{block}
|
| 74 |
+
</Tooltip>
|
| 75 |
+
)}
|
| 76 |
+
/>
|
| 77 |
+
)}
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
);
|
src/components/Navbar.tsx
CHANGED
|
@@ -1,11 +1,21 @@
|
|
| 1 |
import React from "react";
|
| 2 |
import UserSearchDialog from "./UserSearchDialog";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
const Navbar: React.FC = () => {
|
| 5 |
return (
|
| 6 |
<nav className="w-full mt-4">
|
| 7 |
<div className="max-w-6xl mx-auto px-4 py-3">
|
| 8 |
<div className="flex items-center justify-end gap-3">
|
|
|
|
| 9 |
<UserSearchDialog />
|
| 10 |
</div>
|
| 11 |
</div>
|
|
|
|
| 1 |
import React from "react";
|
| 2 |
import UserSearchDialog from "./UserSearchDialog";
|
| 3 |
+
import dynamic from "next/dynamic";
|
| 4 |
+
|
| 5 |
+
// Dynamically import ThemeToggle to avoid SSR issues
|
| 6 |
+
const ThemeToggle = dynamic(() => import("./ThemeToggle"), {
|
| 7 |
+
ssr: false,
|
| 8 |
+
loading: () => (
|
| 9 |
+
<div className="h-10 w-10 rounded-lg border border-border bg-background animate-pulse"></div>
|
| 10 |
+
),
|
| 11 |
+
});
|
| 12 |
|
| 13 |
const Navbar: React.FC = () => {
|
| 14 |
return (
|
| 15 |
<nav className="w-full mt-4">
|
| 16 |
<div className="max-w-6xl mx-auto px-4 py-3">
|
| 17 |
<div className="flex items-center justify-end gap-3">
|
| 18 |
+
<ThemeToggle />
|
| 19 |
<UserSearchDialog />
|
| 20 |
</div>
|
| 21 |
</div>
|
src/components/ThemeToggle.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
import React, { useEffect, useState } from 'react';
|
| 2 |
import { useTheme } from '../contexts/ThemeContext';
|
| 3 |
import { cn } from '../lib/utils';
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
import React, { useEffect, useState } from 'react';
|
| 4 |
import { useTheme } from '../contexts/ThemeContext';
|
| 5 |
import { cn } from '../lib/utils';
|
src/components/WeeklyHeatmap.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import { Tooltip } from "@mui/material";
|
| 3 |
+
import { getWeekDateRange } from "../utils/weeklyCalendar";
|
| 4 |
+
|
| 5 |
+
type WeeklyActivity = {
|
| 6 |
+
date: string;
|
| 7 |
+
count: number;
|
| 8 |
+
level: number;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
type WeeklyHeatmapProps = {
|
| 12 |
+
data: WeeklyActivity[];
|
| 13 |
+
color: string;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
|
| 17 |
+
// Group data by month and year
|
| 18 |
+
const groupedData = data.reduce((acc, activity) => {
|
| 19 |
+
const date = new Date(activity.date);
|
| 20 |
+
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
| 21 |
+
|
| 22 |
+
if (!acc[yearMonth]) {
|
| 23 |
+
acc[yearMonth] = [];
|
| 24 |
+
}
|
| 25 |
+
acc[yearMonth].push(activity);
|
| 26 |
+
return acc;
|
| 27 |
+
}, {} as Record<string, WeeklyActivity[]>);
|
| 28 |
+
|
| 29 |
+
// Get color intensity based on level
|
| 30 |
+
const getColorIntensity = (level: number) => {
|
| 31 |
+
if (level === 0) return '#161b22'; // Dark background for no activity
|
| 32 |
+
const intensities = ['#0e4429', '#006d32', '#26a641', '#39d353'];
|
| 33 |
+
return intensities[Math.min(level - 1, 3)] || color;
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
// Get month names
|
| 37 |
+
const getMonthName = (yearMonth: string) => {
|
| 38 |
+
const [year, month] = yearMonth.split('-');
|
| 39 |
+
const date = new Date(parseInt(year), parseInt(month) - 1);
|
| 40 |
+
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
const sortedMonths = Object.keys(groupedData).sort();
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className="w-full">
|
| 47 |
+
<div className="flex flex-wrap gap-4 justify-center">
|
| 48 |
+
{sortedMonths.map((yearMonth) => {
|
| 49 |
+
const monthData = groupedData[yearMonth];
|
| 50 |
+
return (
|
| 51 |
+
<div key={yearMonth} className="flex flex-col items-center">
|
| 52 |
+
<div className="text-xs mb-2 text-gray-600 dark:text-gray-400">
|
| 53 |
+
{getMonthName(yearMonth)}
|
| 54 |
+
</div>
|
| 55 |
+
<div className="flex gap-1">
|
| 56 |
+
{monthData.map((activity, index) => (
|
| 57 |
+
<Tooltip
|
| 58 |
+
key={`${yearMonth}-${index}`}
|
| 59 |
+
title={`${activity.count} new repos in week of ${getWeekDateRange(activity.date)}`}
|
| 60 |
+
arrow
|
| 61 |
+
>
|
| 62 |
+
<div
|
| 63 |
+
className="w-3 h-3 rounded-sm cursor-pointer transition-opacity hover:opacity-80"
|
| 64 |
+
style={{
|
| 65 |
+
backgroundColor: getColorIntensity(activity.level),
|
| 66 |
+
}}
|
| 67 |
+
/>
|
| 68 |
+
</Tooltip>
|
| 69 |
+
))}
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
);
|
| 73 |
+
})}
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* Legend */}
|
| 77 |
+
<div className="flex items-center justify-center mt-4 text-xs text-gray-600 dark:text-gray-400">
|
| 78 |
+
<span className="mr-2">Less</span>
|
| 79 |
+
<div className="flex gap-1">
|
| 80 |
+
{[0, 1, 2, 3, 4].map((level) => (
|
| 81 |
+
<div
|
| 82 |
+
key={level}
|
| 83 |
+
className="w-2.5 h-2.5 rounded-sm"
|
| 84 |
+
style={{
|
| 85 |
+
backgroundColor: getColorIntensity(level),
|
| 86 |
+
}}
|
| 87 |
+
/>
|
| 88 |
+
))}
|
| 89 |
+
</div>
|
| 90 |
+
<span className="ml-2">More</span>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
);
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
export default WeeklyHeatmap;
|
src/contexts/ThemeContext.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
import React, { createContext, useContext, useEffect, useState } from 'react';
|
| 2 |
|
| 3 |
type Theme = 'light' | 'dark';
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
import React, { createContext, useContext, useEffect, useState } from 'react';
|
| 4 |
|
| 5 |
type Theme = 'light' | 'dark';
|
src/pages/_app.tsx
CHANGED
|
@@ -1,6 +1,11 @@
|
|
| 1 |
import "@/styles/globals.css";
|
| 2 |
import type { AppProps } from "next/app";
|
|
|
|
| 3 |
|
| 4 |
export default function App({ Component, pageProps }: AppProps) {
|
| 5 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
|
|
|
| 1 |
import "@/styles/globals.css";
|
| 2 |
import type { AppProps } from "next/app";
|
| 3 |
+
import { ThemeProvider } from "../contexts/ThemeContext";
|
| 4 |
|
| 5 |
export default function App({ Component, pageProps }: AppProps) {
|
| 6 |
+
return (
|
| 7 |
+
<ThemeProvider>
|
| 8 |
+
<Component {...pageProps} />
|
| 9 |
+
</ThemeProvider>
|
| 10 |
+
);
|
| 11 |
}
|
src/utils/weeklyCalendar.ts
CHANGED
|
@@ -3,46 +3,39 @@ import { Activity } from "../types/heatmap";
|
|
| 3 |
export const aggregateToWeeklyData = (dailyData: Activity[]): Activity[] => {
|
| 4 |
if (!dailyData || dailyData.length === 0) return [];
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
let currentWeekCount = 0;
|
| 9 |
-
let currentWeekLevel = 0;
|
| 10 |
|
| 11 |
for (const dayActivity of dailyData) {
|
| 12 |
const date = new Date(dayActivity.date);
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
date: currentWeekStart.toISOString().split('T')[0],
|
| 21 |
-
count: currentWeekCount,
|
| 22 |
-
level: currentWeekLevel,
|
| 23 |
-
});
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
// Start new week
|
| 27 |
-
currentWeekStart = new Date(date);
|
| 28 |
-
currentWeekCount = dayActivity.count;
|
| 29 |
-
currentWeekLevel = dayActivity.level;
|
| 30 |
-
} else {
|
| 31 |
-
// Add to current week
|
| 32 |
-
currentWeekCount += dayActivity.count;
|
| 33 |
-
// Use the maximum level for the week
|
| 34 |
-
currentWeekLevel = Math.max(currentWeekLevel, dayActivity.level);
|
| 35 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
-
//
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
weeklyData.push({
|
| 41 |
-
date:
|
| 42 |
-
count:
|
| 43 |
-
level:
|
| 44 |
});
|
| 45 |
-
}
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
return weeklyData;
|
| 48 |
};
|
|
|
|
| 3 |
export const aggregateToWeeklyData = (dailyData: Activity[]): Activity[] => {
|
| 4 |
if (!dailyData || dailyData.length === 0) return [];
|
| 5 |
|
| 6 |
+
// Create a map to group activities by week
|
| 7 |
+
const weeklyMap = new Map<string, { count: number; level: number; dates: string[] }>();
|
|
|
|
|
|
|
| 8 |
|
| 9 |
for (const dayActivity of dailyData) {
|
| 10 |
const date = new Date(dayActivity.date);
|
| 11 |
+
// Get the Sunday of this week (week start)
|
| 12 |
+
const weekStart = new Date(date);
|
| 13 |
+
weekStart.setDate(date.getDate() - date.getDay());
|
| 14 |
+
const weekKey = weekStart.toISOString().split('T')[0];
|
| 15 |
+
|
| 16 |
+
if (!weeklyMap.has(weekKey)) {
|
| 17 |
+
weeklyMap.set(weekKey, { count: 0, level: 0, dates: [] });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
+
|
| 20 |
+
const weekData = weeklyMap.get(weekKey)!;
|
| 21 |
+
weekData.count += dayActivity.count;
|
| 22 |
+
weekData.level = Math.max(weekData.level, dayActivity.level);
|
| 23 |
+
weekData.dates.push(dayActivity.date);
|
| 24 |
}
|
| 25 |
|
| 26 |
+
// Convert to true weekly data - one entry per week, not per day
|
| 27 |
+
const weeklyData: Activity[] = [];
|
| 28 |
+
|
| 29 |
+
weeklyMap.forEach((weekInfo, weekStartDate) => {
|
| 30 |
weeklyData.push({
|
| 31 |
+
date: weekStartDate, // Use the week start date
|
| 32 |
+
count: weekInfo.count,
|
| 33 |
+
level: weekInfo.level,
|
| 34 |
});
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// Sort by date to ensure proper order
|
| 38 |
+
weeklyData.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
| 39 |
|
| 40 |
return weeklyData;
|
| 41 |
};
|