feat(metrics): activity calendar grid

This commit is contained in:
Dave 2026-09-16 13:46:26 +02:00
parent b379669db3
commit dbf524b04c
2 changed files with 162 additions and 0 deletions

View file

@ -0,0 +1,69 @@
import { describe, it, expect } from "vitest";
import { buildHeatmap, HEATMAP_LEVELS } from "@/lib/metrics/heatmap";
describe("buildHeatmap", () => {
it("returns no weeks for an empty range", () => {
expect(buildHeatmap({}, "", "").weeks).toEqual([]);
});
it("starts each week on Monday", () => {
// 2026-09-01 is a Tuesday. The first column must begin on Monday the 31st
// so rows line up with the weekday labels.
const grid = buildHeatmap({ "2026-09-01": 1 }, "2026-09-01", "2026-09-07");
expect(grid.weeks[0][0].date).toBe("2026-08-31");
expect(grid.weeks[0][0].inRange).toBe(false);
expect(grid.weeks[0][1].date).toBe("2026-09-01");
expect(grid.weeks[0][1].inRange).toBe(true);
});
it("gives every week exactly seven days", () => {
const grid = buildHeatmap({}, "2026-05-24", "2026-09-08");
expect(grid.weeks.every((w) => w.length === 7)).toBe(true);
});
it("carries the commit count onto the right day", () => {
const grid = buildHeatmap({ "2026-09-02": 5 }, "2026-09-01", "2026-09-07");
const day = grid.weeks.flat().find((d) => d.date === "2026-09-02");
expect(day?.count).toBe(5);
});
it("assigns level 0 to a day with no commits", () => {
const grid = buildHeatmap({ "2026-09-02": 5 }, "2026-09-01", "2026-09-07");
const day = grid.weeks.flat().find((d) => d.date === "2026-09-03");
expect(day?.count).toBe(0);
expect(day?.level).toBe(0);
});
it("scales levels against the busiest day", () => {
const grid = buildHeatmap(
{ "2026-09-01": 1, "2026-09-02": 10, "2026-09-03": 20 },
"2026-09-01",
"2026-09-07"
);
const byDate = new Map(grid.weeks.flat().map((d) => [d.date, d]));
expect(byDate.get("2026-09-03")!.level).toBe(HEATMAP_LEVELS - 1);
expect(byDate.get("2026-09-01")!.level).toBe(1);
expect(byDate.get("2026-09-02")!.level).toBeGreaterThan(1);
});
it("gives any day with commits at least level 1", () => {
// A day someone worked must never look identical to a day they did not.
const grid = buildHeatmap(
{ "2026-09-01": 1, "2026-09-02": 500 },
"2026-09-01",
"2026-09-07"
);
const day = grid.weeks.flat().find((d) => d.date === "2026-09-01");
expect(day?.level).toBe(1);
});
it("reports the busiest day and total", () => {
const grid = buildHeatmap(
{ "2026-09-01": 3, "2026-09-02": 9 },
"2026-09-01",
"2026-09-07"
);
expect(grid.busiestCount).toBe(9);
expect(grid.totalCommits).toBe(12);
});
});

View file

@ -0,0 +1,93 @@
/**
* The activity calendar grid.
*
* Weeks are columns and weekdays are rows, so the shape of a project's
* working rhythm is visible at a glance. This was the most legible element of
* the developer's original build log.
*
* Weeks start on Monday, matching the Mon/Wed/Fri row labels and the workday
* framing used everywhere else.
*/
import { toIsoDate } from "@/lib/metrics/workdays";
/** Level 0 is "nothing"; 1..4 are increasing intensity. */
export const HEATMAP_LEVELS = 5;
const DAY_MS = 86_400_000;
export interface HeatmapDay {
date: string;
count: number;
level: number;
/** False for padding days outside the project's span. */
inRange: boolean;
}
export interface Heatmap {
weeks: HeatmapDay[][];
busiestCount: number;
totalCommits: number;
}
function toUtc(isoDate: string): number {
return Date.UTC(
Number(isoDate.slice(0, 4)),
Number(isoDate.slice(5, 7)) - 1,
Number(isoDate.slice(8, 10))
);
}
/** Monday of the week containing this date. */
function mondayOf(time: number): number {
const day = new Date(time).getUTCDay();
const back = day === 0 ? 6 : day - 1;
return time - back * DAY_MS;
}
export function buildHeatmap(
activity: Record<string, number>,
from: string,
to: string
): Heatmap {
if (!from || !to) return { weeks: [], busiestCount: 0, totalCommits: 0 };
const start = toUtc(from);
const end = toUtc(to);
if (end < start) return { weeks: [], busiestCount: 0, totalCommits: 0 };
let busiestCount = 0;
let totalCommits = 0;
for (const count of Object.values(activity)) {
totalCommits += count;
if (count > busiestCount) busiestCount = count;
}
const weeks: HeatmapDay[][] = [];
for (let cursor = mondayOf(start); cursor <= end; cursor += 7 * DAY_MS) {
const week: HeatmapDay[] = [];
for (let i = 0; i < 7; i++) {
const time = cursor + i * DAY_MS;
const date = toIsoDate(time);
const inRange = time >= start && time <= end;
const count = inRange ? (activity[date] ?? 0) : 0;
let level = 0;
if (count > 0 && busiestCount > 0) {
// Any day with work is at least level 1, so a day someone coded never
// looks identical to a day they did not — which a purely proportional
// scale would do whenever one day dwarfs the rest.
const share = count / busiestCount;
level = Math.max(1, Math.ceil(share * (HEATMAP_LEVELS - 1)));
}
week.push({ date, count, level, inRange });
}
weeks.push(week);
}
return { weeks, busiestCount, totalCommits };
}