From 502c44d560cbba7b2aa19d99f9a7fbd14d39ae31 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 22 May 2026 17:54:42 -0500 Subject: [PATCH] feat(metrics): update heatmap metrics to reflect data for the last year --- backend/app/api/forgejo_metrics.py | 6 +- backend/app/schemas/metrics.py | 2 +- .../src/components/git/ForgejoHeatmap.tsx | 57 ++++++++++++------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/backend/app/api/forgejo_metrics.py b/backend/app/api/forgejo_metrics.py index e13194d..7669720 100644 --- a/backend/app/api/forgejo_metrics.py +++ b/backend/app/api/forgejo_metrics.py @@ -22,7 +22,7 @@ from app.models.forgejo_connections import ForgejoConnection from app.models.forgejo_issues import ForgejoIssue from app.models.forgejo_repositories import ForgejoRepository from app.schemas.metrics import HeatmapDay, HeatmapResponse, LastPushRead, MetricsResponse -from app.services.forgejo_client import ForgejoAPIClient, get_forgejo_client +from app.services.forgejo_client import ForgejoAPIClient if TYPE_CHECKING: from sqlmodel.ext.asyncio.session import AsyncSession @@ -371,14 +371,14 @@ async def get_forgejo_metrics( "/heatmap", response_model=HeatmapResponse, summary="Forgejo issue activity heatmap", - description="Daily issue open+close event counts for the last 6 months, scoped to the caller's organisation.", + description="Daily issue open+close event counts for the last year, scoped to the caller's organisation.", ) async def get_forgejo_heatmap( organization_id: UUID | None = Query(None, description="Filter by organisation ID"), session: AsyncSession = SESSION_DEP, ctx: OrganizationContext = ORG_MEMBER_DEP, ) -> HeatmapResponse: - """Return per-day issue event counts and total line contributions for the last 6 months.""" + """Return per-day issue event counts and total line contributions for the last year.""" if organization_id and organization_id != ctx.organization.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) diff --git a/backend/app/schemas/metrics.py b/backend/app/schemas/metrics.py index c6bd76a..f0bd335 100644 --- a/backend/app/schemas/metrics.py +++ b/backend/app/schemas/metrics.py @@ -140,7 +140,7 @@ class HeatmapDay(SQLModel): class HeatmapResponse(SQLModel): - """Issue activity heatmap for the last 6 months.""" + """Issue activity heatmap for the last year.""" days: list[HeatmapDay] max_count: int diff --git a/frontend/src/components/git/ForgejoHeatmap.tsx b/frontend/src/components/git/ForgejoHeatmap.tsx index bb2da9c..f96d0fe 100644 --- a/frontend/src/components/git/ForgejoHeatmap.tsx +++ b/frontend/src/components/git/ForgejoHeatmap.tsx @@ -28,17 +28,23 @@ const LCH = LVH - LP_T - LP_B; const HCELL = 11; const HGAP = 3; const HSTRIDE = HCELL + HGAP; -const HWEEKS = 52; const HLEFT = 30; const HTOP = 18; -const HVW = HLEFT + HWEEKS * HSTRIDE; -const HVH = HTOP + 7 * HSTRIDE - HGAP + 22; // +22 for legend +const HMIN_VW = 420; +const HVH = LVH; // ── Types & data ─────────────────────────────────────────────────────────── type RangeKey = "7d" | "30d" | "3m" | "6m" | "1y"; const RANGE_DAYS: Record = { "7d": 7, "30d": 30, "3m": 91, "6m": 183, "1y": 365, }; +const RANGE_SUMMARY: Record = { + "7d": "7 days", + "30d": "30 days", + "3m": "3 months", + "6m": "6 months", + "1y": "1 year", +}; const RANGE_LABELS: RangeKey[] = ["7d", "30d", "3m", "6m", "1y"]; const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; @@ -185,7 +191,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: { {/* SVG */} - @@ -249,41 +255,52 @@ function HeatmapGrid({ days, range, onRangeChange }: { range: RangeKey; onRangeChange: (r: RangeKey) => void; }) { - const { weeks, monthLabels, totalEvents } = useMemo(() => { + const { weeks, monthLabels, totalEvents, viewWidth } = useMemo(() => { const lookup = new Map(days.map(d => [d.date, d.count])); const today = new Date(); today.setHours(0,0,0,0); - const start = new Date(today); - start.setDate(start.getDate() - start.getDay()); - start.setDate(start.getDate() - (HWEEKS-1)*7); + const numDays = RANGE_DAYS[range]; + const rangeStart = new Date(today); + rangeStart.setDate(rangeStart.getDate() - (numDays - 1)); + const calendarStart = new Date(rangeStart); + calendarStart.setDate(calendarStart.getDate() - calendarStart.getDay()); + const calendarEnd = new Date(today); + calendarEnd.setDate(calendarEnd.getDate() + (6 - calendarEnd.getDay())); + const weekCount = Math.max( + 1, + Math.ceil(((calendarEnd.getTime() - calendarStart.getTime()) / 86400000 + 1) / 7), + ); - type Cell = { date:string; count:number; future:boolean }; + type Cell = { date:string; count:number; outsideRange:boolean }; const builtWeeks: Cell[][] = []; const monthLabelList: {weekIdx:number; label:string}[] = []; let lastMonth = -1; - const cur = new Date(start); + const cur = new Date(calendarStart); - for (let w = 0; w < HWEEKS; w++) { + for (let w = 0; w < weekCount; w++) { const week: Cell[] = []; for (let d = 0; d < 7; d++) { const dateStr = isoDate(cur); const month = cur.getMonth(); - if (d === 0 && month !== lastMonth) { + if (cur >= rangeStart && cur <= today && month !== lastMonth) { monthLabelList.push({ weekIdx: w, label: MONTHS[month] }); lastMonth = month; } - week.push({ date: dateStr, count: lookup.get(dateStr) ?? 0, future: cur > today }); + week.push({ + date: dateStr, + count: lookup.get(dateStr) ?? 0, + outsideRange: cur < rangeStart || cur > today, + }); cur.setDate(cur.getDate()+1); } builtWeeks.push(week); } // contributions count for selected range - const numDays = RANGE_DAYS[range]; - const cutoff = new Date(today); cutoff.setDate(cutoff.getDate() - numDays); - const cutoffStr = isoDate(cutoff); + const cutoffStr = isoDate(rangeStart); const totalEvents = days.filter(d => d.date >= cutoffStr).reduce((s,d) => s+d.count, 0); + const viewWidth = Math.max(HMIN_VW, HLEFT + weekCount * HSTRIDE); - return { weeks: builtWeeks, monthLabels: monthLabelList, totalEvents }; + return { weeks: builtWeeks, monthLabels: monthLabelList, totalEvents, viewWidth }; }, [days, range]); return ( @@ -308,7 +325,7 @@ function HeatmapGrid({ days, range, onRangeChange }: { {/* SVG */} - + {monthLabels.map(({weekIdx, label}) => ( {label} ))} @@ -316,7 +333,7 @@ function HeatmapGrid({ days, range, onRangeChange }: { {label} ))} {weeks.map((week, wi) => - week.map((cell, di) => cell.future ? null : ( + week.map((cell, di) => cell.outsideRange ? null : ( - contributions across all tracked repositories in the last {range} + contributions across all tracked repositories in the last {RANGE_SUMMARY[range]}