timeline.ts

2.0 KB · raw

1import type { Save } from "../mirror/queries.ts";2import { type Html, html } from "./html.ts";3import { formatDate } from "./layout.ts";45const TIP_CHARS = 120;67/**8 * §21.4's dotted timeline as SVG, so it draws without JavaScript (kepr.js swaps it for Kepr 2's9 * drifting canvas, reading each save from its dot): one dot per save on a dashed line, x by10 * time (as a percentage, so it scales without squashing dots), brighter and fuller where11 * neighbours crowd within a fortieth of the span.12 */13export function timelineSvg(repo: string, saves: Save[]): Html {14	const times = saves.map((save) => save.created_at);15	// Replayed saves keep their own times, so the line is not in time order.16	const first = times.reduce((a, b) => Math.min(a, b), times[0] ?? 0);17	const span = times.reduce((a, b) => Math.max(a, b), first) - first;18	const reach = Math.max(span / 40, 3600);19	const dots = saves.map((save) => {20		const x = span === 0 ? 50 : 1 + (98 * (save.created_at - first)) / span;21		// ponytail: O(n²) neighbour count; a sliding window over sorted times if a repo22		// reaches ~5,000 saves.23		const near = times.filter((t) => Math.abs(t - save.created_at) <= reach).length;24		const level = near >= 8 ? 3 : near >= 4 ? 2 : near >= 2 ? 1 : 0;25		const meta = [formatDate(save.created_at), save.agent, save.session]26			.filter(Boolean)27			.join(" · ");28		// The tip is 340 px wide; a whole message wrapped to many lines and was most of a big29		// repo's page.30		const line = save.message.split("\n")[0] as string;31		const message = line.length > TIP_CHARS ? `${line.slice(0, TIP_CHARS - 1)}…` : line;32		const tip = `${save.id}\n${message}\n${meta}`;33		return html`<a href="/${34			repo35		}/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>`;36	});37	return html`<svg class="timeline" width="100%" height="56" role="img" aria-label="${38		saves.length39	} saves over time">40<line class="timeline-line" x1="0" x2="100%" y1="28" y2="28"/>41${dots}42</svg>`;43}