1// The activity grid (ui.activity in site/_includes/ui.njk): a year of days,2// a column a week (Sunday at the top), each day a cell coloured by how much3// happened on it. The counts come from the lake's exports (lib/insights.ts):4// commits for an owner's page, events for a repo's insights. Days are UTC.5// site/assets/ui.ts recounts and filters it in the browser; this is the6// grid as the page has it without JS.7import fs from 'node:fs';8import path from 'node:path';910export const WEEKS = 53;1112export interface Cell {13 day: string;14 n: number;15 level: number;16 // Weeks before the last column (0 for this week's): where the grid is too17 // narrow for every week at its smallest, the oldest go (style.css).18 age: number;19}2021export interface Grid {22 // Columns of up to seven days; the last ends today.23 weeks: Cell[][];24 total: number;25 noun: string;26 today: string;27}2829// 0 for nothing, then 1 to 4 by the share of the busiest day (as ui.ts does).30export const level = (n: number, max: number): number => (n <= 0 || max <= 0 ? 0 : Math.min(4, Math.ceil((4 * n) / max)));3132const today = (): Date => new Date(Number(process.env.FORGE_NOW) * 1000 || Date.now());33const iso = (t: Date): string => t.toISOString().slice(0, 10);3435export function grid(counts: Record<string, number>, noun: string): Grid {36 const end = today();37 const last = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate());38 const start = last - (new Date(last).getUTCDay() + (WEEKS - 1) * 7) * 86400000;39 const days: { day: string; n: number }[] = [];40 for (let t = start; t <= last; t += 86400000) {41 const day = iso(new Date(t));42 days.push({ day, n: counts[day] ?? 0 });43 }44 const max = Math.max(0, ...days.map((d) => d.n));45 const weeks: Cell[][] = [];46 days.forEach((d, i) => {47 const w = Math.floor(i / 7);48 (weeks[w] ??= []).push({ ...d, level: level(d.n, max), age: WEEKS - 1 - w });49 });50 return { weeks, total: days.reduce((a, d) => a + d.n, 0), noun, today: iso(end) };51}5253// An export of the lake's, or null (no lake yet, or not exported).54export function readExport<T>(state: string, ...p: string[]): T | null {55 try {56 return JSON.parse(fs.readFileSync(path.join(state, 'lake', 'export', ...p), 'utf8')) as T;57 } catch {58 return null;59 }60}6162export interface PersonExport {63 // Repo name: its commits' times in minutes, delta-encoded (encodeMinutes64 // in lib/insights.ts).65 repos: Record<string, number[]>;66 joined: number | null;67}6869export const decodeMinutes = (enc: number[]): number[] => {70 let m = 0;71 return enc.map((d, i) => (m = i ? m + d : d));72};7374export interface RepoActivityExport {75 days: Record<string, number>;76 // Each event's time (ms), newest first.77 events: number[];78}