1import type { Save } from "../mirror/queries.ts";2import { type Html, html } from "./html.ts";3import { formatDate } from "./layout.ts";45const DAY = 86400;67const PACE: [number, string][] = [8 [1, "very active"],9 [0.3, "active"],10 [0.1, "occasional"],11];1213141516171819export function timelineSvg(repo: string, saves: Save[]): Html {20 const times = saves.map((save) => save.created_at);21 22 const first = times.reduce((a, b) => Math.min(a, b), times[0] ?? 0);23 const span = times.reduce((a, b) => Math.max(a, b), first) - first;24 const reach = Math.max(span / 40, 3600);25 const dots = saves.map((save) => {26 const x = span === 0 ? 50 : 1 + (98 * (save.created_at - first)) / span;27 28 29 const near = times.filter((t) => Math.abs(t - save.created_at) <= reach).length;30 const level = near >= 8 ? 3 : near >= 4 ? 2 : near >= 2 ? 1 : 0;31 const meta = [formatDate(save.created_at), save.agent, save.session]32 .filter(Boolean)33 .join(" · ");34 const tip = `${save.id}\n${save.message.split("\n")[0]}\n${meta}`;35 return html`<a href="/${36 repo37 }/saves/${save.id}" class="dot d${level}" data-t="${save.created_at}"><title>${tip}</title><circle cx="${x.toFixed(3)}%" cy="28" r="${2.5 + level * 0.5}"/></a>`;38 });39 const perDay = saves.length / Math.max(span / DAY, 1);40 const pace = PACE.find(([min]) => perDay > min)?.[1] ?? "quiet";41 return html`<svg class="timeline" width="100%" height="56" role="img" aria-label="${42 saves.length43 } saves over time">44<line class="timeline-line" x1="0" x2="100%" y1="28" y2="28"/>45${dots}46</svg>47<p class="timeline-legend mono">${pace} · density = activity · position = time</p>`;48}