// Tasks: the taxonomy as a browser. Empty branches stay visible, so coverage gaps show. import { sparkline } from "./charts.js"; import { color, dayLabel, empty, fmt, h, redraws, sectionHead, system } from "./util.js"; const leafKey = (domain, leaf) => `${domain}/${leaf}`; /** Best agent, expert forecast and best TSFM on a task's live leaderboard. */ function headline(rows) { const best = (kind) => rows.filter((r) => system(r.system).kind === kind).sort((a, b) => a.scaled_crps - b.scaled_crps)[0]; return [["Agent", best("agent")], ["Expert", best("expert")], ["TSFM", best("tsfm")]].filter(([, r]) => r); } function scoreMini(rows) { const lines = headline(rows); if (!lines.length) return h("span", { class: "muted small" }, "not scored yet"); const max = d3.max(lines, ([, r]) => r.scaled_crps) * 1.1; return h("div", { class: "score-mini", title: "Scaled CRPS on live forecasts, common pairs; lower is better, 1 = seasonal naive" }, lines.map(([label, r]) => h("div", { class: "row" }, h("span", { title: system(r.system).label }, label), h("span", { class: "bar-track" }, h("span", { class: "bar-fill", style: { width: `${(r.scaled_crps / max) * 100}%`, background: color(r.system) } })), h("span", { class: "v" }, fmt.ratio(r.scaled_crps))))); } export function tasksPage(app, index, params) { const { taxonomy, tasks } = index; const domains = taxonomy.domains; const leafCount = d3.rollup(tasks, (v) => v.length, (t) => t.taxonomy); const domainCount = d3.rollup(tasks, (v) => v.length, (t) => t.taxonomy?.split("/")[0]); const leaves = domains.flatMap((d) => d.leaves.map((l) => ({ ...l, domain: d, key: leafKey(d.id, l.id) }))); const covered = leaves.filter((l) => leafCount.get(l.key)); const names = new Map(leaves.map((l) => [l.key, `${l.domain.name} › ${l.name}`])); const drivers = taxonomy.facets.driver; const state = { area: params.get("area") || "", q: params.get("q") || "", driver: params.get("driver") || "", tradable: params.get("tradable") || "" }; const tree = h("nav", { class: "tree", "aria-label": "Taxonomy" }); const toolbar = h("div", { class: "toolbar" }); const listing = h("div"); function save() { const query = new URLSearchParams(Object.entries(state).filter(([, v]) => v)); history.replaceState(null, "", `#/tasks${query.size ? `?${query}` : ""}`); } function renderTree() { const pick = (area) => () => { state.area = state.area === area ? "" : area; save(); render(); }; tree.replaceChildren( h("div", { class: "tree-head" }, h("span", { class: "eyebrow" }, "Taxonomy"), h("span", { class: "note" }, `${covered.length} of ${leaves.length} areas covered`)), h("div", { class: "coverage", title: `${covered.length} of ${leaves.length} areas have a task` }, h("div", { style: { width: `${(covered.length / leaves.length) * 100}%` } })), h("button", { type: "button", "aria-pressed": String(!state.area), onclick: pick("") }, h("span", {}, "All tasks"), h("span", { class: "count" }, String(tasks.length))), ...domains.flatMap((d) => [ h("button", { type: "button", class: "domain", "aria-pressed": String(state.area === d.id), onclick: pick(d.id) }, h("span", {}, d.name), h("span", { class: "count" }, String(domainCount.get(d.id) || 0))), d.leaves.map((l) => { const key = leafKey(d.id, l.id); const n = leafCount.get(key) || 0; return h("button", { type: "button", class: `leaf${n ? "" : " gap"}`, "aria-pressed": String(state.area === key), onclick: pick(key), title: n ? null : `Candidates: ${l.examples.join("; ")}` }, h("span", {}, l.name), h("span", { class: "count" }, String(n))); }), ]).flat()); } function matches(t) { if (state.area && !(t.taxonomy === state.area || t.taxonomy?.startsWith(`${state.area}/`))) return false; if (state.driver && t.driver !== state.driver) return false; if (state.tradable && String(Boolean(t.tradable)) !== state.tradable) return false; const q = state.q.trim().toLowerCase(); return !q || [t.title, t.question, t.summary, t.region, names.get(t.taxonomy)].some((s) => s?.toLowerCase().includes(q)); } function renderToolbar() { const facet = (key, value, text) => h("button", { type: "button", class: "chip", "aria-pressed": String(state[key] === value), onclick: () => { state[key] = state[key] === value ? "" : value; save(); render(); }, }, text); const search = h("input", { class: "search", type: "search", placeholder: "Search tasks", value: state.q, "aria-label": "Search tasks" }); search.addEventListener("input", () => { state.q = search.value; save(); renderListing(); }); toolbar.replaceChildren(search, h("div", { class: "facets" }, h("div", { class: "chips" }, h("span", { class: "facet-label" }, "Driver"), Object.entries(drivers).map(([id, text]) => facet("driver", id, text))), h("div", { class: "chips" }, h("span", { class: "facet-label" }, "Market"), facet("tradable", "false", "Not tradable"), facet("tradable", "true", "Tradable controls")))); } function renderListing() { const shown = tasks.filter(matches); const leaf = leaves.find((l) => l.key === state.area); if (!shown.length) { listing.replaceChildren(leaf ? h("div", { class: "gap-card" }, h("h4", {}, `No task in ${leaf.domain.name} › ${leaf.name} yet`), h("p", { class: "secondary small" }, `Candidate targets: ${leaf.examples.join("; ")}.`)) : empty("No task matches these filters.")); return; } listing.replaceChildren(h("div", { class: "card" }, h("div", { class: "table-wrap" }, h("table", {}, h("thead", {}, h("tr", {}, h("th", { class: "col-task" }, "Task"), h("th", { class: "hide-sm" }, "Recent values"), h("th", { class: "hide-sm" }, "Run days"), h("th", {}, "Live scaled CRPS"))), h("tbody", {}, shown.map((t) => { const spark = h("div", { class: "chart", style: { width: "120px" } }); redraws.push(() => sparkline(spark, { truth: t.truth })); const open = () => { location.hash = `#/t/${t.id}`; }; return h("tr", { class: "link", onclick: open }, h("td", { class: "col-task" }, h("a", { class: "task-title", href: `#/t/${t.id}`, onclick: (e) => e.stopPropagation() }, t.title), h("span", { class: "task-q" }, t.question), h("div", { class: "chips", style: { marginTop: "6px" } }, h("span", { class: "chip" }, names.get(t.taxonomy) || "unclassified"), t.driver ? h("span", { class: "chip" }, drivers[t.driver]) : null, t.tradable ? h("span", { class: "chip" }, "tradable") : null, h("span", { class: "note" }, `${t.region} · ${t.unit}`))), h("td", { class: "hide-sm" }, spark, h("span", { class: "cell-sub" }, `last ${dayLabel(t.last_observed)}`)), h("td", { class: "nowrap hide-sm" }, h("div", {}, String(t.run_days)), h("span", { class: "cell-sub" }, t.live_since ? `since ${dayLabel(t.live_since)}` : "not started")), h("td", {}, scoreMini(t.leaderboard))); })))))); redraws.forEach((draw) => draw()); } function render() { redraws.length = 0; renderTree(); renderToolbar(); renderListing(); } const coverageTable = h("div", { class: "card" }, h("div", { class: "table-wrap" }, h("table", {}, h("thead", {}, h("tr", {}, h("th", {}, "Domain"), h("th", { class: "num" }, "Areas covered"), h("th", { class: "num" }, "Tasks"), h("th", {}, "Open areas and candidate targets"))), h("tbody", {}, domains.map((d) => { const open = d.leaves.filter((l) => !leafCount.get(leafKey(d.id, l.id))); return h("tr", {}, h("td", {}, h("strong", {}, d.name)), h("td", { class: "num" }, `${d.leaves.length - open.length} / ${d.leaves.length}`), h("td", { class: "num" }, String(domainCount.get(d.id) || 0)), h("td", { class: "small secondary" }, open.length ? open.map((l, i) => [i ? " · " : "", h("span", { title: l.examples.join("; ") }, l.name)]) : h("span", { class: "muted" }, "all covered"))); }))))); const agents = index.systems.filter((s) => s.kind === "agent").length; app.replaceChildren( h("header", { class: "page-head" }, h("span", { class: "eyebrow" }, "Tasks"), h("h1", {}, "Forecasting tasks"), h("p", { class: "lead" }, "Each task is a real series with an expert forecast to beat. Tasks are organised by a taxonomy of the systems that generate them; empty areas are gaps to fill."), h("div", { class: "stats" }, [[tasks.length, "tasks"], [d3.sum(tasks, (t) => t.run_days), "run days"], [agents, "agents"], [`${covered.length} / ${leaves.length}`, "areas covered"]] .map(([v, l]) => h("div", { class: "stat" }, h("div", { class: "v" }, String(v)), h("div", { class: "l" }, l))))), h("section", { class: "section browser" }, tree, h("div", {}, toolbar, listing)), h("section", { class: "section" }, sectionHead("coverage", "Coverage", "Tasks per domain of the taxonomy. Open areas list the targets that would fill them; the taxonomy itself is revised as tasks are added."), coverageTable)); render(); }