Something went wrong. Try again.
unified development activity
Something went wrong. Try again.
115 kB · 3154 lines
JavaScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155"use strict";
const $ = (id) => document.getElementById(id);// One period for the whole page. Every selector sets it and every card that// covers a span of time follows it; the heatmap alone stays a year.const PERIODS = { "30d": { days: 30, label: "30 days" }, "90d": { days: 90, label: "90 days" }, "1y": { days: 365, label: "1 year" }, all: { days: Infinity, label: "all time" },};const DEFAULT_PERIOD = "30d";// How the date chart and the wheel stack their commits.const BREAKDOWNS = ["project", "identity"];const DEFAULT_BREAKDOWN = "project";
const state = { data: null, period: DEFAULT_PERIOD, by: DEFAULT_BREAKDOWN, // One Table toggle opens both stacked charts' tables. table: false, typesTable: false, // The token burn pulse over the date chart and the wheel. burn: true, timelinesTable: false, loading: false, // The UTC day open in the day panel, and what gets focus back on close: // the heatmap cell for that day, or "best" for the best-day link. openDay: null, dayReturn: null, // The heatmap cell that takes Tab; the arrow keys move it. matrixFocus: null,};
/* ---------- formatting ---------- */
const number = new Intl.NumberFormat(undefined);const compactNumber = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1,});
function fmt(n) { return n >= 100000 ? compactNumber.format(n) : number.format(n);}
function pct(n) { return `${Number(n).toFixed(1)}%`;}
function seriesColor(color) { return `var(--${color || "series-1"})`;}
function shortDate(iso) { const date = new Date(iso); if (Number.isNaN(date.getTime())) return iso; return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });}
function dateTime(iso) { const date = new Date(iso); if (Number.isNaN(date.getTime())) return iso; return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", });}
function dayLabel(day) { const date = new Date(`${day}T12:00:00Z`); return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });}
function el(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node;}
// A dense row of label/value pairs. `meta` trails the value in muted type;// `title` holds detail that does not need the space; `href` makes the value// a link and `onclick` runs when it is followed.function statRow(container, items) { container.replaceChildren(); for (const item of items) { const stat = el("div", "stat"); const value = el("dd"); const body = item.href ? el("a", "stat-link") : value; body.append(item.value); if (item.meta) body.append(el("span", "stat-meta", item.meta)); if (item.href) { body.href = item.href; if (item.onclick) body.addEventListener("click", item.onclick); value.append(body); } stat.append(el("dt", "", item.label), value); if (item.title) stat.title = item.title; container.append(stat); }}
/* ---------- series ---------- */
// The series the date chart and the wheel stack, bottom up, for the// chosen breakdown.function seriesList() { if (!state.data) return []; return state.by === "identity" ? identitySeries() : projectSeries();}
// The projects, named like the sidebar, then everything else. Entry i owns// column i of a day's `projects` and of an hourly project row after its// hour. A project's colour comes with the data and stays put whatever its// rank or the period shown.function projectSeries() { return [ ...(state.data.chart_projects || []).map((project) => ({ label: project.full_name || project.name, color: project.color, project, })), { label: "other", color: "muted" }, ].map((item, index) => ({ ...item, index, kind: "project" }));}
const IDENTITY_ORDER = [ "series-red", "series-1", "series-3", "series-4", "series-5", "series-6", "series-7",];
// Each personal handle and GitHub account in its configured colour, then// every agent together.function identitySeries() { const data = state.data; const out = data.identities .filter((identity) => identity.kind === "personal" || identity.kind === "github") .map((identity) => ({ key: identity.did, label: identity.handle, color: identity.color, kind: identity.kind, })); if (data.summary.bots_tangled > 0) { out.push({ key: "bots", label: "did.bot agents", color: "series-5", kind: "bots" }); } const slot = (color) => { const index = IDENTITY_ORDER.indexOf(color); return index >= 0 ? index : 99; }; return out.sort((a, b) => slot(a.color) - slot(b.color));}
function valueFor(point, series) { if (series.kind === "project") return (point.projects && point.projects[series.index]) || 0; if (series.kind === "bots") return point.bots || 0; return (point.identities && point.identities[series.key]) || 0;}
// The series with any commits over the whole window, for the legends.function legendSeries() { return seriesList().filter((item) => state.data.daily.some((point) => valueFor(point, item) > 0), );}
function tooltipRow(tooltip, label, value, color) { const row = el("div", "tip-row"); const name = el("span", "tip-name"); if (color) { const key = el("span", "tip-key"); key.style.background = color; name.append(key); } name.append(el("span", "", label)); row.append(name, el("span", "tip-value", value)); tooltip.append(row);}
// A day's or hour's projects, busiest first, "other" last.function tooltipSeries(tooltip, series, valueOf) { series .map((item) => ({ item, value: valueOf(item) })) .filter((entry) => entry.value > 0) .sort((a, b) => (a.item.color === "muted") - (b.item.color === "muted") || b.value - a.value) .forEach(({ item, value }) => tooltipRow(tooltip, item.label, number.format(value), seriesColor(item.color)), );}
function periodDays() { return PERIODS[state.period].days;}
// The daily and hourly series hold one year, so "all" shows that year.function seriesLabel() { return state.period === "all" ? PERIODS["1y"].label : PERIODS[state.period].label;}
function visiblePoints() { return state.data.daily.slice(-periodDays());}
/* ---------- load ---------- */
async function load() { // A single-file mockup embeds the data before this script, so it can be // opened without a server or a fetch. if (window.__TALLY_DATA__) { state.data = window.__TALLY_DATA__; render(); return; } const main = document.querySelector("main"); if (state.loading) return; state.loading = true; main.classList.add("loading"); try { const response = await fetch("data/site.json", { cache: "no-store" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); state.data = await response.json(); // New data means new day files; the open day refetches on next open. dayShards.clear(); render(); } catch (error) { $("asof").textContent = `could not load data: ${error}`; } finally { state.loading = false; main.classList.remove("loading"); }}
/* ---------- render ---------- */
function render() { const data = state.data; const summary = data.summary; document.title = `${summary.title} · dev.permadeath.com`; $("title").textContent = summary.title; $("asof").textContent = `updated ${dateTime(summary.generated_at)}`;
renderMatrix(); renderProjects(); renderShare(summary); renderShareTrend(summary); renderKpis(summary); renderLegend(); renderChart(); renderTable(); renderWheel(); renderCommitTypes(); renderTimelines(); renderLandGap(); renderRepos(); renderRecent(); renderStatus(); syncDay(); startActivity();}
/* ---------- agentic activity: live agent lines from vibescrobble ---------- */
// The card polls in the browser while the tab is visible, keeps one row per// agent (its newest line in the last day), and moves rows in place rather// than redrawing, so a new line slides in at the top. Everything shown is// set with textContent; a repo is linked only when it is a public Tangled// repo this page already lists, and nothing else is named.const ACTIVITY_POLL_MS = 75 * 1000;const ACTIVITY_TICK_MS = 15 * 1000;const ACTIVITY_ACTIVE_MS = 15 * 60 * 1000;const ACTIVITY_FRESH_MS = 10 * 60 * 1000;const ACTIVITY_WINDOW_MS = 24 * 60 * 60 * 1000;// Rows in the single-column layout, where the card has no height to fill.const ACTIVITY_NARROW_ROWS = 6;const activityNarrow = window.matchMedia("(max-width: 860px)");
const activity = { started: false, fetchedAt: 0, pollTimer: null, tickTimer: null, inFlight: false, // Newest line per agent DID: { item, at, row }. Only the rows that fit // are in the DOM. agents: new Map(), observer: null,};
// An http(s) URL as a string, or "" for anything else.function safeUrl(value) { try { const url = new URL(value); return url.protocol === "https:" || url.protocol === "http:" ? url.href : ""; } catch { return ""; }}
function activityFeed() { const feed = state.data && state.data.summary && state.data.summary.activity; const base = feed ? safeUrl(feed.url) : ""; return base && feed.operator ? { ...feed, base: base.replace(/\/+$/, "") } : null;}
function startActivity() { if (activity.started) return; activity.started = true; // The single-file mockup is a deterministic preview: no live card unless // asked for with ?activity=live. if (window.__TALLY_DATA__ && new URLSearchParams(window.location.search).get("activity") !== "live") { hideActivity(); return; } document.addEventListener("visibilitychange", () => { if (document.hidden) { pauseActivity(); } else { if (Date.now() - activity.fetchedAt >= ACTIVITY_POLL_MS) pollActivity(); resumeActivity(); } }); pollActivity(); resumeActivity();}
function resumeActivity() { if (document.hidden) return; clearInterval(activity.pollTimer); clearInterval(activity.tickTimer); activity.pollTimer = setInterval(pollActivity, ACTIVITY_POLL_MS); activity.tickTimer = setInterval(tickActivity, ACTIVITY_TICK_MS);}
function pauseActivity() { clearInterval(activity.pollTimer); clearInterval(activity.tickTimer); activity.pollTimer = null; activity.tickTimer = null; const live = $("activity-live"); live.classList.add("paused"); $("activity-updated").textContent = "paused";}
function hideActivity() { $("activity").hidden = true; $("activity-list").replaceChildren(); activity.agents.clear();}
async function pollActivity() { const feed = activityFeed(); if (!feed) { hideActivity(); return; } if (activity.inFlight || document.hidden) return; activity.inFlight = true; let items; try { const url = new URL(`${feed.base}/scrobbles`); url.searchParams.set("operator", feed.operator); url.searchParams.set("limit", "60"); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 8000); const response = await fetch(url, { cache: "no-store", credentials: "omit", signal: controller.signal, }); clearTimeout(timer); if (!response.ok) throw new Error(`HTTP ${response.status}`); const body = await response.json(); items = Array.isArray(body) ? body : body && Array.isArray(body.items) ? body.items : []; } catch { hideActivity(); return; } finally { activity.inFlight = false; } activity.fetchedAt = Date.now(); updateActivity(items, feed);}
function normalRepoUrl(value) { try { const url = new URL(value); if (url.protocol !== "https:") return ""; return `https://${url.host.toLowerCase()}${url.pathname.replace(/\.git$/, "").replace(/\/+$/, "")}`; } catch { return ""; }}
// The repositories a line may link to: public Tangled repos this page// already names. Anything else, GitHub included, is not shown at all.function activityRepos() { const data = state.data; const allowed = new Map(); const add = (item) => { if (!item || item.redacted) return; const key = normalRepoUrl(item.url || ""); if (!key.startsWith("https://tangled.org/") || allowed.has(key)) return; allowed.set(key, { url: item.url, name: item.full_name || item.name || key, project: item }); }; (data.repos || []).forEach(add); (data.projects || []).forEach(add); ((data.project_timelines && data.project_timelines.projects) || []).forEach(add); return allowed;}
function agentName(did) { const identity = (state.data.identities || []).find((item) => item.did === did); const handle = (identity && identity.handle) || ""; const web = /^did:web:([^.:]+)/.exec(did); if (handle && !handle.startsWith("did:")) return handle.split(".")[0]; if (web) return web[1]; return `${did.slice(0, 16)}…`;}
function firstGrapheme(text) { const value = String(text || ""); if (typeof Intl !== "undefined" && Intl.Segmenter) { const first = new Intl.Segmenter(undefined, { granularity: "grapheme" }) .segment(value) [Symbol.iterator]() .next(); return first.done ? "" : first.value.segment; } return Array.from(value)[0] || "";}
function ageLabel(ms) { const minutes = Math.floor(Math.max(0, ms) / 60000); if (minutes < 1) return "now"; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;}
// One row's contents, rebuilt only when its line changes.function fillActivityRow(row, item, feed, repos) { row.replaceChildren(); row.append(el("span", "activity-emoji", firstGrapheme(item.emoji))); const body = el("div", "activity-body"); if (feed.text && typeof item.text === "string" && item.text) { const text = el("p", "activity-text", item.text); text.title = item.text; body.append(text); } const meta = el("p", "activity-meta"); meta.append(el("span", "activity-agent", agentName(item.did))); if (feed.text) { if (typeof item.model === "string" && item.model) meta.append(el("span", "", item.model)); const repo = item.repo && typeof item.repo.url === "string" ? repos.get(normalRepoUrl(item.repo.url)) : undefined; if (repo) { const project = el("span", "activity-project"); project.append(el("span", "activity-repo", repo.name)); const links = projectLinks(repo.project); if (links) project.append(links); meta.append(project); } } body.append(meta); row.append(body, el("span", "activity-age"));}
function updateActivity(items, feed) { const now = Date.now(); const newest = new Map(); for (const item of items) { if (!item || typeof item.did !== "string") continue; const at = Date.parse(item.createdAt); if (!Number.isFinite(at) || now - at > ACTIVITY_WINDOW_MS) continue; const seen = newest.get(item.did); if (!seen || at > seen.at) newest.set(item.did, { item, at }); } if (newest.size === 0) { hideActivity(); return; } const repos = activityRepos(); const first = activity.agents.size === 0; for (const [did, entry] of activity.agents) { if (!newest.has(did)) { entry.row.remove(); activity.agents.delete(did); } } for (const [did, next] of newest) { const known = activity.agents.get(did); if (known && known.item.uri === next.item.uri && known.at === next.at) continue; const row = known ? known.row : el("li", "activity-row"); row.dataset.did = did; fillActivityRow(row, next.item, feed, repos); if (!first) { // A line that arrived since the last poll flashes once. row.classList.remove("is-new"); void row.offsetWidth; row.classList.add("is-new"); } activity.agents.set(did, { item: next.item, at: next.at, row }); } fitActivity();
const operator = (state.data.identities || []).find((item) => item.did === feed.operator); $("activity-source").href = operator && operator.handle && !operator.handle.startsWith("did:") ? `${feed.base}/@${encodeURIComponent(operator.handle)}` : feed.base; $("activity").hidden = false; $("activity-live").classList.remove("paused"); tickActivity();}
// The card never scrolls: newest first, as many rows as fit its height// (a fixed count in one column), and the rest leave the DOM. A row already// in place is not touched.function fitActivity() { const list = $("activity-list"); const frame = list.parentElement; if (!activity.observer) { activity.observer = new ResizeObserver(() => fitActivity()); activity.observer.observe(frame); } const narrow = activityNarrow.matches; const room = frame.clientHeight; const order = [...activity.agents.values()].sort((a, b) => b.at - a.at); let shown = 0; for (const entry of order) { if (narrow ? shown >= ACTIVITY_NARROW_ROWS : room <= 0) break; if (list.children[shown] !== entry.row) list.insertBefore(entry.row, list.children[shown] || null); if (!narrow && entry.row.offsetTop + entry.row.offsetHeight > room) break; shown += 1; } while (list.children.length > shown) list.lastElementChild.remove();}
// Ages, the active count and the freshness line move on between polls.function tickActivity() { if ($("activity").hidden) return; const now = Date.now(); let active = 0; let latest = 0; for (const { at, row } of activity.agents.values()) { const age = row.querySelector(".activity-age"); age.textContent = ageLabel(now - at); age.title = dateTime(new Date(at).toISOString()); age.classList.toggle("fresh", now - at < ACTIVITY_FRESH_MS); if (now - at <= ACTIVITY_ACTIVE_MS) active += 1; latest = Math.max(latest, at); } $("activity-sub").textContent = active > 0 ? `${plural(active, "agent")} active in the last 15 min` : `last line ${ageLabel(now - latest)} ago`; const since = now - activity.fetchedAt; $("activity-updated").textContent = since < 60000 ? "live" : `live · ${ageLabel(since)} ago`;}
function dayDate(day) { return new Date(`${day}T12:00:00Z`);}
function matrixLevels(days) { const values = days .map((point) => point.total) .filter((value) => value > 0) .sort((a, b) => a - b); if (values.length === 0) return [1, 2, 3, 4]; const pick = (fraction) => values[Math.min(values.length - 1, Math.floor((values.length - 1) * fraction))]; let previous = 0; return [pick(0.25), pick(0.5), pick(0.75), pick(0.9)].map((value) => { previous = Math.max(previous + 1, value); return previous; });}
function matrixLevel(value, thresholds) { if (value <= 0) return 0; if (value <= thresholds[0]) return 1; if (value <= thresholds[1]) return 2; if (value <= thresholds[2]) return 3; if (value <= thresholds[3]) return 4; return 5;}
function renderMatrix() { const svg = $("matrix"); const days = state.data.daily || []; const total = days.reduce((sum, point) => sum + (point.total || 0), 0); $("matrix-total").textContent = fmt(total); svg.replaceChildren(); if (days.length === 0) return;
const start = dayDate(days[0].date); const startWeekday = start.getUTCDay(); const columns = Math.ceil((startWeekday + days.length) / 7); const cell = 12; const gap = 3; const step = cell + gap; const left = 30; const top = 18; const width = left + columns * step; const height = top + 7 * step; svg.setAttribute("viewBox", `0 0 ${width} ${height}`); svg.setAttribute("preserveAspectRatio", "xMinYMin meet");
const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; };
// Month labels sit at the first week that falls inside a new month. let lastMonth = -1; for (let column = 0; column < columns; column += 1) { const firstIndex = Math.max(0, column * 7 - startWeekday); if (firstIndex >= days.length) break; const date = dayDate(days[firstIndex].date); const month = date.getUTCMonth(); if (month === lastMonth) continue; lastMonth = month; const label = make("text", { x: left + column * step, y: 10, class: "matrix-month", }); label.textContent = date.toLocaleDateString(undefined, { month: "short" }).toUpperCase(); svg.append(label); }
for (const [row, label] of [[1, "MON"], [3, "WED"], [5, "FRI"]]) { const node = make("text", { x: 0, y: top + row * step + cell - 2, class: "matrix-weekday", }); node.textContent = label; svg.append(node); }
const thresholds = matrixLevels(days); if (!days.some((point) => point.date === state.matrixFocus)) { state.matrixFocus = days[days.length - 1].date; } for (let index = 0; index < columns * 7; index += 1) { const dayIndex = index - startWeekday; if (dayIndex < 0 || dayIndex >= days.length) continue; const point = days[dayIndex]; const value = point.total || 0; const level = matrixLevel(value, thresholds); const column = Math.floor(index / 7); const row = index % 7; const rect = make("rect", { x: left + column * step, y: top + row * step, width: cell, height: cell, rx: 2, class: "matrix-cell", "data-level": level, "data-date": point.date, "aria-label": `${point.date}: ${value} commits`, role: "button", tabindex: point.date === state.matrixFocus ? 0 : -1, }); rect.style.fill = `var(--matrix-${level})`; if (point.date === state.openDay) rect.classList.add("selected"); rect.addEventListener("pointerenter", (event) => showMatrixTooltip(event, point)); rect.addEventListener("pointermove", (event) => showMatrixTooltip(event, point)); rect.addEventListener("pointerleave", hideMatrixTooltip); rect.addEventListener("click", () => selectDay(point.date, point.date)); rect.addEventListener("keydown", (event) => matrixKey(event, days, dayIndex)); svg.append(rect); }
const activeDays = days.filter((point) => (point.total || 0) > 0).length; const best = days.reduce( (winner, point) => ((point.total || 0) > (winner.total || 0) ? point : winner), days[0], ); let streak = 0; for (let index = days.length - 1; index >= 0; index -= 1) { if ((days[index].total || 0) > 0) streak += 1; else break; } // Typical is the median active day, busy the 90th percentile, over the // page's period with the year beside it; at a year or more there is only // the year. const short = periodDays() < 365; const year = activeCadence(days); const recent = short ? activeCadence(days.slice(-periodDays())) : year; const span = short ? state.period : "1y"; const spanText = short ? `the last ${PERIODS[state.period].label}` : "the last year"; const cadence = (value) => (value === null ? "—" : rate(value)); const cadenceItems = year.active === 0 ? [] : [ { label: `${span} typical`, value: cadence(recent.typical), meta: short ? `1y ${cadence(year.typical)}` : "", title: `Median active day: ${cadence(recent.typical)} over ${spanText} (${recent.active} active days)` + (short ? `, ${cadence(year.typical)} over the last year` : ""), }, { label: "busy", value: cadence(recent.busy), meta: short ? `1y ${cadence(year.busy)}` : "", title: `90th percentile active day: ${cadence(recent.busy)} over ${spanText}` + (short ? `, ${cadence(year.busy)} over the last year` : ""), }, ]; statRow($("matrix-stats"), [ ...cadenceItems, { label: "active", value: `${activeDays}/${days.length}`, title: `${Math.round((activeDays / days.length) * 100)}% of days had a commit`, }, { label: "best", value: fmt(best ? best.total : 0), meta: best ? dayLabel(best.date) : "", ...(best && best.total > 0 ? { href: dayHash(best.date), title: `Show ${dayLabel(best.date)}`, onclick: (event) => { event.preventDefault(); selectDay(best.date, "best"); }, } : {}), }, { label: "streak", value: `${streak}d`, title: "current run of days with a commit" }, ]);
const legend = $("matrix-legend"); legend.replaceChildren(); legend.append(el("li", "", "less")); for (let level = 1; level <= 5; level += 1) { const item = el("li"); const swatch = el("span", "matrix-swatch"); swatch.style.background = `var(--matrix-${level})`; item.append(swatch); legend.append(item); } legend.append(el("li", "", "more"));
const firstActive = days.find((point) => (point.total || 0) > 0); const note = $("matrix-note"); if (!firstActive) { note.textContent = "no activity · utc"; } else if (firstActive.date !== days[0].date) { note.textContent = `from ${dayLabel(firstActive.date)} · utc`; } else { note.textContent = "utc"; }
renderSparkline(days);}
function rate(value) { return Number.isInteger(value) ? fmt(value) : value.toFixed(1);}
function median(values) { if (values.length === 0) return 0; const middle = Math.floor(values.length / 2); return values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2;}
function percentile(values, fraction) { if (values.length === 0) return 0; const index = Math.min(values.length - 1, Math.ceil(fraction * values.length) - 1); return values[Math.max(0, index)];}
function activeCadence(points) { const active = points .map((point) => Math.max(0, point.total || 0)) .filter((value) => value > 0) .sort((a, b) => a - b); if (active.length === 0) return { typical: null, busy: null, active: 0 }; return { typical: median(active), busy: percentile(active, 0.9), active: active.length, };}
function renderSparkline(days) { const wrap = $("matrix-spark"); const svg = $("spark"); const values = days.map((point) => Math.max(0, point.total || 0)); const peak = Math.max(...values, 0); if (peak <= 0) { wrap.hidden = true; svg.replaceChildren(); return; }
const average = values.map((_, index) => { const start = Math.max(0, index - 6); const window = values.slice(start, index + 1); return window.reduce((sum, value) => sum + value, 0) / window.length; });
const width = 1000; const height = 72; const padX = 2; const padY = 7; const x = (index) => padX + (index / Math.max(1, values.length - 1)) * (width - padX * 2); const y = (value) => height - padY - (value / peak) * (height - padY * 2); const path = (series) => series .map((value, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(2)},${y(value).toFixed(2)}`) .join(" ");
svg.replaceChildren(); const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; };
svg.append( make("line", { x1: padX, x2: width - padX, y1: height - padY, y2: height - padY, class: "spark-baseline", }), ); const areaPath = `${path(values).replace(/^M/, "M")} L${x(values.length - 1).toFixed(2)},${height - padY} L${x(0).toFixed(2)},${height - padY} Z`; svg.append(make("path", { d: areaPath, class: "spark-area" })); svg.append(make("path", { d: path(values), class: "spark-daily" })); svg.append(make("path", { d: path(average), class: "spark-average" }));
const best = days.reduce( (winner, point) => ((point.total || 0) > (winner.total || 0) ? point : winner), days[0], ); const title = make("title"); title.textContent = `Daily commits, ${fmt(values.reduce((sum, value) => sum + value, 0))} total; peak ${fmt(peak)} on ${best.date}`; svg.prepend(title); svg.setAttribute( "aria-label", `Daily commits with a seven-day average. Peak ${fmt(peak)} on ${best.date}.`, ); wrap.hidden = false;}
function showMatrixTooltip(event, point) { const tooltip = $("matrix-tooltip"); tooltip.replaceChildren(); tooltip.append(el("div", "tip-date", point.date)); tooltipSeries(tooltip, seriesList(), (item) => valueFor(point, item)); tooltipRow(tooltip, "total", number.format(point.total || 0), "var(--ink)"); tooltip.hidden = false;
const main = document.querySelector(".matrix-main").getBoundingClientRect(); let left = (event.clientX || 0) - main.left + 12; let top = (event.clientY || 0) - main.top + 12; const box = tooltip.getBoundingClientRect(); if (left + box.width > main.width - 6) left = Math.max(6, left - box.width - 24); if (top + box.height > main.height - 6) top = Math.max(6, top - box.height - 24); tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`;}
function hideMatrixTooltip() { $("matrix-tooltip").hidden = true;}
/* ---------- day panel ---------- */
// Arrow keys walk the heatmap like its grid: up and down a day, left and// right a week. Enter or Space opens the day.function matrixKey(event, days, index) { const moves = { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -7, ArrowRight: 7 }; let next = index; if (event.key in moves) next = index + moves[event.key]; else if (event.key === "Home") next = 0; else if (event.key === "End") next = days.length - 1; else if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectDay(days[index].date, days[index].date); return; } else return; event.preventDefault(); next = Math.max(0, Math.min(days.length - 1, next)); focusMatrixCell(days[next].date);}
function matrixCell(day) { return document.querySelector(`#matrix .matrix-cell[data-date="${day}"]`);}
function focusMatrixCell(day) { const cell = matrixCell(day); if (!cell) return; for (const other of document.querySelectorAll('#matrix .matrix-cell[tabindex="0"]')) { other.setAttribute("tabindex", "-1"); } cell.setAttribute("tabindex", "0"); state.matrixFocus = day; cell.focus();}
// The fragment is `key=value` pairs so other deep links can share it; a// bare anchor such as `#repos-title` carries none.function hashParams() { const hash = window.location.hash.slice(1); return new URLSearchParams(hash.includes("=") ? hash : "");}
function validDay(day) { if (!/^\d{4}-\d{2}-\d{2}$/.test(day || "")) return null; const date = new Date(`${day}T00:00:00Z`); return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === day ? day : null;}
function dayHash(day) { const params = hashParams(); if (day) params.set("day", day); else params.delete("day"); const text = params.toString(); return text ? `#${text}` : `${window.location.pathname}${window.location.search}`;}
// A click names the day and what should take focus back. A new day goes// through the fragment, so Back and Forward replay it.function selectDay(day, returnTo) { state.dayReturn = returnTo; if (validDay(hashParams().get("day")) === day) { if (state.openDay !== day) openDay(day); $("day-panel").focus(); return; } window.location.hash = dayHash(day).slice(1);}
function syncDay() { if (!state.data) return; const day = validDay(hashParams().get("day")); if (day === state.openDay) return; if (day) openDay(day); else hideDay();}
const dayShards = new Map();
function dayShard(month) { if (!dayShards.has(month)) { const request = fetch(`data/days/${month}.json`, { cache: "no-cache" }).then((response) => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }); request.catch(() => dayShards.delete(month)); dayShards.set(month, request); } return dayShards.get(month);}
function plural(count, word) { return `${fmt(count)} ${word}${count === 1 ? "" : "s"}`;}
async function openDay(day) { state.openDay = day; for (const cell of document.querySelectorAll("#matrix .matrix-cell.selected")) { cell.classList.remove("selected"); } const cell = matrixCell(day); if (cell) { cell.classList.add("selected"); if (state.dayReturn === null) state.dayReturn = day; // On a narrow screen the heatmap scrolls sideways; bring the day into // view without moving the page. const scroller = cell.closest(".matrix-scroll"); const box = cell.getBoundingClientRect(); const outer = scroller ? scroller.getBoundingClientRect() : box; if (scroller && (box.left < outer.left || box.right > outer.right)) { scroller.scrollLeft += box.left - outer.left - outer.width / 2; } } const panel = $("day-panel"); const status = $("day-status"); const list = $("day-list"); const year = new Date(`${day}T12:00:00Z`).getUTCFullYear(); $("day-title").textContent = `${dayLabel(day)}, ${year}`; list.replaceChildren(); panel.hidden = false; panel.focus();
const days = state.data.daily || []; if (days.length === 0 || day < days[0].date || day > days[days.length - 1].date) { status.textContent = "outside the last 365 days"; return; } status.textContent = "loading…"; let shard; try { shard = await dayShard(day.slice(0, 7)); } catch (error) { if (state.openDay === day) status.textContent = "could not load this day"; return; } if (state.openDay !== day) return; const view = (shard.days || {})[day] || { commits: [], private: 0 }; const commits = view.commits || []; const hidden = view.private || 0; const total = commits.length + hidden; if (total === 0) { status.textContent = "no commits this day"; return; } const projects = new Set(commits.map((commit) => commit.project)); status.textContent = [ plural(total, "commit"), hidden ? `${fmt(hidden)} private` : "", projects.size ? plural(projects.size, "project") : "", ] .filter(Boolean) .join(" · "); // A project's links show on its first row only. const linked = new Set(); for (const commit of commits) { const project = (shard.projects || [])[commit.project] || {}; // A commit carries its own URL only when it isn't the project's // `{url}/commit/{sha}`; an empty one means there is no page. const found = commit.url ?? (project.url ? `${project.url}/commit/${commit.sha}` : ""); const url = /^https?:\/\//.test(found) ? found : ""; const short = String(commit.sha || "").slice(0, 10); const item = el("li", "day-commit"); item.append(el("span", "day-time", commit.time)); const name = el("span", "day-project"); const label = el("span", "day-project-name", project.name || ""); if (project.full_name) label.title = project.full_name; name.append(label); const links = linked.has(commit.project) ? null : projectLinks(project); if (links) { linked.add(commit.project); name.append(links); } item.append(name); const sha = url ? el("a", "day-sha", short) : el("span", "day-sha", short); if (url) sha.href = url; item.append(sha, el("span", "day-subject", commit.subject || "(no message)")); list.append(item); } if (hidden > 0) list.append(el("li", "day-private", plural(hidden, "private commit")));}
function hideDay() { const panel = $("day-panel"); const hadFocus = panel.contains(document.activeElement); const day = state.openDay; const returnTo = state.dayReturn; state.openDay = null; state.dayReturn = null; panel.hidden = true; $("day-list").replaceChildren(); for (const cell of document.querySelectorAll("#matrix .matrix-cell.selected")) { cell.classList.remove("selected"); } if (!hadFocus) return; if (returnTo === "best") { const link = document.querySelector("#matrix-stats .stat-link"); if (link) { link.focus(); return; } } if (day) focusMatrixCell(day);}
function closeDay() { if (state.openDay === null) return; try { window.history.pushState(null, "", dayHash(null)); } catch { // A file:// mockup may refuse; closing still works. } hideDay();}
function projectMonogram(name) { const letters = (name || "?").replace(/[^a-z0-9]/gi, ""); return (letters.slice(0, 2) || "?").toUpperCase();}
function repoSource(repoKey) { return repoKey && repoKey.startsWith("github:") ? "github" : "tangled";}
// Known project homes. A repo record can carry a website; these fill in the// projects that predate that field. Everything else falls back to a domain// shaped repo/owner name, then to the owner's GitHub avatar.const KNOWN_SITES = { atgc: "https://atgc.codes", "atgc-stack-lab": "https://atgc.codes", substandard: "https://substandard.blog", didbot: "https://did.bot", "didbot-claude": "https://did.bot", "terraform-aws-didbot-pds": "https://did.bot", "vibescrobble.com": "https://vibescrobble.com", blogposts: "https://permadeath.com", "cat-photos": "https://permadeath.com",};
const KNOWN_FAVICONS = { atgc: "https://tangled.org/permadeath.com/atgc/raw/main/brand/png/icon-128.png", "atgc-stack-lab": "https://tangled.org/permadeath.com/atgc/raw/main/brand/png/icon-128.png", substandard: "https://substandard.blog/favicon.svg", didbot: "https://did.bot/favicon.svg", "didbot-claude": "https://did.bot/favicon.svg", "terraform-aws-didbot-pds": "https://did.bot/favicon.svg", "vibescrobble.com": "https://vibescrobble.com/favicon-32.png", blogposts: "https://permadeath.com/icon", "cat-photos": "https://permadeath.com/icon", "dev.permadeath.com": "https://permadeath.com/icon",};
function faviconFor(project) { const explicit = KNOWN_FAVICONS[project.name || ""]; if (explicit) return explicit; const known = KNOWN_SITES[project.name || ""]; const site = (known || project.website || "").trim(); let origin = ""; if (site) { try { origin = new URL(site).origin; } catch { origin = ""; } } const name = (project.name || "").trim(); if (!origin && name.includes(".")) origin = `https://${name}`; const owner = (project.full_name || "").split("/")[0]; if (!origin && owner.includes(".")) origin = `https://${owner}`; if (origin) return `${origin}/favicon.ico`; if ((project.repo_key || "").startsWith("github:") && owner) { return `https://github.com/${owner}.png?size=64`; } return "";}
function projectIcon(project) { const redacted = project.redacted || (project.repo_key || "").startsWith("github:redacted:"); const fallback = redacted ? "//" : projectMonogram(project.name); const icon = el("span", "project-icon"); const url = redacted ? "" : faviconFor(project); if (url) { const img = el("img", "project-favicon"); img.src = url; img.alt = ""; img.loading = "lazy"; img.decoding = "async"; img.addEventListener("error", () => { img.remove(); icon.classList.add("fallback"); icon.textContent = fallback; }); icon.append(img); } else { icon.classList.add("fallback"); icon.textContent = fallback; } icon.title = redacted ? "redacted repository" : project.name || ""; return icon;}
function hostOf(url) { try { return /^https?:\/\//.test(url || "") ? new URL(url).host : ""; } catch { return ""; }}
function redactedProject(project) { return !project || project.redacted || (project.repo_key || "").startsWith("github:redacted:");}
// The project favicon, linking to the project's live site when the build// found one.function projectMark(project, small = false) { const icon = projectIcon(project); if (small) icon.classList.add("small"); const site = redactedProject(project) ? "" : project.site || ""; const host = site.startsWith("https://") ? hostOf(site) : ""; if (!host) return icon; const link = el("a", "project-site"); link.href = site; link.target = "_blank"; link.rel = "noopener"; link.title = host; link.setAttribute("aria-label", host); icon.title = ""; link.append(icon); return link;}
// The Tangled or GitHub logo in the page's red, linking to the repository.// `reserve` keeps an empty slot when there is none, so columns line up.function sourceLink(project, small = false, reserve = false) { const host = redactedProject(project) ? "" : hostOf(project.url); if (!host) { if (!reserve) return null; const empty = el("span", "source-link empty"); empty.setAttribute("aria-hidden", "true"); return empty; } const forge = host === "github.com" ? "GitHub" : "Tangled"; const link = el("a", `source-link source-${forge.toLowerCase()}${small ? " small" : ""}`); link.href = project.url; link.title = `${project.full_name || project.name} on ${forge}`; link.setAttribute("aria-label", link.title); return link;}
// Both links for a line of text that has no favicon of its own: the site// (as the favicon) when there is one, then the repository.function projectLinks(project) { const links = el("span", "project-links"); if (!redactedProject(project) && project.site) links.append(projectMark(project, true)); const source = sourceLink(project, true); if (source) links.append(source); return links.childElementCount ? links : null;}
// The site lists the busiest projects of every period; this ranks them// for the page's period and keeps as many as the site ranks per period.const PROJECTS_SHOWN = 5;function renderProjects() { const list = $("project-ranking"); list.replaceChildren(); const label = PERIODS[state.period].label; const count = (project) => (project.commits && project.commits[state.period]) || 0; const projects = (state.data.projects || []) .filter((project) => count(project) > 0) .sort((a, b) => count(b) - count(a) || a.repo_key.localeCompare(b.repo_key)) .slice(0, PROJECTS_SHOWN); if (projects.length === 0) { list.append(el("li", "sub", `No project activity in ${label}.`)); return; } // One scale for every project, so the strips compare volume as well as // rhythm. const weeklyMax = Math.max(1, ...projects.flatMap((project) => project.weekly || [])); projects.forEach((project, index) => { const item = el("li", "project-row");
const rank = el("span", "project-rank", String(index + 1).padStart(2, "0")); const icon = el("span", "project-icons"); icon.append(projectMark(project)); const source = sourceLink(project, false, true); if (source) icon.append(source);
const body = el("div", "project-body"); const name = el("span", "project-name"); name.append(projectLabel(project.full_name || project.name)); name.title = project.full_name || project.name; const meta = el( "span", "project-meta", project.redacted ? "redacted repo" : project.knot || "source", ); body.append(name, meta);
const commits = count(project); const total = el("span", "project-count", fmt(commits)); total.title = `${number.format(commits)} commits, ${label}`;
// Momentum compares the last 30 days with the 90 before, so it only // shows beside a 30-day count. const last30 = project.commits["30d"]; if (state.period === "30d" && Number.isFinite(project.prior_90)) { const priorRate = project.prior_90 / 3; const ratio = priorRate > 0 ? last30 / priorRate : last30 > 0 ? 2 : 1; const trend = ratio >= 1.25 ? "up" : ratio <= 0.8 ? "down" : "flat"; const symbol = trend === "up" ? "↑" : trend === "down" ? "↓" : "→"; const momentum = el("span", `project-momentum ${trend}`, symbol); momentum.title = `${number.format(last30)} in the last 30 days vs ` + `${number.format(project.prior_90)} in the prior 90 days ` + `(${priorRate.toFixed(1)} per 30 days)`; momentum.setAttribute("aria-label", momentum.title); total.append(momentum); }
item.append(rank, icon, body, total, projectSpark(project, weeklyMax)); list.append(item); });}
// A repo name that wraps only after "/" or " · ", never inside a word; a// part too long for the column ends in an ellipsis.function projectLabel(text) { const label = el("span", "project-label"); text.split(/(?<=\/)|(?<= · )/).forEach((part, index) => { if (index > 0) label.append(document.createElement("wbr")); label.append(el("span", "project-label-part", part)); }); return label;}
// Twelve weekly bars under a project, oldest first; the last ends now.function projectSpark(project, max) { const weekly = project.weekly || []; const ns = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(ns, "svg"); svg.setAttribute("class", "project-spark"); svg.setAttribute("viewBox", "0 0 120 22"); svg.setAttribute("preserveAspectRatio", "none"); svg.setAttribute("role", "img"); svg.setAttribute( "aria-label", `${project.full_name || project.name}: ${weekly.join(", ")} commits a week over 12 weeks`, ); const band = 120 / Math.max(1, weekly.length); weekly.forEach((value, week) => { const rect = document.createElementNS(ns, "rect"); const h = value > 0 ? Math.max(1.5, (value / max) * 22) : 0.75; rect.setAttribute("x", String(week * band + band * 0.15)); rect.setAttribute("y", String(22 - h)); rect.setAttribute("width", String(band * 0.7)); rect.setAttribute("height", String(h)); rect.setAttribute("class", value > 0 ? "project-spark-bar" : "project-spark-empty"); const title = document.createElementNS(ns, "title"); const weeksAgo = weekly.length - 1 - week; title.textContent = `${weeksAgo === 0 ? "last 7 days" : `${weeksAgo + 1} weeks ago`}: ${number.format(value)}`; rect.append(title); svg.append(rect); }); return svg;}
const PERIOD_LABELS = { "30d": "30d", "90d": "90d", "1y": "1y", all: "all",};
function renderShare(summary) { const periods = summary.share_periods || {}; const period = periods[state.period] || periods.all; if (!period) { $("share-value").textContent = "—"; return; } const label = PERIOD_LABELS[state.period] || state.period; $("share-label").textContent = `tangled · ${label}`;
if (!summary.share_available || period.share_all > 100) { $("share-value").textContent = "—"; $("share-fill").style.width = "0%"; $("share-meter").hidden = true; $("share-meter").setAttribute("aria-label", "Global Tangled total unavailable"); $("share-sub").textContent = summary.global_partial ? `partial · ${pct(summary.coverage_pct)} reached` : summary.global_summary_stale ? `stale · ${dateTime(summary.global_summary_at)}` : "no global total"; return; }
$("share-meter").hidden = false; $("share-value").textContent = pct(period.share_all); $("share-fill").style.width = `${Math.min(100, Math.max(0, period.share_all))}%`; $("share-meter").setAttribute( "aria-label", `${pct(period.share_all)} of Tangled commits over ${label}`, ); $("share-sub").textContent = `${fmt(period.me_tangled)} + ${fmt(period.bots_tangled)} / ${fmt(period.tangled_total)}` + ` · as of ${summary.global_summary_at ? dateTime(summary.global_summary_at) : "last run"}` + (summary.global_partial ? ` · ${pct(summary.coverage_pct)} reached` : "");}
function renderShareTrend(summary) { const wrap = $("share-trend"); const svg = $("share-spark"); svg.replaceChildren(); // The trailing 30-day share, one point a day across the page's period. const points = summary.share_available ? (summary.share_trend || []).slice(-periodDays()) : []; if (points.length < 2) { wrap.hidden = true; return; } wrap.hidden = false; wrap.querySelector(".share-trend-label").textContent = `trailing 30d, daily · since ${dayLabel(points[0][0])}`; const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; }; const W = 400; const H = 40; const max = Math.max(...points.map((point) => point[1])) * 1.1 || 1; const x = (index) => (index / (points.length - 1)) * W; // Zero sits on the baseline, so the line's height reads as the share. const y = (value) => H - 1 - (value / max) * (H - 4); svg.append(make("line", { x1: 0, x2: W, y1: H - 0.5, y2: H - 0.5, class: "chart-axis" })); const d = points.map((point, index) => `${index ? "L" : "M"} ${x(index).toFixed(1)} ${y(point[1]).toFixed(1)}`); svg.append(make("path", { d: d.join(" "), class: "share-trend-line" })); const cursor = make("line", { x1: 0, x2: 0, y1: 0, y2: H, class: "share-trend-cursor", visibility: "hidden" }); svg.append(cursor); const hit = make("rect", { x: 0, y: 0, width: W, height: H, class: "chart-hit" }); const tooltip = $("share-tooltip"); hit.addEventListener("pointermove", (event) => { const box = svg.getBoundingClientRect(); const index = Math.round(((event.clientX - box.left) / box.width) * (points.length - 1)); const point = points[Math.max(0, Math.min(points.length - 1, index))]; cursor.setAttribute("x1", x(points.indexOf(point))); cursor.setAttribute("x2", x(points.indexOf(point))); cursor.setAttribute("visibility", "visible"); tooltip.replaceChildren(el("div", "tip-date", dayLabel(point[0]))); const row = el("div", "tip-row"); row.append(el("span", "tip-name", "trailing 30d"), el("span", "tip-value", pct(point[1]))); tooltip.append(row); tooltip.hidden = false; const wrapBox = wrap.getBoundingClientRect(); const width = tooltip.offsetWidth || 160; let left = event.clientX - wrapBox.left + 12; if (left + width > wrapBox.width) left = Math.max(0, left - width - 24); tooltip.style.left = `${left}px`; tooltip.style.top = `${H + 6}px`; }); hit.addEventListener("pointerleave", () => { tooltip.hidden = true; cursor.setAttribute("visibility", "hidden"); }); svg.append(hit);}
function renderKpis(summary) { statRow($("kpis"), [ { label: "all time", value: fmt(summary.me_total) }, { label: "tangled", value: fmt(summary.me_tangled) }, { label: "github", value: fmt(summary.me_github) }, { label: "agents", value: fmt(summary.bots_tangled) }, // The period's own total, beside the all-time one it would repeat at // "all". ...(state.period === "all" ? [] : [ { label: PERIODS[state.period].label, value: fmt( { "30d": summary.windows.me_30, "90d": summary.windows.me_90, "1y": summary.windows.me_365, }[state.period] || 0, ), }, ]), ]);}
function renderLegend() { const list = $("legend"); list.replaceChildren(); for (const series of legendSeries()) { const item = el("li"); const swatch = el("span", "swatch"); swatch.style.background = seriesColor(series.color); item.append(swatch, el("span", "", series.label)); const links = series.project && projectLinks(series.project); if (links) item.append(links); list.append(item); } if (!state.data.burn) return; const button = el("button", "burn-toggle"); button.type = "button"; button.setAttribute("aria-pressed", String(state.burn)); button.append(el("span", "burn-key"), el("span", "", "burn (scaled)")); button.addEventListener("click", () => { state.burn = !state.burn; renderLegend(); renderChart(); renderTable(); renderWheel(); }); const item = el("li"); item.append(button); list.append(item);}
/* ---------- token burn pulse ---------- */
// The agents' token burn, drawn over the date chart and the wheel as one// jagged trace scaled to its own peak. Tokens are not commits, so it has no// axis; the tooltips and tables carry its values.function burnShown() { return Boolean(state.data.burn) && state.burn;}
// A UTC day's burn, or null before the first day any agent reported usage.function burnOn(date) { const burn = state.data.burn; if (!burn || date < burn.days[0][0]) return null; const day = burn.days.find((row) => row[0] === date); if (!day) return 0; return day[2].reduce( (sum, [, ...tokens]) => sum + tokens.reduce((s, n, i) => s + n * burn.weights[i], 0), 0, );}
// Burn per hour of the day in the wheel's zone, over the selected period.function hourlyBurn(zone) { const bins = Array(24).fill(0); const hourOf = hourOfDay(zone); const start = periodStart(); for (const [hour, burn] of (state.data.burn && state.data.burn.hours) || []) { if (hour * 3600000 >= start) bins[hourOf(hour * 3600000)] += burn; } return bins;}
// The trace through `points`, on a casing of the page colour so it reads// over bars of any colour.function burnTrace(make, points, close) { const d = points.map(([x, y], i) => `${i ? "L" : "M"} ${x.toFixed(1)} ${y.toFixed(1)}`).join(" ") + (close ? " Z" : ""); return [make("path", { d, class: "burn-casing" }), make("path", { d, class: "burn-pulse" })];}
function burnText(value) { return value === null ? "–" : fmt(Math.round(value));}
function niceMax(value) { if (value <= 5) return 5; const exponent = Math.floor(Math.log10(value)); const fraction = value / 10 ** exponent; const step = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10; return step * 10 ** exponent;}
function renderChart() { const svg = $("chart"); svg.replaceChildren(); const points = visiblePoints(); const series = seriesList(); if (points.length === 0) { const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); text.setAttribute("x", "300"); text.setAttribute("y", "250"); text.setAttribute("text-anchor", "middle"); text.style.fill = "var(--muted)"; text.textContent = "No commits in this range yet."; svg.append(text); return; }
const W = 600; const H = 320; const margin = { left: 44, right: 10, top: 14, bottom: 30 }; const plotW = W - margin.left - margin.right; const plotH = H - margin.top - margin.bottom; const totals = points.map((point) => series.reduce((sum, item) => sum + valueFor(point, item), 0), ); const max = niceMax(Math.max(1, ...totals)); const y = (value) => margin.top + plotH - (value / max) * plotH;
const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) { node.setAttribute(key, String(value)); } return node; };
// Recessive, solid hairline grid. for (let i = 0; i <= 4; i += 1) { const value = (max * i) / 4; const gy = y(value); svg.append( make("line", { x1: margin.left, x2: W - margin.right, y1: gy, y2: gy, class: "chart-grid", }), ); const label = make("text", { x: margin.left - 8, y: gy + 4, "text-anchor": "end", class: "chart-tick", }); label.textContent = fmt(Math.round(value)); svg.append(label); }
const band = plotW / points.length; // On a one-year view the band is barely two pixels wide. Never let the bar // exceed its band; a hairline with air around it is more honest than two // columns sharing a pixel. const barW = Math.min(18, Math.max(1, band * 0.62)); const xLabelStep = Math.max(1, Math.ceil(points.length / 4));
points.forEach((point, index) => { const x = margin.left + index * band + (band - barW) / 2;
// Build bottom-up so the top of the stack can take the rounded cap. const segments = []; let cursor = margin.top + plotH; for (const item of series) { const value = valueFor(point, item); if (value <= 0) continue; const height = (value / max) * plotH; segments.push({ item, value, y: cursor - height, height }); cursor -= height; }
segments.forEach((segment, segmentIndex) => { const isBottom = segmentIndex === 0; const isTop = segmentIndex === segments.length - 1; const drawY = isBottom ? segment.y : segment.y + 2; const drawH = Math.max(0, segment.height - (isBottom ? 0 : 2)); if (drawH <= 0) return; const fill = seriesColor(segment.item.color); let node; if (isTop && drawH > 4) { const radius = 4; node = make("path", { d: roundedTopPath(x, drawY, barW, drawH, radius), }); } else { node = make("rect", { x, y: drawY, width: barW, height: drawH, rx: isTop ? 2 : 0, }); } node.style.fill = fill; svg.append(node); });
if (index % xLabelStep === 0 || index === points.length - 1) { const label = make("text", { x: margin.left + index * band + band / 2, y: H - 9, "text-anchor": "middle", class: "chart-tick", }); label.textContent = dayLabel(point.date); svg.append(label); }
const hit = make("rect", { x: margin.left + index * band, y: margin.top, width: band, height: plotH, class: "chart-hit", tabindex: "0", role: "button", "aria-label": `${point.date}: ${totals[index]} commits`, }); const show = (event) => showTooltip(event, point, series, totals[index], band, index, points.length); hit.addEventListener("pointermove", show); hit.addEventListener("pointerenter", show); hit.addEventListener("focus", show); hit.addEventListener("pointerleave", hideTooltip); hit.addEventListener("blur", hideTooltip); svg.append(hit); });
const burns = points.map((point) => burnOn(point.date)); const burnPeak = Math.max(0, ...burns); if (burnShown() && burnPeak > 0) { const trace = burns.flatMap((value, index) => value === null ? [] : [[margin.left + (index + 0.5) * band, margin.top + plotH * (1 - (0.9 * value) / burnPeak)]], ); svg.append(...burnTrace(make, trace, false)); }
// Baseline is the only axis rule that needs ink. svg.append( make("line", { x1: margin.left, x2: W - margin.right, y1: margin.top + plotH, y2: margin.top + plotH, class: "chart-axis", }), );}
function roundedTopPath(x, y, width, height, radius) { const r = Math.min(radius, width / 2, height); return [ `M ${x} ${y + height}`, `L ${x} ${y + r}`, `Q ${x} ${y} ${x + r} ${y}`, `L ${x + width - r} ${y}`, `Q ${x + width} ${y} ${x + width} ${y + r}`, `L ${x + width} ${y + height}`, "Z", ].join(" ");}
function showTooltip(event, point, series, total, band, index, count) { const tooltip = $("tooltip"); tooltip.replaceChildren(); tooltip.append(el("div", "tip-date", dayLabel(point.date))); tooltipSeries(tooltip, series, (item) => valueFor(point, item)); tooltipRow(tooltip, "total", number.format(total)); const burn = burnOn(point.date); if (burnShown() && burn !== null) tooltipRow(tooltip, "burn", burnText(burn), "#ff0000");
tooltip.hidden = false; const wrap = tooltip.parentElement.getBoundingClientRect(); let left; if (event && event.clientX !== undefined) { left = event.clientX - wrap.left + 12; } else { left = (index + 0.5) * (wrap.width / count) + 12; } const width = tooltip.offsetWidth || 180; if (left + width > wrap.width - 6) left = Math.max(6, left - width - 24); tooltip.style.left = `${left}px`; tooltip.style.top = "12px";}
function hideTooltip() { $("tooltip").hidden = true;}
function renderTable() { const wrap = $("table-view"); const button = $("table-toggle"); button.setAttribute("aria-expanded", String(state.table)); button.textContent = state.table ? "Hide table" : "Table"; wrap.replaceChildren(); wrap.hidden = !state.table; if (!state.table) return;
const points = visiblePoints(); const series = seriesList(); const table = document.createElement("table"); const head = document.createElement("thead"); const headRow = document.createElement("tr"); headRow.append(el("th", "", "Date")); for (const item of series) headRow.append(el("th", "", item.label)); headRow.append(el("th", "", "Total")); if (burnShown()) headRow.append(el("th", "", "Burn")); head.append(headRow); table.append(head);
const body = document.createElement("tbody"); for (const point of points) { const row = document.createElement("tr"); row.append(el("td", "", point.date)); let total = 0; for (const item of series) { const value = valueFor(point, item); total += value; row.append(el("td", "", number.format(value))); } row.append(el("td", "", number.format(total))); if (burnShown()) row.append(el("td", "", burnText(burnOn(point.date)))); body.append(row); } table.append(body); wrap.append(table); wrap.append(el("p", "table-scroll-note", "utc"));}
/* ---------- time-of-day wheel ---------- */
// The zone the wheel is drawn in: the site's, if it names a valid one,// otherwise the viewer's own.function wheelZone() { const viewer = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; const pinned = state.data.summary.timezone; if (pinned) { try { new Intl.DateTimeFormat("en-US", { timeZone: pinned }); return { zone: pinned, pinned: true }; } catch { // An unknown zone name falls back to the viewer's. } } return { zone: viewer, pinned: false };}
// Commits per hour of the day, per series, over the selected range.function wheelBins(series, zone) { const hourly = state.data.hourly || {}; const byProject = state.by !== "identity"; const rows = (byProject ? hourly.projects : hourly.rows) || []; const column = new Map((hourly.series || []).map((key, index) => [key, index + 1])); const columns = series.map((item) => (byProject ? item.index + 1 : column.get(item.key))); const bins = Array.from({ length: 24 }, () => series.map(() => 0)); const start = periodStart(); const hourOf = hourOfDay(zone); for (const row of rows) { const at = row[0] * 3600000; if (at < start) continue; const hour = hourOf(at); columns.forEach((col, index) => { if (col !== undefined) bins[hour][index] += row[col] || 0; }); } return bins;}
// The first UTC midnight of the selected period.function periodStart() { const generated = new Date(state.data.summary.generated_at); const today = Date.UTC(generated.getUTCFullYear(), generated.getUTCMonth(), generated.getUTCDate()); return today - (periodDays() - 1) * 86400000;}
// A function from epoch milliseconds to the hour of the day in `zone`.function hourOfDay(zone) { const format = new Intl.DateTimeFormat("en-US", { timeZone: zone, hour: "numeric", hourCycle: "h23" }); return (at) => Number(format.formatToParts(new Date(at)).find((part) => part.type === "hour").value) % 24;}
function hourRange(hour) { const pad = (value) => String(value).padStart(2, "0"); return `${pad(hour)}:00–${pad((hour + 1) % 24)}:00`;}
function renderWheel() { const svg = $("wheel"); svg.replaceChildren(); const series = seriesList(); const { zone, pinned } = wheelZone(); const bins = wheelBins(series, zone); const totals = bins.map((counts) => counts.reduce((sum, value) => sum + value, 0)); const total = totals.reduce((sum, value) => sum + value, 0); $("wheel-sub").textContent = pinned ? zone : `your time · ${zone}`;
const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; }; const C = 220; const R0 = 72; const R = 186; // The busiest hour reaches the edge; rings mark round counts inside it. const max = Math.max(1, ...totals); const step = niceMax(max) / 4; const rings = [1, 2, 3, 4].map((k) => k * step).filter((value) => value <= max).slice(-2); // Area, not radius, follows the count: a wedge twice as far out would // otherwise read as four times the work. const radius = (value) => Math.sqrt(R0 * R0 + ((R * R - R0 * R0) * value) / max); const angle = (hour) => (hour / 24) * 2 * Math.PI - Math.PI / 2; const point = (r, a) => `${(C + r * Math.cos(a)).toFixed(2)} ${(C + r * Math.sin(a)).toFixed(2)}`; // An annular sector with a 1px inset on each side, so neighbours keep a // parallel 2px gap at every radius. const sector = (inner, outer, a1, a2) => { const outerInset = 1 / outer; const innerInset = 1 / inner; return [ `M ${point(outer, a1 + outerInset)}`, `A ${outer} ${outer} 0 0 1 ${point(outer, a2 - outerInset)}`, `L ${point(inner, a2 - innerInset)}`, `A ${inner} ${inner} 0 0 0 ${point(inner, a1 + innerInset)}`, "Z", ].join(" "); };
// Recessive rings, labelled once just inside the top of each. for (const value of rings) { const r = radius(value); svg.append(make("circle", { cx: C, cy: C, r, class: "chart-grid wheel-ring" })); const label = make("text", { x: C + 4, y: C - r + 11, class: "chart-tick wheel-ring-label" }); label.textContent = fmt(Math.round(value)); svg.append(label); } svg.append(make("circle", { cx: C, cy: C, r: R0, class: "chart-axis wheel-ring" }));
bins.forEach((counts, hour) => { const a1 = angle(hour); const a2 = angle(hour + 1); let cumulative = 0; const drawn = []; series.forEach((item, index) => { const value = counts[index]; if (value <= 0) return; const inner = radius(cumulative) + (drawn.length > 0 ? 2 : 0); cumulative += value; const outer = radius(cumulative); if (outer - inner < 0.5) return; const path = make("path", { d: sector(inner, outer, a1, a2) }); path.style.fill = seriesColor(item.color); drawn.push(path); }); svg.append(...drawn); });
const burn = hourlyBurn(zone); const burnPeak = Math.max(...burn); if (burnShown() && burnPeak > 0) { const trace = burn.map((value, hour) => { const r = R0 + ((R - R0) * value) / burnPeak; return [C + r * Math.cos(angle(hour + 0.5)), C + r * Math.sin(angle(hour + 0.5))]; }); svg.append(...burnTrace(make, trace, true)); }
// Hour labels every three hours, outside the ring; midnight at the top. for (let hour = 0; hour < 24; hour += 3) { const a = angle(hour); const label = make("text", { x: (C + (R + 16) * Math.cos(a)).toFixed(2), y: (C + (R + 16) * Math.sin(a) + 3.5).toFixed(2), "text-anchor": "middle", class: "chart-tick", }); label.textContent = String(hour).padStart(2, "0"); svg.append(label); }
const centre = make("text", { x: C, y: C + 4, "text-anchor": "middle", class: "wheel-total" }); centre.textContent = fmt(total); const centreLabel = make("text", { x: C, y: C + 22, "text-anchor": "middle", class: "chart-tick" }); centreLabel.textContent = "commits"; svg.append(centre, centreLabel);
// Hit targets cover the whole hour, inner ring to outer edge. bins.forEach((counts, hour) => { const hit = make("path", { d: sector(R0, R + 6, angle(hour), angle(hour + 1)), class: "chart-hit", tabindex: "0", role: "button", "aria-label": `${hourRange(hour)}: ${totals[hour]} commits`, }); const show = (event) => showWheelTooltip(event, hour, counts, series, totals[hour], total, burn[hour]); hit.addEventListener("pointermove", show); hit.addEventListener("pointerenter", show); hit.addEventListener("focus", show); hit.addEventListener("pointerleave", hideWheelTooltip); hit.addEventListener("blur", hideWheelTooltip); svg.append(hit); });
renderWheelStats(totals, total); renderWheelTable(bins, series, totals, burn);}
function showWheelTooltip(event, hour, counts, series, hourTotal, total, burn) { const tooltip = $("wheel-tooltip"); tooltip.replaceChildren(); tooltip.append(el("div", "tip-date", hourRange(hour))); tooltipSeries(tooltip, series, (item) => counts[series.indexOf(item)]); const share = total > 0 ? ` · ${pct((hourTotal / total) * 100)}` : ""; tooltipRow(tooltip, "total", `${number.format(hourTotal)}${share}`); if (burnShown()) tooltipRow(tooltip, "burn", burnText(burn), "#ff0000");
tooltip.hidden = false; const wrap = tooltip.parentElement.getBoundingClientRect(); const width = tooltip.offsetWidth || 180; const height = tooltip.offsetHeight || 80; let left; let top; if (event && event.clientX !== undefined) { left = event.clientX - wrap.left + 14; top = event.clientY - wrap.top + 14; } else { const a = (hour + 0.5) / 24 * 2 * Math.PI - Math.PI / 2; left = wrap.width / 2 + Math.cos(a) * wrap.width * 0.3; top = wrap.height / 2 + Math.sin(a) * wrap.height * 0.3; } if (left + width > wrap.width - 6) left = Math.max(6, left - width - 28); if (top + height > wrap.height - 6) top = Math.max(6, top - height - 28); tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`;}
function hideWheelTooltip() { $("wheel-tooltip").hidden = true;}
function renderWheelStats(totals, total) { const stats = $("wheel-stats"); stats.replaceChildren(); if (total === 0) return; const peak = totals.indexOf(Math.max(...totals)); // The busiest eight hours in a row, wrapping past midnight. let bestStart = 0; let best = -1; for (let start = 0; start < 24; start += 1) { let sum = 0; for (let offset = 0; offset < 8; offset += 1) sum += totals[(start + offset) % 24]; if (sum > best) { best = sum; bestStart = start; } } const night = totals.slice(0, 6).reduce((sum, value) => sum + value, 0); statRow(stats, [ { label: "peak", value: hourRange(peak), meta: fmt(totals[peak]), title: `${fmt(totals[peak])} commits in the peak hour`, }, { label: "busiest 8h", value: `${hourRange(bestStart).slice(0, 5)}–${hourRange((bestStart + 7) % 24).slice(-5)}`, meta: pct((best / total) * 100), title: `${pct((best / total) * 100)} of commits`, }, { label: "00–06", value: pct((night / total) * 100), meta: fmt(night), title: `${fmt(night)} commits`, }, ]);}
function renderWheelTable(bins, series, totals, burn) { // The shared Table button is labelled by renderTable. const wrap = $("wheel-table"); wrap.replaceChildren(); wrap.hidden = !state.table; if (!state.table) return; const table = document.createElement("table"); const head = document.createElement("thead"); const headRow = document.createElement("tr"); headRow.append(el("th", "", "Hour")); for (const item of series) headRow.append(el("th", "", item.label)); headRow.append(el("th", "", "Total")); if (burnShown()) headRow.append(el("th", "", "Burn")); head.append(headRow); const body = document.createElement("tbody"); bins.forEach((counts, hour) => { const row = document.createElement("tr"); row.append(el("td", "", hourRange(hour))); for (const value of counts) row.append(el("td", "", number.format(value))); row.append(el("td", "", number.format(totals[hour]))); if (burnShown()) row.append(el("td", "", burnText(burn[hour]))); body.append(row); }); table.append(head, body); wrap.append(table);}
/* ---------- commit types ---------- */
// Each type keeps its colour whatever its rank, in a validated adjacent// order from the bottom of the stack up; anything else folds into "other".const TYPE_COLORS = [ ["feat", "series-1"], ["fix", "series-2"], ["docs", "series-3"], ["test", "series-4"], ["refactor", "series-7"], ["other", "muted"],];
// The page's period in bins: a day each over 30 days (weekly would leave// four points), a week each over longer periods. A year or more starts at// the first week with enough typed commits for a share to mean something// (falling back to the first with any).function typesView(data) { const days = periodDays(); const daily = days <= 31 && (data.days || []).length > 0; let labels; let from; let counts; if (daily) { from = Math.max(0, data.days.length - days); labels = data.days; counts = (bins) => bins.daily; } else { labels = data.weeks; counts = (bins) => bins.weekly; const generated = new Date(state.data.summary.generated_at); const start = Date.UTC(generated.getUTCFullYear(), generated.getUTCMonth(), generated.getUTCDate()) - (days - 1) * 86400000; // The first week that ends inside the period. from = Math.max(0, labels.findIndex((week) => dayDate(week).getTime() + 6 * 86400000 >= start)); if (days >= 365) { const total = (bin) => data.types.reduce((sum, row) => sum + row.weekly[bin], 0); let first = labels.findIndex((_, bin) => bin >= from && total(bin) >= 20); if (first < 0) first = labels.findIndex((_, bin) => bin >= from && total(bin) > 0); from = Math.max(from, first); } } const bins = labels.slice(from); const sum = (values) => values.reduce((total, value) => total + value, 0); const named = new Set(TYPE_COLORS.map(([name]) => name)); const folded = data.types.filter((row) => !named.has(row.name) || row.name === "other"); const types = TYPE_COLORS.map(([name, color]) => { const rows = name === "other" ? folded : data.types.filter((row) => row.name === name); const series = bins.map((_, bin) => rows.reduce((total, row) => total + counts(row)[from + bin], 0)); return { name, color, members: name === "other" ? folded.map((row) => row.name).filter((n) => n !== "other") : [], total: sum(series), counts: series, }; }).filter((row) => row.total > 0); return { unit: daily ? "day" : "week", labels: bins, types, untyped: data.untyped ? sum(counts(data.untyped).slice(from)) : 0, noSubject: data.no_subject ? sum(counts(data.no_subject).slice(from)) : 0, };}
function renderCommitTypes() { const card = document.querySelector(".types-card"); const data = state.data.commit_types; const svg = $("types"); svg.replaceChildren(); if (!data || data.types.length === 0) { card.hidden = true; return; } card.hidden = false; const view = typesView(data); const typed = view.types.reduce((sum, row) => sum + row.total, 0); $("types-sub").textContent = `conventional commits · ${seriesLabel()}, share of each ${view.unit}`;
const legend = $("types-legend"); legend.replaceChildren(); for (const row of view.types) { const item = el("li"); const swatch = el("span", "swatch"); swatch.style.background = seriesColor(row.color); const label = row.members.length > 0 ? `other (${row.members.join(", ")})` : row.name; item.append(swatch, el("span", "", `${label} · ${pct((row.total / typed) * 100)}`)); legend.append(item); }
const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; }; const W = 560; const H = 300; const margin = { left: 54, right: 14, top: 14, bottom: 30 }; const plotW = W - margin.left - margin.right; const plotH = H - margin.top - margin.bottom; const count = view.labels.length; const totals = view.labels.map((_, week) => view.types.reduce((sum, row) => sum + row.counts[week], 0)); // Each bin's mix, as shares of that bin's typed commits; volume is // already on the page elsewhere. const share = (row, week) => (totals[week] > 0 ? (row.counts[week] / totals[week]) * 100 : 0); // Days are columns, so a day with no typed commits stays empty; weeks // are a continuous area. const bars = view.unit === "day"; const binW = plotW / Math.max(1, count); const x = (week) => bars ? margin.left + (week + 0.5) * binW : margin.left + (count === 1 ? plotW / 2 : (week / (count - 1)) * plotW); const binAt = (svgX) => bars ? Math.floor((svgX - margin.left) / binW) : Math.round(((svgX - margin.left) / plotW) * (count - 1)); const y = (value) => margin.top + plotH - (value / 100) * plotH;
for (const value of [0, 25, 50, 75, 100]) { svg.append(make("line", { x1: margin.left, x2: W - margin.right, y1: y(value), y2: y(value), class: "chart-grid" })); const tick = make("text", { x: margin.left - 8, y: y(value) + 4, "text-anchor": "end", class: "chart-tick" }); tick.textContent = `${value}%`; svg.append(tick); }
// Bottom-up bands; each is the area between the running total before and // after it, outlined in the surface colour so neighbours keep a gap. let below = view.labels.map(() => 0); for (const row of view.types) { const above = below.map((value, week) => value + share(row, week)); if (bars) { const width = binW * 0.72; above.forEach((value, day) => { if (value <= below[day]) return; const rect = make("rect", { x: (x(day) - width / 2).toFixed(1), y: y(value).toFixed(1), width: width.toFixed(1), height: (y(below[day]) - y(value)).toFixed(1), class: "type-band", }); rect.style.fill = seriesColor(row.color); svg.append(rect); }); below = above; continue; } const top = above.map((value, week) => `${x(week).toFixed(1)} ${y(value).toFixed(1)}`); const bottom = below .map((value, week) => `${x(week).toFixed(1)} ${y(value).toFixed(1)}`) .reverse(); const band = make("path", { d: `M ${top.join(" L ")} L ${bottom.join(" L ")} Z`, class: "type-band" }); band.style.fill = seriesColor(row.color); svg.append(band); below = above; }
for (const week of [0, Math.floor((count - 1) / 2), count - 1]) { const anchor = week === 0 ? "start" : week === count - 1 ? "end" : "middle"; const label = make("text", { x: x(week), y: H - 9, "text-anchor": anchor, class: "chart-tick" }); label.textContent = dayLabel(view.labels[week]); svg.append(label); } svg.append(make("line", { x1: margin.left, x2: W - margin.right, y1: margin.top + plotH, y2: margin.top + plotH, class: "chart-axis", }));
const cursor = make("line", { y1: margin.top, y2: margin.top + plotH, class: "types-cursor", visibility: "hidden" }); svg.append(cursor); const hit = make("rect", { x: margin.left, y: margin.top, width: plotW, height: plotH, class: "chart-hit" }); const tooltip = $("types-tooltip"); hit.addEventListener("pointermove", (event) => { const box = svg.getBoundingClientRect(); const svgX = ((event.clientX - box.left) / box.width) * W; const week = Math.max(0, Math.min(count - 1, binAt(svgX))); cursor.setAttribute("x1", x(week)); cursor.setAttribute("x2", x(week)); cursor.setAttribute("visibility", "visible"); const when = dayLabel(view.labels[week]); tooltip.replaceChildren(el("div", "tip-date", view.unit === "day" ? when : `week of ${when}`)); for (const row of [...view.types].reverse()) { if (row.counts[week] <= 0) continue; const line = el("div", "tip-row"); const name = el("span", "tip-name"); const key = el("span", "tip-key"); key.style.background = seriesColor(row.color); name.append(key, el("span", "", row.name)); line.append(name, el("span", "tip-value", `${pct(share(row, week))} · ${number.format(row.counts[week])}`)); tooltip.append(line); } const total = el("div", "tip-row"); total.append(el("span", "tip-name", "typed commits"), el("span", "tip-value", number.format(totals[week]))); tooltip.append(total); tooltip.hidden = false; const wrap = tooltip.parentElement.getBoundingClientRect(); const width = tooltip.offsetWidth || 180; let left = event.clientX - wrap.left + 14; if (left + width > wrap.width - 6) left = Math.max(6, left - width - 28); tooltip.style.left = `${left}px`; tooltip.style.top = "12px"; }); hit.addEventListener("pointerleave", () => { tooltip.hidden = true; cursor.setAttribute("visibility", "hidden"); }); svg.append(hit);
const notes = []; if (view.untyped > 0) notes.push(`${fmt(view.untyped)} without a type`); if (view.noSubject > 0) notes.push(`${fmt(view.noSubject)} redacted, no subject`); $("types-note").textContent = notes.join(" · "); renderTypesTable(view);}
function renderTypesTable(data) { const wrap = $("types-table"); const button = $("types-table-toggle"); button.setAttribute("aria-expanded", String(state.typesTable)); button.textContent = state.typesTable ? "Hide table" : "Table"; wrap.replaceChildren(); wrap.hidden = !state.typesTable; if (!state.typesTable) return; const table = document.createElement("table"); const head = document.createElement("thead"); const headRow = document.createElement("tr"); headRow.append(el("th", "", data.unit === "day" ? "Day" : "Week of")); for (const row of data.types) headRow.append(el("th", "", row.name)); head.append(headRow); const body = document.createElement("tbody"); data.labels.forEach((binStart, bin) => { const tr = document.createElement("tr"); tr.append(el("td", "", binStart)); for (const row of data.types) tr.append(el("td", "", number.format(row.counts[bin]))); body.append(tr); }); table.append(head, body); wrap.append(table);}
/* ---------- project timelines ---------- */
const DAY_MS = 86400000;
function addDays(day, days) { return new Date(Date.parse(`${day}T00:00:00Z`) + days * DAY_MS).toISOString().slice(0, 10);}
// The lanes inside the page's period, by first active day, on one shared// day axis to the window's end. A short period's axis is the whole period;// a year (or "all", which is the same year) starts at the earliest lane's// first commit.function timelinesView(data, days) { const total = Math.round((Date.parse(`${data.end}T00:00:00Z`) - Date.parse(`${data.start}T00:00:00Z`)) / DAY_MS) + 1; const from = Math.max(0, total - days); const lanes = data.projects .map((project) => { const counts = new Map(); project.daily.forEach((value, index) => { const day = project.first + index; if (value > 0 && day >= from) counts.set(day, value); }); const active = [...counts.keys()]; return { project, counts, first: Math.min(...active), last: Math.max(...active), total: [...counts.values()].reduce((sum, value) => sum + value, 0), }; }) .filter((lane) => lane.total > 0) .sort((a, b) => a.first - b.first || b.total - a.total); const domainStart = days < 365 || !lanes.length ? from : Math.min(...lanes.map((lane) => lane.first)); return { lanes, start: data.start, domainStart, domainEnd: total - 1 };}
// Monday-to-Sunday week of a day offset, clipped to the domain.function timelineWeek(view, day) { const weekday = (new Date(`${addDays(view.start, day)}T00:00:00Z`).getUTCDay() + 6) % 7; const from = Math.max(view.domainStart, day - weekday); const to = Math.min(view.domainEnd, day - weekday + 6); return { from, to, monday: day - weekday };}
function weekCommits(lane, week) { let sum = 0; for (let day = week.from; day <= week.to; day += 1) sum += lane.counts.get(day) || 0; return sum;}
function busiestWeek(view, lane) { let best = null; for (let monday = timelineWeek(view, lane.first).monday; monday <= lane.last; monday += 7) { const week = timelineWeek(view, monday); const value = weekCommits(lane, week); if (!best || value > best.value) best = { week, value }; } return best;}
function renderTimelines() { const card = document.querySelector(".timelines-card"); const data = state.data.project_timelines; const wrap = $("timelines"); if (!data || !data.projects || data.projects.length === 0) { card.hidden = true; return; } card.hidden = false; const view = timelinesView(data, periodDays()); $("timelines-sub").textContent = `commits per day, smoothed, √ scale · busiest projects by first commit, ${seriesLabel()}`; wrap.replaceChildren(); $("timelines-tooltip").hidden = true; $("timelines-cursor").hidden = true; if (view.lanes.length === 0) { wrap.append(el("p", "sub", "No project activity in this period.")); renderTimelinesTable(view); return; }
const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; };
const rows = view.lanes.map((lane) => { const row = el("div", "timeline-lane"); row.setAttribute("role", "listitem"); row.tabIndex = 0; const label = el("div", "timeline-label"); const name = el("span", "timeline-name", lane.project.name); const owner = lane.project.redacted ? "redacted" : (lane.project.full_name || "").split("/")[0]; const meta = el("span", "timeline-meta", `${owner ? `${owner} · ` : ""}${fmt(lane.total)}`); const text = el("span", "timeline-text"); text.append(name, meta); label.title = lane.project.full_name || lane.project.name; label.append(projectMark(lane.project)); const source = sourceLink(lane.project, false, true); if (source) label.append(source); label.append(text); const svg = make("svg", { class: "timeline-plot", "aria-hidden": "true" }); row.append(label, svg); const peak = busiestWeek(view, lane); row.setAttribute( "aria-label", `${lane.project.full_name || lane.project.name}: ${number.format(lane.total)} commits, ` + `first ${dayLabel(addDays(view.start, lane.first))}, ` + `busiest week of ${dayLabel(addDays(view.start, peak.week.monday))} with ${number.format(peak.value)}, ` + `last ${dayLabel(addDays(view.start, lane.last))}`, ); wrap.append(row); return { row, svg, lane, peak }; }); const axisRow = el("div", "timeline-lane timeline-axis-row"); axisRow.setAttribute("aria-hidden", "true"); const axis = make("svg", { class: "timeline-plot timeline-axis" }); axisRow.append(el("div", "timeline-label"), axis); wrap.append(axisRow);
// Smoothing follows the drawn width: about seven pixels of days, so a // phone and a desktop both read as shapes. Over a year it never goes // under a day and a half; a shorter period, where a day is tens of // pixels wide, keeps it near a day so the √ tails don't smear a single // day's commits across a week. const width = Math.max(120, rows[0].svg.getBoundingClientRect().width || 600); const days = view.domainEnd - view.domainStart + 1; const x = (day) => ((day - view.domainStart) / days) * width; const dayAt = (px) => Math.max(view.domainStart, Math.min(view.domainEnd, view.domainStart + Math.floor((px / width) * days))); const sigma = Math.max(periodDays() < 365 ? 0.6 : 1.5, (7 * days) / width); // Where a day is wider than a few pixels, the curve is sampled within // the day too, so a short period draws curves rather than diamonds. const perDay = Math.max(1, Math.ceil(width / days / 3)); const samples = days * perDay; const reach = Math.ceil(3 * sigma * perDay); const kernel = []; for (let k = -reach; k <= reach; k += 1) { const offset = k / perDay; kernel.push(Math.exp(-(offset * offset) / (2 * sigma * sigma))); } const kernelSum = kernel.reduce((sum, value) => sum + value, 0) / perDay; const smooth = rows.map(({ lane }) => { const out = new Float64Array(samples); for (const [day, value] of lane.counts) { const centre = (day - view.domainStart) * perDay + Math.floor(perDay / 2); for (let k = -reach; k <= reach; k += 1) { const index = centre + k; if (index >= 0 && index < samples) out[index] += (value * kernel[k + reach]) / kernelSum; } } return out; }); // One square-root scale for every lane: volume still compares, and a // small project stays visible beside the busiest. const peakRate = Math.max(1e-9, ...smooth.map((series) => Math.max(...series)));
const months = []; for (let month = new Date(`${addDays(view.start, view.domainStart).slice(0, 7)}-01T00:00:00Z`); ; ) { const day = Math.round((month - Date.parse(`${view.start}T00:00:00Z`)) / DAY_MS); if (day > view.domainEnd) break; if (day > view.domainStart) months.push({ day, month }); month = new Date(Date.UTC(month.getUTCFullYear(), month.getUTCMonth() + 1, 1)); }
rows.forEach(({ svg, lane }, index) => { const height = svg.getBoundingClientRect().height || 34; svg.setAttribute("viewBox", `0 0 ${width} ${height}`); const mid = height / 2; const half = mid - 2; for (const { day } of months) { svg.append(make("line", { x1: x(day), x2: x(day), y1: 0, y2: height, class: "chart-grid" })); } svg.append(make("line", { x1: x(lane.first), x2: x(lane.last + 1), y1: mid, y2: mid, class: "timeline-span" })); const series = smooth[index]; const top = []; const bottom = []; for (let i = 0; i < samples; i += 1) { const h = half * Math.sqrt(series[i] / peakRate); if (h < 0.05 && top.length === 0) continue; const px = x(view.domainStart + (i + 0.5) / perDay).toFixed(1); top.push(`${px} ${(mid - h).toFixed(2)}`); bottom.push(`${px} ${(mid + h).toFixed(2)}`); } if (top.length > 1) { svg.append(make("path", { d: `M ${top.join(" L ")} L ${bottom.reverse().join(" L ")} Z`, class: "violin" })); } });
const axisHeight = axis.getBoundingClientRect().height || 22; axis.setAttribute("viewBox", `0 0 ${width} ${axisHeight}`); axis.append(make("line", { x1: 0, x2: width, y1: 0.5, y2: 0.5, class: "chart-axis" })); const labels = []; const place = (px, text, anchor) => { const w = text.length * 6.2; const left = anchor === "start" ? px : anchor === "end" ? px - w : px - w / 2; if (labels.some(([l, r]) => left < r + 8 && left + w > l - 8)) return; labels.push([left, left + w]); const label = make("text", { x: px, y: 15, "text-anchor": anchor, class: "chart-tick" }); label.textContent = text; axis.append(label); }; place(0, dayLabel(addDays(view.start, view.domainStart)), "start"); place(width, dayLabel(addDays(view.start, view.domainEnd)), "end"); for (const { day, month } of months) { axis.append(make("line", { x1: x(day), x2: x(day), y1: 0, y2: 4, class: "chart-axis" })); place(x(day), month.toLocaleDateString(undefined, { month: "short", timeZone: "UTC" }), "middle"); }
const tooltip = $("timelines-tooltip"); const cursor = $("timelines-cursor"); const show = (entry, day) => { const { row, svg, lane } = entry; const week = timelineWeek(view, day); const box = svg.getBoundingClientRect(); const outer = tooltip.parentElement.getBoundingClientRect(); tooltip.replaceChildren(el("div", "tip-date", lane.project.full_name || lane.project.name)); const line = el("div", "tip-row"); line.append( el("span", "tip-name", `week of ${dayLabel(addDays(view.start, week.monday))}`), el("span", "tip-value", number.format(weekCommits(lane, week))), ); const dayLine = el("div", "tip-row"); dayLine.append( el("span", "tip-name", dayLabel(addDays(view.start, day))), el("span", "tip-value", number.format(lane.counts.get(day) || 0)), ); tooltip.append(line, dayLine); tooltip.hidden = false; const scale = box.width / width; cursor.style.left = `${box.left - outer.left + x(week.from) * scale}px`; cursor.style.width = `${Math.max(2, (x(week.to + 1) - x(week.from)) * scale)}px`; cursor.style.top = `${row.getBoundingClientRect().top - outer.top}px`; cursor.style.height = `${row.getBoundingClientRect().height}px`; cursor.hidden = false; const tipWidth = tooltip.offsetWidth || 180; let left = box.left - outer.left + x(day + 0.5) * scale + 14; if (left + tipWidth > outer.width - 6) left = Math.max(6, left - tipWidth - 28); tooltip.style.left = `${left}px`; tooltip.style.top = `${Math.max(0, row.getBoundingClientRect().top - outer.top - tooltip.offsetHeight - 4)}px`; entry.day = day; }; const hide = () => { tooltip.hidden = true; cursor.hidden = true; }; rows.forEach((entry, index) => { const { row, svg } = entry; row.addEventListener("pointermove", (event) => { const box = svg.getBoundingClientRect(); if (event.clientX < box.left) return hide(); show(entry, dayAt(((event.clientX - box.left) / box.width) * width)); }); row.addEventListener("pointerleave", hide); row.addEventListener("focus", () => show(entry, Math.max(view.domainStart, entry.day ?? entry.peak.week.monday))); row.addEventListener("blur", hide); row.addEventListener("keydown", (event) => { const step = { ArrowLeft: -7, ArrowRight: 7 }[event.key]; if (step) { event.preventDefault(); const current = Math.max(view.domainStart, entry.day ?? entry.peak.week.monday); show(entry, Math.max(view.domainStart, Math.min(view.domainEnd, current + step))); } else if (event.key === "ArrowUp" || event.key === "ArrowDown") { const next = rows[index + (event.key === "ArrowUp" ? -1 : 1)]; if (next) { event.preventDefault(); next.day = entry.day; next.row.focus(); } } else if (event.key === "Home" || event.key === "End") { event.preventDefault(); show(entry, event.key === "Home" ? entry.lane.first : entry.lane.last); } }); }); renderTimelinesTable(view, rows);}
function renderTimelinesTable(view, rows = []) { const wrap = $("timelines-table"); const button = $("timelines-table-toggle"); button.setAttribute("aria-expanded", String(state.timelinesTable)); button.textContent = state.timelinesTable ? "Hide table" : "Table"; wrap.replaceChildren(); wrap.hidden = !state.timelinesTable; if (!state.timelinesTable) return; const table = document.createElement("table"); const head = document.createElement("thead"); const headRow = document.createElement("tr"); for (const name of ["Project", "First", "Busiest week", "Last", "Commits"]) headRow.append(el("th", "", name)); head.append(headRow); const body = document.createElement("tbody"); for (const { lane, peak } of rows) { const tr = document.createElement("tr"); tr.append( el("td", "", lane.project.full_name || lane.project.name), el("td", "", addDays(view.start, lane.first)), el("td", "", `${addDays(view.start, peak.week.monday)} · ${number.format(peak.value)}`), el("td", "", addDays(view.start, lane.last)), el("td", "", number.format(lane.total)), ); body.append(tr); } table.append(head, body); wrap.append(table);}
function renderRepos() { const wrap = $("repos"); wrap.replaceChildren(); const repos = state.data.repos || []; if (repos.length === 0) { wrap.append(el("p", "sub", "no commits")); return; } const max = Math.max(...repos.map((repo) => repo.mine), 1); for (const repo of repos) { const row = el("div", "repo-row"); const name = el("span", "repo-name"); name.append(projectMark(repo)); const source = sourceLink(repo, false, true); if (source) name.append(source); name.append(el("span", "repo-label", repo.full_name || repo.name)); name.title = repo.full_name || repo.name; row.append(name, el("span", "repo-count", fmt(repo.mine))); row.append( el( "span", "repo-meta", repo.knot ? `${fmt(repo.commits)} commits · ${repo.knot}` : `${fmt(repo.commits)} commits`, ), ); const track = el("div", "repo-bar-track"); const bar = el("div", "repo-bar"); bar.style.width = `${Math.max(2, (repo.mine / max) * 100)}%`; track.append(bar); row.append(track); wrap.append(row); }}
function renderRecent() { const list = $("recent"); list.replaceChildren(); const recent = state.data.recent || []; if (recent.length === 0) { list.append(el("li", "", "no commits")); return; } const limit = 18; for (const commit of recent.slice(0, limit)) { const item = el("li"); const dot = el("span", "dot"); dot.style.background = seriesColor(commit.color); const body = el("div"); const message = commit.url ? el("a", "msg", commit.message || "(no message)") : el("p", "msg", commit.message || "(no message)"); if (commit.url) message.href = commit.url; body.append(message); const meta = el("p", "meta"); const sourceLabel = repoSource(commit.repo_key) === "github" ? "github" : "tangled"; meta.textContent = `${sourceLabel} · ${commit.author} · ${commit.repo} · ${commit.short}`; body.append(meta); item.append(dot, body, el("span", "when", shortDate(commit.authored_at))); list.append(item); }}
/* ---------- written to landed ---------- */
function duration(secs) { if (secs < 60) return `${Math.round(secs)}s`; if (secs < 3600) return `${Math.round(secs / 60)}m`; if (secs < 86400) return `${(secs / 3600).toFixed(secs < 36000 ? 1 : 0)}h`; return `${(secs / 86400).toFixed(secs < 864000 ? 1 : 0)}d`;}
function renderLandGap() { const card = document.querySelector(".gap-card"); const data = state.data.land_gap; const svg = $("gap"); svg.replaceChildren(); if (!data || (data.gaps || []).length === 0) { card.hidden = true; return; } card.hidden = false; // Only the changes written inside the page's period, by author date. // `keep` maps each shown change to its place in the full list, which // also fixes its scatter so dots stay put as the period changes. const start = new Date(`${data.start || "1970-01-01"}T00:00:00Z`); const generated = new Date(state.data.summary.generated_at); const today = Math.floor((generated - start) / 86400000); const allDays = data.authored_days || data.gaps.map(() => 0); const firstAllowed = today - periodDays() + 1; const keep = []; allDays.forEach((day, index) => { if (day >= firstAllowed) keep.push(index); }); const gaps = keep.map((index) => data.gaps[index]); const days = keep.map((index) => allDays[index]); $("gap-sub").textContent = `author date → commit date, log scale · tangled changes by the day written, ${seriesLabel()}`; $("gap-stats").replaceChildren(); if (gaps.length === 0) { svg.setAttribute("viewBox", "0 0 560 360"); const empty = document.createElementNS("http://www.w3.org/2000/svg", "text"); empty.setAttribute("x", "280"); empty.setAttribute("y", "180"); empty.setAttribute("text-anchor", "middle"); empty.setAttribute("class", "chart-tick"); empty.textContent = "No changes written in this period."; svg.append(empty); return; } const quantile = (q) => gaps[Math.round((gaps.length - 1) * q)]; const medianSecs = quantile(0.5); const p90Secs = quantile(0.9); const p99Secs = quantile(0.99); // The site names the slowest 1% of each period; take this one's. const position = new Map(keep.map((index, shown) => [index, shown])); const slowList = (data.slowest || []) .filter((change) => position.has(change.index)) .sort((a, b) => b.index - a.index) .slice(0, Math.max(1, Math.floor(gaps.length / 100))) .map((change) => ({ ...change, shown: position.get(change.index) })); const ns = "http://www.w3.org/2000/svg"; const make = (tag, attrs = {}) => { const node = document.createElementNS(ns, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); return node; }; const W = 560; const H = 360; const margin = { left: 52, right: 16, top: 34, bottom: 30 }; const plotW = W - margin.left - margin.right; const plotH = H - margin.top - margin.bottom; const n = gaps.length; // Log time from one second to just past the slowest change; a zero gap // sits at the left edge. const top = Math.log10(Math.max(60, gaps[n - 1]) * 1.15); const x = (secs) => margin.left + (Math.log10(Math.max(1, secs)) / top) * plotW; const secsAt = (px) => 10 ** (((px - margin.left) / plotW) * top); // Height is the day the change was written, newest at the top. Within a // day a fixed scatter (a murmur-style mix of the index) spreads the dots. const firstDay = Math.max(firstAllowed, Math.min(...days)); const span = Math.max(1, today + 1 - firstDay); const yDay = (day) => margin.top + ((today + 1 - day) / span) * plotH; const jitter = (shown) => { let h = Math.imul(keep[shown] ^ 0x9e3779b9, 0x85ebca6b); h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); h ^= h >>> 16; return yDay(days[shown] + (h >>> 0) / 4294967296); };
// Rules down the left: each Monday over a month or less, else each month. const weekly = span <= 31; const next = (date) => weekly ? new Date(date.getTime() + 7 * 86400000) : new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1)); const firstDate = new Date(start.getTime() + firstDay * 86400000); const toMonday = (8 - firstDate.getUTCDay()) % 7; const first = weekly ? new Date(firstDate.getTime() + toMonday * 86400000) : new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), 1)); for (let rule = first; ; rule = next(rule)) { const day = Math.round((rule - start) / 86400000); if (day > today) break; if (day >= firstDay) { const my = yDay(day); svg.append(make("line", { x1: margin.left, x2: W - margin.right, y1: my, y2: my, class: "chart-grid" })); const label = make("text", { x: margin.left - 8, y: my + 4, "text-anchor": "end", class: "chart-tick" }); label.textContent = weekly ? rule.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone: "UTC" }) : rule.toLocaleDateString(undefined, { month: "short", timeZone: "UTC" }); svg.append(label); } }
for (const [secs, label] of [ [1, "≤1s"], [60, "1m"], [600, "10m"], [3600, "1h"], [21600, "6h"], [86400, "1d"], [604800, "1w"], [2592000, "30d"], [7776000, "90d"], [31536000, "1y"], ]) { if (Math.log10(secs) > top) continue; const gx = x(secs); svg.append(make("line", { x1: gx, x2: gx, y1: margin.top, y2: margin.top + plotH, class: "chart-grid" })); const tick = make("text", { x: gx, y: H - 9, "text-anchor": "middle", class: "chart-tick" }); tick.textContent = label; svg.append(tick); }
// No change written on a day can have waited longer than the time since: // the empty upper right is that limit, not a trend. const limit = []; for (let day = firstDay; day <= today; day += 1) { const secs = (today + 1 - day) * 86400; if (Math.log10(secs) > top) continue; limit.push(`${limit.length ? "L" : "M"} ${x(secs).toFixed(1)} ${yDay(day + 0.5).toFixed(1)}`); } if (limit.length > 1) { svg.append(make("path", { d: limit.join(" "), class: "gap-limit" })); // In the empty corner past the curve. const note = make("text", { x: W - margin.right, y: margin.top + 14, "text-anchor": "end", class: "chart-tick gap-limit-label", }); note.textContent = "longest possible wait"; svg.append(note); }
const slowCount = slowList.length; gaps.forEach((secs, index) => { const slow = index >= n - slowCount; svg.append(make("circle", { cx: x(secs).toFixed(1), cy: jitter(index).toFixed(1), r: slow ? 2.8 : 1.8, class: slow ? "gap-dot slow" : "gap-dot", })); });
// A label too close to the one before it steps up a row; a short // period can bunch the quantiles together. let lastLabel = { x: -Infinity, row: 1 }; for (const [secs, label] of [ [medianSecs, "median"], [p90Secs, "90th"], [p99Secs, "99th"], ]) { const qx = x(secs); const row = qx - lastLabel.x < 80 && lastLabel.row === 0 ? 1 : 0; lastLabel = { x: qx, row }; svg.append(make("line", { x1: qx, x2: qx, y1: margin.top - 6 - row * 12, y2: margin.top + plotH, class: "gap-quantile" })); const text = make("text", { x: qx, y: margin.top - 12 - row * 12, "text-anchor": "middle", class: "chart-tick gap-quantile-label" }); text.textContent = `${label} ${duration(secs)}`; svg.append(text); } svg.append(make("line", { x1: margin.left, x2: W - margin.right, y1: margin.top + plotH + 0.5, y2: margin.top + plotH + 0.5, class: "chart-axis", }));
// Hover reads the share landed within the pointer's gap, or names a // slow change when the pointer is on one. const cursor = make("line", { y1: margin.top, y2: margin.top + plotH, class: "gap-cursor", visibility: "hidden" }); svg.append(cursor); const hit = make("rect", { x: margin.left, y: margin.top - 6, width: plotW, height: plotH + 6, class: "chart-hit" }); const tooltip = $("gap-tooltip"); const within = (secs) => { let lo = 0; let hi = n; while (lo < hi) { const mid = (lo + hi) >> 1; if (gaps[mid] <= secs) lo = mid + 1; else hi = mid; } return lo; }; hit.addEventListener("pointermove", (event) => { const box = svg.getBoundingClientRect(); const scale = W / box.width; const px = (event.clientX - box.left) * scale; const py = (event.clientY - box.top) * scale; tooltip.replaceChildren(); let nearest = null; slowList.forEach((change) => { const d = Math.hypot(x(change.gap_secs) - px, jitter(change.shown) - py); if (d < 10 && (!nearest || d < nearest.d)) nearest = { change, d }; }); if (nearest) { const change = nearest.change; tooltip.append(el("div", "tip-date", `${duration(change.gap_secs)} · ${change.repo}`)); tooltip.append(el("div", "tip-subject", change.subject)); tooltip.append(el("div", "tip-dates", `${shortDate(change.authored_at)} → ${shortDate(change.committed_at)}`)); cursor.setAttribute("visibility", "hidden"); } else { const secs = secsAt(px); const count = within(secs); cursor.setAttribute("x1", px); cursor.setAttribute("x2", px); cursor.setAttribute("visibility", "visible"); tooltip.append(el("div", "tip-date", `within ${duration(secs)}`)); const row = el("div", "tip-row"); row.append(el("span", "tip-name", `${fmt(count)} changes`), el("span", "tip-value", pct((count / n) * 100))); tooltip.append(row); } tooltip.hidden = false; const wrap = tooltip.parentElement.getBoundingClientRect(); const width = tooltip.offsetWidth || 200; let left = event.clientX - wrap.left + 14; if (left + width > wrap.width - 6) left = Math.max(6, left - width - 28); tooltip.style.left = `${left}px`; tooltip.style.top = `${Math.max(6, event.clientY - wrap.top - 60)}px`; }); hit.addEventListener("pointerleave", () => { tooltip.hidden = true; cursor.setAttribute("visibility", "hidden"); }); svg.append(hit);
const slowest = slowList[0]; statRow($("gap-stats"), [ { label: "median", value: duration(medianSecs), meta: `${fmt(n)} changes` }, { label: "p90", value: duration(p90Secs), title: "1 in 10 waits longer" }, { label: "p99", value: duration(p99Secs), title: "1 in 100 waits longer" }, { label: "slowest", value: duration(gaps[n - 1]), meta: slowest ? slowest.repo : "" }, ]);}
function renderStatus() { const wrap = $("status"); wrap.replaceChildren(); const status = state.data.status; const summary = state.data.summary; const grid = el("dl", "stat-row status-grid"); statRow(grid, [ { label: "repos", value: `${fmt(status.repos_scanned)}/${fmt(status.repos_total)}` }, { label: "branches", value: `${fmt(status.branches_indexed)}/${fmt(status.branches_total)}` }, { label: "agents", value: fmt(status.bots_known) }, { label: "new", value: fmt(status.commits_new_this_run) }, ]); wrap.append(grid);
const bits = []; bits.push(status.scope === "owned" ? "owned · hourly" : "global"); if (status.last_crawl_finished) bits.push(dateTime(status.last_crawl_finished)); if (status.global_summary_at) { bits.push( `${status.global_summary_stale ? "global stale" : summary.global_partial ? "global partial" : "global"} · ${dateTime(status.global_summary_at)}`, ); } if (status.budget_exhausted) bits.push("budget hit"); if (status.github && status.github.configured) { bits.push(`github ${fmt(status.github.commits_new)} new`); } if (status.work_inbox) { bits.push(`work ${fmt(status.work_inbox.commits_new)} new`); } wrap.append(el("p", "sub", bits.join(" · ")));
if (status.errors && status.errors.length > 0) { const details = el("details", "method"); details.append(el("summary", "", `${status.errors.length} repositories could not be read`)); const list = el("ul", "errors"); for (const error of status.errors) { list.append(el("li", "", `${error.repo}: ${error.error}`)); } details.append(list); wrap.append(details); }
$("methodology").textContent = summary.methodology; const prior = $("prior-claim"); prior.replaceChildren(); const link = el("a", "", `${summary.prior_claim.percent}% on ${summary.prior_claim.date}`); link.href = summary.prior_claim.url; prior.append(link, document.createTextNode(`, ${summary.prior_claim.note}.`));}
/* ---------- controls ---------- */
// `?range=` keeps the period in links; the old day counts still read.function periodFromUrl() { const range = new URLSearchParams(window.location.search).get("range"); if (range && Object.hasOwn(PERIODS, range)) return range; return { 30: "30d", 90: "90d", 365: "1y" }[Number(range)] || DEFAULT_PERIOD;}
function periodToUrl() { const url = new URL(window.location.href); if (state.period === DEFAULT_PERIOD) url.searchParams.delete("range"); else url.searchParams.set("range", state.period); try { window.history.replaceState(null, "", url); } catch { // A file:// mockup may refuse; the page still follows the period. }}
// `?by=identity` keeps the breakdown in links; the default leaves no trace.function breakdownFromUrl() { const by = new URLSearchParams(window.location.search).get("by"); return BREAKDOWNS.includes(by) ? by : DEFAULT_BREAKDOWN;}
function breakdownToUrl() { const url = new URL(window.location.href); if (state.by === DEFAULT_BREAKDOWN) url.searchParams.delete("by"); else url.searchParams.set("by", state.by); try { window.history.replaceState(null, "", url); } catch { // A file:// mockup may refuse; the charts still follow the toggle. }}
function syncBreakdownButtons() { for (const button of document.querySelectorAll(".breakdown-picker button[data-by]")) { const active = button.dataset.by === state.by; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }}
// Every period picker on the page is this one control: the same four// buttons in the same order, filled into each empty `.period-picker` slot.function buildPeriodPickers() { for (const picker of document.querySelectorAll(".period-picker")) { if (picker.querySelector("button[data-period]")) continue; for (const period of Object.keys(PERIODS)) { const button = el("button", "", period); button.type = "button"; button.dataset.period = period; button.title = PERIODS[period].label; picker.append(button); } }}
function syncPeriodButtons() { for (const button of document.querySelectorAll(".period-picker button[data-period]")) { const active = button.dataset.period === state.period; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }}
function initControls() { buildPeriodPickers(); state.period = periodFromUrl(); syncPeriodButtons(); for (const button of document.querySelectorAll(".period-picker button[data-period]")) { button.addEventListener("click", () => { if (button.dataset.period === state.period) return; state.period = button.dataset.period; syncPeriodButtons(); periodToUrl(); if (state.data) render(); }); }
// One breakdown for both charts; each card carries a copy of the toggle. state.by = breakdownFromUrl(); syncBreakdownButtons(); for (const button of document.querySelectorAll(".breakdown-picker button[data-by]")) { button.addEventListener("click", () => { if (button.dataset.by === state.by) return; state.by = button.dataset.by; syncBreakdownButtons(); breakdownToUrl(); if (!state.data) return; renderLegend(); renderChart(); renderTable(); renderWheel(); }); }
$("types-table-toggle").addEventListener("click", () => { state.typesTable = !state.typesTable; renderTypesTable(typesView(state.data.commit_types)); });
$("timelines-table-toggle").addEventListener("click", () => { state.timelinesTable = !state.timelinesTable; renderTimelines(); });
// The lanes are drawn in pixels, so a new width redraws them. let timelinesWidth = 0; new ResizeObserver((entries) => { const width = Math.round(entries[0].contentRect.width); if (width === timelinesWidth) return; timelinesWidth = width; if (state.data) renderTimelines(); }).observe($("timelines"));
$("day-close").addEventListener("click", closeDay); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && state.openDay !== null) closeDay(); }); window.addEventListener("hashchange", syncDay); window.addEventListener("popstate", syncDay);
$("table-toggle").addEventListener("click", () => { state.table = !state.table; renderTable(); renderWheel(); });
const requestedTheme = new URLSearchParams(window.location.search).get("theme"); const stored = localStorage.getItem("tally-theme"); if (requestedTheme === "light" || requestedTheme === "dark") { document.documentElement.dataset.theme = requestedTheme; } else if (stored === "light" || stored === "dark") { document.documentElement.dataset.theme = stored; } $("theme").addEventListener("click", () => { const effective = document.documentElement.dataset.theme || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); const next = effective === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; localStorage.setItem("tally-theme", next); });}
initControls();load();setInterval(load, 5 * 60 * 1000);