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 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, getWeekDateRange } from "../utils/weeklyCalendar";
 
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
- <ActivityCalendar
56
- data={processedData}
57
- theme={{
58
- dark: ["#161b22", color],
59
- light: ["#e0e0e0", color],
60
- }}
61
- blockSize={viewMode === 'weekly' ? 15 : 11}
62
- hideTotalCount
63
- renderBlock={(block, activity) => (
64
- <Tooltip
65
- title={
66
- viewMode === 'weekly'
67
- ? `${activity.count} new repos in week of ${getWeekDateRange(activity.date)}`
68
- : `${activity.count} new repos on ${activity.date}`
69
- }
70
- arrow
71
- >
72
- {block}
73
- </Tooltip>
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 <Component {...pageProps} />;
 
 
 
 
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
- const weeklyData: Activity[] = [];
7
- let currentWeekStart: Date | null = null;
8
- let currentWeekCount = 0;
9
- let currentWeekLevel = 0;
10
 
11
  for (const dayActivity of dailyData) {
12
  const date = new Date(dayActivity.date);
13
- const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
14
-
15
- // If it's Sunday or we don't have a current week, start a new week
16
- if (dayOfWeek === 0 || currentWeekStart === null) {
17
- // Save the previous week if it exists
18
- if (currentWeekStart !== null) {
19
- weeklyData.push({
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
- // Don't forget the last week
39
- if (currentWeekStart !== null) {
 
 
40
  weeklyData.push({
41
- date: currentWeekStart.toISOString().split('T')[0],
42
- count: currentWeekCount,
43
- level: currentWeekLevel,
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
  };