timeline.ts

1.9 KB · raw

1import type { Save } from "../mirror/queries.ts";2import { type Html, html } from "./html.ts";3import { formatDate } from "./layout.ts";45const DAY = 86400;6// Kepr 2's labels for saves per day across the whole history.7const PACE: [number, string][] = [8	[1, "very active"],9	[0.3, "active"],10	[0.1, "occasional"],11];1213/**14 * §21.4's dotted timeline as SVG, so it draws without JavaScript: one dot per save on a dashed15 * line, x by time (as a percentage, so it scales without squashing dots), brighter and fuller16 * where neighbours crowd within a fortieth of the span.17 */18export function timelineSvg(repo: string, saves: Save[]): Html {19	const first = saves[0]?.created_at ?? 0;20	const span = (saves[saves.length - 1]?.created_at ?? 0) - first;21	const reach = Math.max(span / 40, 3600);22	const times = saves.map((save) => save.created_at);23	const dots = saves.map((save) => {24		const x = span === 0 ? 50 : 1 + (98 * (save.created_at - first)) / span;25		// ponytail: O(n²) neighbour count; a sliding window over sorted times if a repo reaches ~5,000 saves.26		const near = times.filter((t) => Math.abs(t - save.created_at) <= reach).length;27		const level = near >= 8 ? 3 : near >= 4 ? 2 : near >= 2 ? 1 : 0;28		const meta = [formatDate(save.created_at), save.agent, save.session]29			.filter(Boolean)30			.join(" · ");31		const tip = `${save.id}\n${save.message.split("\n")[0]}\n${meta}`;32		return html`<a href="/${repo}/saves/${save.id}" class="dot d${level}"><title>${tip}</title><circle cx="${x.toFixed(3)}%" cy="28" r="${2.5 + level * 0.5}"/></a>`;33	});34	const perDay = saves.length / Math.max(span / DAY, 1);35	const pace = PACE.find(([min]) => perDay > min)?.[1] ?? "quiet";36	return html`<svg class="timeline" width="100%" height="56" role="img" aria-label="${saves.length} saves over time">37<line class="timeline-line" x1="0" x2="100%" y1="28" y2="28"/>38${dots}39</svg>40<p class="timeline-legend mono">${pace} · density = activity · position = time</p>`;41}