// DOM, formatting and color helpers, and components several pages share. // Data strings only ever enter the DOM as text nodes. export function h(tag, attrs = {}, ...children) { const el = document.createElement(tag); for (const [key, value] of Object.entries(attrs || {})) { if (value == null || value === false) continue; if (key === "class") el.className = value; else if (key === "style" && typeof value === "object") Object.assign(el.style, value); else if (key.startsWith("on")) el.addEventListener(key.slice(2), value); else el.setAttribute(key, value === true ? "" : value); } for (const child of children.flat(Infinity)) { if (child == null || child === false) continue; el.append(child instanceof Node ? child : document.createTextNode(String(child))); } return el; } // Chart renderers re-run on resize and theme changes; pages register them here. export const redraws = []; export const parseDay = d3.utcParse("%Y-%m-%d"); export const fmt = { int: d3.format(",.0f"), ratio: (v) => (v == null ? "—" : d3.format(".3f")(v)), pct: (v) => (v == null ? "—" : d3.format(".0%")(v)), // whole numbers for large targets (MW), two decimals for small ones (river stage, AQHI) value: (v) => (v == null ? "—" : d3.format(Math.abs(v) < 100 ? ",.2f" : ",.0f")(v)), compact: (v) => (v == null ? "—" : d3.format(".3~s")(v).replace("G", "B")), bytes: (v) => (v == null ? "—" : `${d3.format(".2~s")(v)}B`), usd: (v) => (v == null ? "—" : `$${d3.format(".2f")(v)}`), day: d3.utcFormat("%b %-d"), dow: d3.utcFormat("%a %b %-d"), long: d3.utcFormat("%a, %b %-d, %Y"), }; export const dayLabel = (iso, style = "day") => (iso ? fmt[style](parseDay(iso)) : "—"); export function timeIn(iso, timeZone, withDate = false) { if (!iso) return "—"; const options = { timeZone, hour: "2-digit", minute: "2-digit", hour12: false, timeZoneName: "short" }; if (withDate) Object.assign(options, { weekday: "short", month: "short", day: "numeric" }); return new Intl.DateTimeFormat("en-US", options).format(new Date(iso)); } export function duration(seconds) { if (seconds == null) return "—"; const m = Math.round(seconds / 60); return m < 60 ? `${m} min` : `${Math.floor(m / 60)} h ${m % 60} min`; } export function host(url) { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return null; } } // Color follows the system, never its rank: fixed slots for known systems, stable spill-over // slots for new ones, and recessive gray for the seasonal-naive reference. const SLOTS = { "codex-cli-gpt-6.1-sol": 1, "claude-code-claude-sonnet-5": 2, "chronos-2": 3, "chronos-2-univariate": 4, "timesfm-3": 5, "chronos-2-expert": 6, "timesfm-3-expert": 8, }; const SPARE = [6, 8]; let systemIndex = new Map(); export function setSystems(systems) { systemIndex = new Map(systems.map((s) => [s.id, s])); let spare = 0; for (const s of systems) { if (s.id in SLOTS || s.kind === "reference" || s.kind === "expert") continue; // a continual agent takes its fresh twin's slot: the same agent, carrying on const twin = s.id.replace(/-continual$/, ""); SLOTS[s.id] = twin in SLOTS ? SLOTS[twin] : SPARE[spare++ % SPARE.length]; } } export const system = (id) => systemIndex.get(id) || { id, kind: "agent", label: id, description: "" }; // every task's expert forecast shares one slot; a task page shows only its own export const color = (id) => { const kind = system(id).kind; if (kind === "reference") return "var(--muted)"; return `var(--series-${kind === "expert" ? 7 : SLOTS[id] || 8})`; }; export const KINDS = ["agent", "expert", "tsfm", "reference"]; export const KIND_GROUP = { agent: "Agents", expert: "Expert forecast", tsfm: "TSFM baselines", reference: "Reference" }; export const KIND_LABEL = { agent: "Agent", expert: "Expert", tsfm: "TSFM", reference: "Reference" }; export const bySystem = (a, b) => KINDS.indexOf(system(a).kind) - KINDS.indexOf(system(b).kind) || system(a).label.localeCompare(system(b).label); export const keyLine = (id) => h("span", { class: "key-line", style: { background: color(id) } }); export const systemCell = (id) => h("span", { class: "system-cell", title: system(id).description }, keyLine(id), system(id).label); export function segmented(options, current, onChange, label) { return h("div", { class: "segmented", role: "group", "aria-label": label }, options.map(([value, text]) => h("button", { type: "button", "aria-pressed": String(value === current), onclick: () => onChange(value) }, text))); } export function sectionHead(id, title, text, ...extra) { return h("div", { class: "section-head", id }, h("div", {}, h("h2", {}, title), text ? h("p", {}, text) : null), ...extra); } export const empty = (text) => h("div", { class: "empty" }, text); /** Text whose $…$ spans are inline TeX, e.g. md("frozen as $X_d$"); for h() children. */ export const md = (text) => text.split(/\$([^$]+)\$/).map((part, i) => (i % 2 ? tex(part) : part)); /** TeX typeset by KaTeX when it loaded, else the source in a code span. */ export function tex(source, display = false) { const el = h(display ? "div" : "span", { class: display ? "tex-display" : "tex" }); if (window.katex) window.katex.render(source, el, { displayMode: display, throwOnError: false, output: "html" }); else el.append(h("code", {}, source)); return el; } export function more(label, ...body) { return h("details", { class: "more" }, h("summary", {}, label), h("div", { class: "body" }, ...body)); } export function sourceLink(source) { return source?.url ? h("a", { href: source.url, target: "_blank", rel: "noopener" }, source.name || host(source.url)) : source?.name || "—"; } export function scoreBar(value, id, max) { if (value == null) return h("span", { class: "muted" }, "—"); return h("div", { class: "bar-cell" }, h("div", { class: "bar-track" }, h("div", { class: "bar-fill", style: { width: `${(Math.min(value, max) / max) * 100}%`, background: color(id) } }), max >= 1 ? h("div", { class: "bar-tick", style: { left: `${(1 / max) * 100}%` }, title: "seasonal naive = 1" }) : null), h("span", { class: "bar-value" }, fmt.ratio(value))); } function coverage(value) { if (value == null) return h("span", { class: "muted small" }, "point"); return h("span", { class: "cov", title: "share of actual values inside the 10–90% interval; 80% is calibrated" }, h("span", { class: "cov-track" }, h("span", { class: "bar-tick", style: { left: "80%" } }), h("span", { class: "cov-dot", style: { left: `${value * 100}%` } })), fmt.pct(value)); } /** Leaderboard rows grouped by kind: agents, expert forecast, TSFM baselines, reference. */ export function leaderboard(rows, unit) { if (!rows.length) return empty("No forecast has been scored yet."); const max = Math.max(1.05, d3.max(rows, (r) => r.scaled_crps) || 1); const groups = d3.group([...rows].sort((a, b) => (a.scaled_crps ?? 9) - (b.scaled_crps ?? 9)), (r) => system(r.system).kind); return h("div", { class: "table-wrap" }, h("table", {}, h("thead", {}, h("tr", {}, h("th", {}, "System"), h("th", {}, "Scaled CRPS ↓"), h("th", { class: "num" }, "Scaled MAE"), h("th", {}, "80% coverage"), h("th", { class: "num" }, `CRPS (${unit})`), h("th", { class: "num" }, "Pairs"))), h("tbody", {}, KINDS.filter((k) => groups.has(k)).map((kind) => [ h("tr", { class: "group" }, h("td", { colspan: 6 }, KIND_GROUP[kind])), groups.get(kind).map((r) => h("tr", {}, h("td", {}, systemCell(r.system)), h("td", {}, scoreBar(r.scaled_crps, r.system, max)), h("td", { class: "num" }, fmt.ratio(r.scaled_mae)), h("td", {}, coverage(r.coverage80)), h("td", { class: "num" }, fmt.value(r.crps)), h("td", { class: "num" }, String(r.n)))), ])))); } const tooltip = () => document.getElementById("tooltip"); export function showTooltip(event, content) { const el = tooltip(); el.replaceChildren(content); el.hidden = false; const { innerWidth, innerHeight } = window; const rect = el.getBoundingClientRect(); let x = event.clientX + 16, y = event.clientY + 16; if (x + rect.width > innerWidth - 8) x = event.clientX - rect.width - 16; if (y + rect.height > innerHeight - 8) y = event.clientY - rect.height - 16; el.style.left = `${Math.max(8, x)}px`; el.style.top = `${Math.max(8, y)}px`; } export const hideTooltip = () => { tooltip().hidden = true; };