From be49a69b2f2777d98b6b34550944403e56691b9c Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 13:51:18 +0200 Subject: [PATCH] feat(dashboard): SVG activity calendar and bar charts --- .../dashboard/activity-calendar.tsx | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/components/dashboard/activity-calendar.tsx diff --git a/src/components/dashboard/activity-calendar.tsx b/src/components/dashboard/activity-calendar.tsx new file mode 100644 index 0000000..0e06259 --- /dev/null +++ b/src/components/dashboard/activity-calendar.tsx @@ -0,0 +1,131 @@ +import { buildHeatmap, HEATMAP_LEVELS } from "@/lib/metrics/heatmap"; + +const CELL = 11; +const GAP = 3; +const LABEL_WIDTH = 26; + +/** Level 0 is the empty-day colour; 1..4 step up in brand opacity. */ +function fill(level: number): string { + if (level === 0) return "var(--surface-raised)"; + const opacity = 0.25 + (level / (HEATMAP_LEVELS - 1)) * 0.75; + return `color-mix(in oklab, var(--brand) ${Math.round(opacity * 100)}%, transparent)`; +} + +const MONTHS = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +export function ActivityCalendar({ + activity, + from, + to, + activeDays, + workdays, +}: { + activity: Record; + from: string; + to: string; + activeDays: number; + workdays: number; +}) { + const grid = buildHeatmap(activity, from, to); + if (grid.weeks.length === 0) return null; + + const width = LABEL_WIDTH + grid.weeks.length * (CELL + GAP); + const height = 7 * (CELL + GAP) + 18; + + // A month label above the first week whose Monday falls in a new month. + const monthLabels: Array<{ x: number; label: string }> = []; + let lastMonth = ""; + grid.weeks.forEach((week, index) => { + const month = week[0].date.slice(0, 7); + if (month !== lastMonth) { + monthLabels.push({ + x: LABEL_WIDTH + index * (CELL + GAP), + label: MONTHS[Number(week[0].date.slice(5, 7)) - 1], + }); + lastMonth = month; + } + }); + + return ( +
+
+

+ Activity +

+

+ {activeDays} active of{" "} + {workdays} workdays ยท {from} to {to} +

+
+ +
+ + {monthLabels.map((label) => ( + + {label.label} + + ))} + + {["Mon", "Wed", "Fri"].map((name, i) => ( + + {name} + + ))} + + {grid.weeks.map((week, weekIndex) => + week.map((day, dayIndex) => { + if (!day.inRange) return null; + return ( + + + {day.date}: {day.count}{" "} + {day.count === 1 ? "commit" : "commits"} + + + ); + }) + )} + +
+ +
+ Less + {Array.from({ length: HEATMAP_LEVELS }, (_, level) => ( + + ))} + More +
+
+ ); +} \ No newline at end of file