diff --git a/ui/src/components/ContributionGraph.tsx b/ui/src/components/ContributionGraph.tsx index 44763ce..fea7ec0 100644 --- a/ui/src/components/ContributionGraph.tsx +++ b/ui/src/components/ContributionGraph.tsx @@ -169,57 +169,59 @@ export function AllTimeGraph() { const navigate = useNavigate(); - const { weeklyData, yearMarkers, thresholds, totalCount } = useMemo(() => { + const { yearRows, thresholds, totalCount } = useMemo(() => { const counts = dailyQ.data ?? []; - if (counts.length === 0) return { weeklyData: [], yearMarkers: [], thresholds: [1, 2, 3], totalCount: 0 }; + if (counts.length === 0) return { yearRows: [], thresholds: [1, 2, 3], totalCount: 0 }; const countMap = new Map(counts.map((d) => [d.date, d.count])); - // Start from the Sunday before earliest - const start = new Date(from); - start.setDate(start.getDate() - start.getDay()); + // Group into year rows, each with ~52 weekly columns + // Start each year from Jan 1, aligned to its preceding Sunday + const startYear = from.getFullYear(); + const endYear = to.getFullYear(); + const yearRows: { year: number; weeks: { weekStart: string; weekEnd: string; count: number; col: number }[] }[] = []; - const weeklyData: { weekStart: string; weekEnd: string; count: number; col: number }[] = []; - const yearMarkers: { col: number; label: string }[] = []; - let col = 0; - let prevYear = -1; - const cursor = new Date(start); + for (let yr = startYear; yr <= endYear; yr++) { + const yearStart = new Date(yr, 0, 1); + const yearEnd = yr === endYear ? to : new Date(yr, 11, 31); + // Align to preceding Sunday + const cursor = new Date(yearStart); + cursor.setDate(cursor.getDate() - cursor.getDay()); - while (cursor <= to) { - const weekStart = fmt(cursor); - let weekCount = 0; - for (let d = 0; d < 7; d++) { - weekCount += countMap.get(fmt(cursor)) ?? 0; - if (d < 6) cursor.setDate(cursor.getDate() + 1); + const weeks: typeof yearRows[0]['weeks'] = []; + let col = 0; + while (cursor <= yearEnd) { + const weekStart = fmt(cursor); + let weekCount = 0; + for (let d = 0; d < 7; d++) { + weekCount += countMap.get(fmt(cursor)) ?? 0; + if (d < 6) cursor.setDate(cursor.getDate() + 1); + } + const weekEnd = fmt(cursor); + cursor.setDate(cursor.getDate() + 1); + weeks.push({ weekStart, weekEnd, count: weekCount, col }); + col++; } - const weekEnd = fmt(cursor); - cursor.setDate(cursor.getDate() + 1); - - const yr = new Date(weekStart).getFullYear(); - if (yr !== prevYear) { - yearMarkers.push({ col, label: String(yr) }); - prevYear = yr; - } - - weeklyData.push({ weekStart, weekEnd, count: weekCount, col }); - col++; + yearRows.push({ year: yr, weeks }); } - const nonZero = weeklyData.map((w) => w.count).filter((c) => c > 0).sort((a, b) => a - b); + const allWeekCounts = yearRows.flatMap((r) => r.weeks.map((w) => w.count)); + const nonZero = allWeekCounts.filter((c) => c > 0).sort((a, b) => a - b); const thresholds = computeThresholds(nonZero); const totalCount = counts.reduce((sum, d) => sum + d.count, 0); - return { weeklyData, yearMarkers, thresholds, totalCount }; + return { yearRows, thresholds, totalCount }; }, [dailyQ.data]); if (!earliest || dailyQ.isLoading) return null; if (dailyQ.isError) return null; - if (weeklyData.length === 0) return null; + if (yearRows.length === 0) return null; - const cols = weeklyData.length; - const labelHeight = 14; - const svgWidth = cols * (CELL_SIZE + GAP); - const svgHeight = labelHeight + CELL_SIZE + GAP; + const maxCols = 53; // max weeks in a year + const yearLabelWidth = 32; + const svgWidth = yearLabelWidth + maxCols * (CELL_SIZE + GAP); + const rows = yearRows.length; + const svgHeight = rows * (CELL_SIZE + GAP); return (