// An environment, whatever its format: /d//. Its adapter (app/envs) says how it's read, gives an overview // (how its tasks are graded, what they run in), its facets and its tasks as cards; this page draws them one way: // inline every task at once (a Harbor index): search and filters run here as you type, with tiles and a map // paged a page at a time (dataset rows): subsets, search and filters run on the server // A Harbor dataset is indexed the first time it's opened; the page shows the indexing's progress meanwhile. import { $, api, esc, fmt, ago, sk, emptyState, toast, spinner, nav, setMeta, ownerLink } from "./util.js"; import { icon } from "./icons.js"; import { supportPanel } from "./support.js"; import { blocks, loadRenderers } from "./blocks.js"; import { taskHref } from "./task.js"; import { typeset } from "./math.js"; import { agentName, connectPanel, wireConnect } from "./connect.js"; const PAGE = 30; const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }); const enc = (s) => s.split("/").map(encodeURIComponent).join("/"); const envApi = (spec) => `/api/env/${enc(spec)}`; // indexing, step by step: what each step does, and how much of the whole it is const STEPS = [["listing", "List the repository", 0.1], ["reading", "Fetch every task's files", 0.7], ["parsing", "Read every task", 0.15], ["saving", "Save the index", 0.05]]; // difficulty reads easy to hard, not by count const LEVEL = ["trivial", "very easy", "easy", "medium", "moderate", "intermediate", "hard", "difficult", "very hard", "expert", "extreme"]; const level = (v) => { const i = LEVEL.indexOf(String(v).toLowerCase()); return i < 0 ? (Number.isFinite(+v) ? +v : 99) : i; }; let poll = null, ticker = null, root = null; let st = {}; let matches = [], shown = 0; const expanded = new Set(), facetOpen = new Map(); export function unmount() { clearTimeout(poll); clearInterval(ticker); scrollWatch?.disconnect(); root = null; document.body.classList.remove("noscroll"); } // the URL's state: ?s=&q=…&f={"facet":["value",…]}&t=&o=; older links (?c=&s= of the rows // page, f=k:v|v;k2:v of the Harbor page) still open where they pointed function readURL(qs) { const sel = {}; const f = qs.get("f") || ""; if (f.startsWith("{")) { try { Object.entries(JSON.parse(f)).forEach(([k, v]) => { sel[k] = new Set((Array.isArray(v) ? v : [v]).map(String)); }); } catch { /* a broken link: no filters */ } } else { f.split(";").filter(Boolean).forEach((pair) => { const i = pair.indexOf(":"); if (i > 0) sel[pair.slice(0, i)] = new Set(pair.slice(i + 1).split("|").map(decodeURIComponent)); }); } const c = qs.get("c"), s = qs.get("s"); return { q: qs.get("q") || "", sel, tile: qs.get("t") ?? qs.get("d") ?? null, offset: Math.max(0, +qs.get("o") || 0), subset: c ? (c === "default" ? s : `${c}/${s}`) : s, open: qs.get("open") || (qs.get("rewards") ? "rewards" : null) }; } function writeURL() { const p = new URLSearchParams(); if (st.subset && st.sum?.subsets?.length > 1) p.set("s", st.subset); if (st.q) p.set("q", st.q); if (st.tile != null) p.set("t", st.tile); const sel = Object.fromEntries(Object.entries(st.sel).filter(([, s]) => s.size).map(([k, s]) => [k, [...s]])); if (Object.keys(sel).length) p.set("f", JSON.stringify(sel)); if (st.offset) p.set("o", st.offset); history.replaceState(null, "", `/d/${enc(st.spec)}${p.toString() ? `?${p}` : ""}`); } // ── page ───────────────────────────────────────────────────────────────────── export async function mount(el, { spec, qs }) { root = el; wireConnect(el); st = { spec, sum: null, cards: null, partial: false, ...readURL(qs) }; el.innerHTML = `
${sk.line(16, 14)}${sk.lines(92, 60)}
${sk.list()}
`; await load(el); } async function load(el) { let r; try { r = await api(`${envApi(st.spec)}${st.subset ? `?subset=${encodeURIComponent(st.subset)}` : ""}`, { retry: 2 }); } catch (e) { if (root !== el) return; if (e.status === 429) { // others are being indexed: wait for a slot $("#body", el).innerHTML = `

Waiting for a slot

${esc(e.message)}. This page starts indexing by itself as soon as one frees up.

`; poll = setTimeout(() => load(el), 5000); return; } if (st.subset && e.status !== 404 && !st.retried) { st.retried = true; st.subset = null; return load(el); } // a subset that's gone $("#head", el).innerHTML = crumbs(); $("#body", el).innerHTML = emptyState("alert", "Couldn't open this environment", esc(e.message), `${icon("external", 14)}See it on the Hub`); return; } if (root !== el) return; st.sum = r; st.subset = r.subset || null; st.renderers = await loadRenderers(r.overview || []); if (root !== el) return; renderHead(el); if (r.state === "indexing") { renderProgress(el, r.progress || {}); poll = setTimeout(() => load(el), 1000); return; } clearInterval(ticker); if (r.state === "error") { $("#body", el).innerHTML = `
${icon("alert")}Couldn't read this environment: ${esc(r.progress?.error || r.error || "")}
`; return; } if (r.inline) { // a big Harbor dataset: its tasks are known, their files still being read; titles and filters fill in when done if (r.progress && r.progress.total) { if (!st.partial) { await loadCards(); if (root !== el) return; renderBody(el); st.partial = true; } partialNote(el, r.progress); poll = setTimeout(() => load(el), 4000); return; } const was = st.partial; st.partial = false; await loadCards(); if (root !== el) return; renderBody(el); if (was) toast("Every task's details are in"); return; } renderBody(el); } async function loadCards() { const state = st; try { state.cards = (await api(`${envApi(state.spec)}/tasks?all=1`)).cards; } catch (e) { state.cards = []; if (state === st) toast(e.message); } if (state !== st) return; const n = state.cards.length; state.cards.forEach((c) => { c._blob = `${c.title} ${c.ref} ${c.id || ""} ${c.brief || ""} ${c.text || ""}`.toLowerCase(); }); // what every task shares says nothing on a card const count = new Map(); st.cards.forEach((c) => c.chips.forEach((x) => count.set(x, (count.get(x) || 0) + 1))); st.shared = new Set(n > 1 ? [...count].filter(([, k]) => k === n).map(([x]) => x) : []); } // ── head ───────────────────────────────────────────────────────────────────── function crumbs() { const name = st.spec.split("/")[1], c = st.sum?.collection; return ``; } function renderHead(el) { const s = st.sum, i = s.info, c = s.collection; const [org, name] = st.spec.split("/"); const norm = (x) => String(x || "").toLowerCase().replace(/[^a-z0-9]/g, ""); const lede = i.brief || (i.heading && !norm(i.heading).includes(norm(name)) ? i.heading : ""); // not the name again const n = s.subsets?.length > 1 ? null : s.total; setMeta({ title: `${name} · ${s.env.framework}`, description: `${st.spec}: ${s.env.framework} environment on Hugging Face${s.total ? ` with ${fmt.format(s.total)} tasks` : ""}. ${lede || s.how?.about || ""}` }); $("#head", el).innerHTML = `${crumbs()}
${c ? `${icon(c.icon, 13)}${esc(c.group)}` : ""} ${esc(s.env.framework)}${(i.badges || []).map((b) => `${esc(b)}`).join("")}

${ownerLink(org, "/")}${esc(name)}

${lede ? `

${esc(lede)}

` : ""}
${n != null ? `${icon("list", 14)}${fmt.format(n)} task${n === 1 ? "" : "s"}` : s.subsets?.length > 1 ? `${icon("columns", 14)}${fmt.format(s.subsets.length)} subsets` : ""} ${icon("download", 14)}${compact.format(i.downloads || 0)} downloads ${icon("heart", 14)}${fmt.format(i.likes || 0)} likes ${i.license ? `${icon("scale", 14)}${esc(i.license)}` : ""} ${i.updated ? `${icon("clock", 14)}Updated ${esc(ago(Date.parse(i.updated) / 1000))}` : ""} ${icon("flag", 14)}${esc((i.sha || "").slice(0, 8))} ${icon("external", 14)}On the Hub
${supportPanel(s.support)}`; $("#copy", el).addEventListener("click", () => navigator.clipboard?.writeText(location.href).then(() => toast("Link copied"))); } // ── indexing progress: a timeline of the steps, live counts, elapsed time and what's left ─ const secsText = (s) => (s < 1 ? "<1 s" : s < 60 ? `${Math.round(s)} s` : `${Math.floor(s / 60)} min ${Math.round(s % 60)} s`); function renderProgress(el, r) { const body = $("#body", el); if (!$(".ix", body)) { body.innerHTML = `

Indexing this dataset

The first visit reads every task once; after that it opens instantly, for everyone.

0%
    ${STEPS.map(([k, label]) => `
  1. ${label}
  2. `).join("")}
`; } const skew = Date.now() / 1000 - (r.now || Date.now() / 1000); // server clock to ours const t = r.t || {}, at = Math.max(0, STEPS.findIndex(([k]) => k === r.state)); const frac = r.total ? Math.min(1, (r.done || 0) / r.total) : 0; const pct = STEPS.slice(0, at).reduce((n, [, , w]) => n + w, 0) + (STEPS[at]?.[2] || 0) * (r.state === "listing" ? 0.5 : frac); $("#ix-pct", body).textContent = `${Math.round(pct * 100)}%`; $("#ix-fill", body).style.width = `${Math.max(3, pct * 100)}%`; $(".ix-bar", body).classList.toggle("busy", !r.state || r.state === "listing" || r.state === "saving"); const detail = { listing: () => (r.files ? `${fmt.format(r.files)} files listed${r.tasks ? `, ${fmt.format(r.tasks)} tasks found` : ""}` : "Asking the Hub for every file"), reading: () => (r.total ? `${fmt.format(r.done || 0)} of ${fmt.format(r.total)} files: task.toml, instruction, tests, Dockerfile` : ""), parsing: () => (r.total ? `${fmt.format(r.done || 0)} of ${fmt.format(r.total)} tasks` : ""), saving: () => "Kept for the next visit", }; const done = { listing: () => `${fmt.format(r.files || 0)} files, ${fmt.format(r.tasks || 0)} tasks`, reading: () => "Fetched", parsing: () => `${fmt.format(r.tasks || 0)} tasks read`, saving: () => "Saved" }; STEPS.forEach(([k], i) => { const li = $(`[data-step="${k}"]`, body); li.className = i < at ? "ok" : i === at ? "on" : "off"; li.querySelector(".mk").innerHTML = i < at ? icon("check", 12) : ""; li.querySelector(".d").textContent = i < at ? done[k]() : i === at ? detail[k]() : ""; const next = STEPS[i + 1] && t[STEPS[i + 1][0]]; li.querySelector(".t").dataset.start = t[k] ? t[k] + skew : ""; li.querySelector(".t").dataset.end = i < at && next ? next + skew : ""; }); const start = t[r.state] ? t[r.state] + skew : null; const rate = start && r.done ? r.done / (Date.now() / 1000 - start) : 0; $("#ix-eta", body).textContent = rate && r.total > r.done ? `about ${secsText((r.total - r.done) / rate)} left in this step` : ""; clearInterval(ticker); const tick = () => body.querySelectorAll(".ix-steps .t").forEach((x) => { const a = +x.dataset.start, b = +x.dataset.end; x.textContent = a ? secsText((b || Date.now() / 1000) - a) : ""; }); tick(); ticker = setInterval(tick, 250); } // a slim strip above the tasks while a walked dataset's files are read function partialNote(el, p) { let box = $("#ix-partial", el); if (!box) { $("#body", el).insertAdjacentHTML("afterbegin", `
`); box = $("#ix-partial", el); } const pct = p.total ? Math.min(100, Math.round((100 * (p.done || 0)) / p.total)) : 0; box.innerHTML = `${spinner("xs")}Reading every task's files ${p.total ? `${fmt.format(p.done || 0)} of ${fmt.format(p.total)}` : "…"}. A big dataset: its tasks are listed already, and titles, grading and filters fill in when this is done. Any task opens now. `; } // ── body ───────────────────────────────────────────────────────────────────── const ROLE = [["task", "The task"], ["id", "Its id"], ["title", "Its title"], ["grading", "Graded by"], ["environment", "Runs in"], ["answer", "Withheld"]]; function how(s) { const h = s.how || {}, roles = h.roles || {}; const cols = (v) => (Array.isArray(v) ? v : [v]).map((x) => `${esc(x)}`).join(" "); const rows = ROLE.filter(([k]) => roles[k] && (!Array.isArray(roles[k]) || roles[k].length)).map(([k, label]) => `
${label}
${cols(roles[k])}
`).join(""); const via = s.inline ? "indexed from its files" : s.source === "files" ? "straight from its files" : "through the Hub's dataset viewer"; return `

${icon("table", 15)}How it's read

${via}

${esc(h.name || s.env.name)}. ${esc(h.about || s.env.about || "")}

${rows ? `
${rows}
` : ""}
${icon("chevronRight", 14, "chev")}Browse it from a coding agent, over MCP ${connectPanel({ url: `${location.origin}/mcp/d/${st.spec}`, name: agentName(st.spec), note: "Tools: describe, list_tasks (search and filters), get_task (a task in full, as text), read_file and random_task. Answers are withheld, as they are here. No sign-in needed; public datasets only." })}
`; } function renderBody(el) { const s = st.sum; if (s.inline && !st.cards.length) { $("#body", el).innerHTML = `
${emptyState("archive", "No tasks to show", esc(s.note || "Nothing in this dataset reads as a task."))}
`; return; } st.facets = (s.facets || []).filter((f) => f.key); st.noun = s.noun || ["task", "tasks"]; const nvals = (key) => (s.inline ? counts(st.cards, key).length : (st.facets.find((f) => f.key === key)?.values || []).length); // tiles: the adapter's (with notes and colours), or, for an inline list, a facet with a handful of values const tkey = s.tiles?.key || s.tile; st.tileKey = s.inline && tkey && nvals(tkey) >= 2 ? tkey : null; st.tileMeta = new Map((s.tiles?.items || []).map((t) => [t.value, t])); if (st.tile != null && !(st.tileKey && counts(st.cards, st.tileKey).some(([v]) => v === st.tile))) st.tile = null; // the map: by each tile's own facet (with tiles), the adapter's facet, or the first facet with enough values st.mapBy = s.map?.by_tile && Object.keys(s.map.by_tile).length ? s.map : null; const mapOrder = [typeof s.map === "string" ? s.map : s.map?.key, "category", "group", ...st.facets.map((f) => f.key).filter((k) => k.startsWith("meta:")), "tags", "difficulty"].filter(Boolean); st.mapKey = s.inline && !st.mapBy ? mapOrder.find((k) => k !== st.tileKey && st.facets.some((f) => f.key === k) && nvals(k) >= 5) || null : null; if (s.inline && s.order === "shuffle") { const seed = Math.random(); st.cards.forEach((x) => (x._rand = hash(x.ref + seed))); st.cards.sort((a, b) => a._rand - b._rand); } buildSearch(); const color = s.collection ? `var(--c-${s.collection.color || s.collection.id})` : "var(--text)"; const subsets = !s.inline && s.subsets?.length > 1; const panel = (sec) => sec.collapsed ? `
${icon("chevronRight", 15, "chev")}

${icon(sec.icon || "list", 15)}${esc(sec.title)}

${sec.note ? `${esc(sec.note)}` : ""}
${blocks(sec.blocks, { files: [], renderers: st.renderers, spec: st.spec })}
` : `

${icon(sec.icon || "list", 15)}${esc(sec.title)}

${sec.note ? `${esc(sec.note)}` : ""}
${blocks(sec.blocks, { files: [], renderers: st.renderers, spec: st.spec })}
`; const open = (s.overview || []).filter((x) => !x.collapsed), folded = (s.overview || []).filter((x) => x.collapsed); const what = st.noun[1]; $("#body", el).innerHTML = `${how(s)} ${open.length ? `
${open.map(panel).join("")}
` : ""} ${folded.map(panel).join("")} ${st.tileKey ? `
` : ""} ${st.mapKey || st.mapBy ? `
${icon("chevronRight", 15, "chev")}

Area is the number of ${esc(what)}. Click a block to filter.
` : ""}
${subsets ? `` : ""} ${st.facets.length ? `` : ``}
${s.inline ? "" : ``}
    `; Object.values(st.renderers || {}).forEach((r) => r.wire?.(el, { spec: st.spec })); if (st.open) requestAnimationFrame(() => $(`#ov-${CSS.escape(st.open)}`, el)?.scrollIntoView({ block: "start" })); $("#q", el).value = st.q; wire(el); run(); } function hash(s) { let h = 2166136261; for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619); return h >>> 0; } // ── search: fuzzy and ranked when MiniSearch is there (prefix, a typo or two, title first), plain words otherwise ─ const CJK = /[㐀-鿿豈-﫿]/; let mini = null, hits = null; function buildSearch() { mini = null; if (!st.sum.inline || !window.MiniSearch) return; mini = new window.MiniSearch({ idField: "ref", fields: ["title", "brief", "text"], storeFields: [], searchOptions: { boost: { title: 3, text: 2 }, prefix: true, fuzzy: 0.15, combineWith: "AND" } }); mini.addAll(st.cards); } function search() { const q = st.q.trim(); if (!q) { hits = null; return null; } if (!mini || CJK.test(q)) { const words = q.toLowerCase().split(/\s+/); const found = st.cards.filter((x) => words.every((w) => x._blob.includes(w))); hits = new Set(found.map((x) => x.ref)); return null; } const res = mini.search(q); hits = new Set(res.map((r) => r.id)); return new Map(res.map((r) => [r.id, r.score])); } const terms = () => (st.q.trim() ? st.q.trim().toLowerCase().split(/\s+/).filter((t) => t.length > 1) : []); function highlight(text) { const t = terms(); if (!t.length) return esc(text); const re = new RegExp(`(${t.map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "gi"); return esc(text).replace(re, "$1"); } const label = (key) => st.facets.find((f) => f.key === key)?.label || key; const order = (key) => st.facets.find((f) => f.key === key)?.order || "count"; // a facet shows when its scope allows: everywhere, with no tile picked ("*"), or with one of its tiles picked const inScope = (f) => !f.scope || (st.tile == null ? f.scope.includes("*") : f.scope.includes(st.tile)); function counts(cards, key) { const c = new Map(); cards.forEach((x) => (x.facets[key] || []).forEach((v) => c.set(v, (c.get(v) || 0) + 1))); return [...c.entries()].sort(order(key) === "level" ? (a, b) => level(a[0]) - level(b[0]) || String(a[0]).localeCompare(String(b[0])) : (a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0]))); } function passes(x, skipKey, ignoreTile = false) { if (!ignoreTile && st.tile != null && !(x.facets[st.tileKey] || []).includes(st.tile)) return false; if (hits && !hits.has(x.ref)) return false; for (const [k, set] of Object.entries(st.sel)) { if (k === skipKey || !set.size) continue; if (!(x.facets[k] || []).some((v) => set.has(v))) return false; } return true; } // one pass: the matches (inline) or the page (paged), then everything that shows them let loading = 0, autoLoaded = 0; async function run() { const nf = Object.values(st.sel).reduce((n, s) => n + s.size, 0); $("#nfilt", root).textContent = nf ? ` (${nf})` : ""; writeURL(); if (st.sum.inline) { const visible = new Set(st.facets.filter(inScope).map((f) => f.key)); Object.keys(st.sel).forEach((k) => { if (!visible.has(k)) delete st.sel[k]; }); // a facet of a tile no longer picked const scores = search(); matches = st.cards.filter((x) => passes(x)); if (scores) matches.sort((a, b) => (scores.get(b.ref) ?? 0) - (scores.get(a.ref) ?? 0)); $("#order-note", root).innerHTML = scores ? "best match first" : st.sum.order === "shuffle" ? `${icon("shuffle", 13)}shuffled each visit` : ""; renderTiles(); renderFacets(); renderActive(matches.length); renderMap(); renderList(true); return; } renderFacets(); const token = ++loading, box = $("#list", root); box.hidden = false; $("#empty", root).hidden = true; box.innerHTML = sk.cards(6); const p = new URLSearchParams({ offset: st.offset }); if (st.subset) p.set("subset", st.subset); if (st.q) p.set("q", st.q); const sel = Object.fromEntries(Object.entries(st.sel).filter(([, s]) => s.size).map(([k, s]) => [k, [...s]])); if (Object.keys(sel).length) p.set("f", JSON.stringify(sel)); let r; const slow = setTimeout(() => { if (token === loading && root) $("#note", root).innerHTML = `
    ${spinner("xs")}Still working: the first search or filter of a dataset can take up to a minute while its rows are read. After that it's quick.
    `; }, 4000); try { r = await api(`${envApi(st.spec)}/tasks?${p}`, { retry: 1 }); } catch (e) { clearTimeout(slow); if (token !== loading || !root) return; $("#note", root).innerHTML = ""; box.innerHTML = `
  1. ${e.status === 409 ? `
    ${spinner("xs")}${esc(e.message)}
    ` : `
    ${icon("alert")}${esc(e.message)}
    `}
  2. `; if (e.status === 409) setTimeout(() => token === loading && root && run(), 8000); return; } clearTimeout(slow); if (token !== loading || !root) return; st.page = r; $("#note", root).innerHTML = r.note ? `
    ${icon("info")}${esc(r.note)}
    ` : ""; renderActive(r.total, r.total == null ? r.cards.length : null); $("#range", root).textContent = r.cards.length ? `${fmt.format(r.offset + 1)}–${fmt.format(r.offset + r.cards.length)}` : ""; $("#prev", root).disabled = r.offset === 0; $("#next", root).disabled = r.cards.length < r.page || (r.total != null && r.offset + r.page >= r.total); box.innerHTML = r.cards.map(card).join(""); box.hidden = !r.cards.length; $("#empty", root).hidden = r.cards.length > 0; } function renderTiles() { const el = $("#tiles", root); if (!el) return; const all = st.cards, key = st.tileKey, t = st.sum.tiles || {}; const filtered = hits || Object.keys(st.sel).length; const values = st.tileMeta.size ? [...st.tileMeta.keys()] : counts(all, key).map(([v]) => v); const tiles = [null, ...values]; const color = (v) => (v != null && st.tileMeta.get(v)?.color ? `var(--c-${st.tileMeta.get(v).color})` : st.sum.collection ? `var(--c-${st.sum.collection.color || st.sum.collection.id})` : "var(--text)"); el.style.gridTemplateColumns = `repeat(${Math.min(6, tiles.length)}, minmax(0, 1fr))`; el.innerHTML = tiles.map((v) => { const meta = v == null ? null : st.tileMeta.get(v); const mine = v == null ? all : all.filter((x) => (x.facets[key] || []).includes(v)); const hit = filtered ? mine.filter((x) => passes(x, null, true)).length : null; const graded = [...new Set(mine.flatMap((x) => x.facets.graded || []))].map((g) => g.toLowerCase()); const note = v == null ? (t.all_note || `${label(key)} below`) : meta?.note || (graded.length ? `Graded by ${graded.join(", ")}` : ""); return ``; }).join(""); } function renderFacets() { const inline = st.sum.inline; const list = st.facets.filter(inScope); $("#facets", root).innerHTML = list.map((f) => { const key = f.key; let items; if (inline) { const pool = st.cards.filter((x) => passes(x, key)); items = counts(pool, key); // a value every task carries (the dataset's own tag, say) tells nothing apart if (key === "tags") items = items.filter(([v, c]) => c < pool.length || st.sel[key]?.has(v)); } else { items = (f.values || []).map(([v, c]) => [String(v), c]); if (f.order === "level") items.sort((a, b) => level(a[0]) - level(b[0]) || a[0].localeCompare(b[0])); } const sel = st.sel[key] || new Set(); sel.forEach((v) => { if (!items.some(([x]) => x === v)) items.push([v, 0]); }); if (items.length <= 1 && !sel.size) return ""; const limit = expanded.has(key) ? Infinity : matchMedia("(max-width: 760px)").matches ? 5 : 8; const hidden = Math.max(0, items.length - limit); const open = facetOpen.has(key) ? facetOpen.get(key) : (!matchMedia("(max-width: 760px)").matches || sel.size > 0); return `

    ${esc(f.label)}${f.sampled ? ' ≈' : ""}

    ${sel.size ? `${sel.size} selected` : ""}${icon("chevronDown", 14, "chev")}
    ${items.slice(0, limit).map(([v, c]) => ``).join("")} ${hidden || expanded.has(key) ? `` : ""}
    `; }).join("") || `

    ${st.facets.length ? "Every one has the same values here: nothing to filter by." : "This dataset has nothing to filter by."}

    `; } function toggle(key, v) { if (key === st.tileKey) { st.tile = st.tile === v ? null : v; st.sel = {}; run(); return; } // a tile's facet is the tile const set = (st.sel[key] ||= new Set()); set.has(v) ? set.delete(v) : set.add(v); if (!set.size) delete st.sel[key]; st.offset = 0; run(); } function renderActive(n, atLeast = null) { const pills = []; if (st.tile != null) pills.push(``); Object.entries(st.sel).forEach(([k, set]) => set.forEach((v) => pills.push(``))); $("#active", root).innerHTML = pills.join(""); const filtered = st.q || pills.length; const [one, many] = st.noun; $("#count", root).innerHTML = n == null ? `${fmt.format(atLeast || 0)}+ ${esc(many)} (still counting)` : `${fmt.format(n)} ${filtered && !st.sum.inline ? "matching " : ""}${esc(n === 1 ? one : many)}`; const sr = $("#show-results", root); if (sr) sr.textContent = n == null ? "Show results" : `Show ${fmt.format(n)} ${n === 1 ? one : many}`; } // ── map ────────────────────────────────────────────────────────────────────── // By one facet; or, with per-tile facets and no tile picked, each tile's top values side by side in its colour. const LEFTOVER = /^(Other|Unknown|unknown|None named|Unspecified|Unrated)$/; const cssVar = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim(); function mapData() { const top = (sorted, n) => { const head = sorted.slice(0, n), rest = sorted.slice(n).reduce((a, [, c]) => a + c, 0); const kids = head.map(([name, value], rank) => ({ name, value, rank, other: LEFTOVER.test(name) })); if (rest) kids.push({ name: `${sorted.length - head.length} more`, value: rest, rank: head.length, other: true, rest: true }); return kids; }; if (st.mapBy) { if (st.tile != null) { const key = st.mapBy.by_tile[st.tile]; return { key, title: `${st.tile}, by ${label(key).toLowerCase()}`, children: [{ name: st.tile, tile: st.tile, key, children: top(counts(st.cards.filter((x) => passes(x, key)), key), 40) }] }; } const children = Object.entries(st.mapBy.by_tile).map(([tile, key]) => { const pool = st.cards.filter((x) => (x.facets[st.tileKey] || []).includes(tile) && passes(x)); return { name: tile, tile, key, children: top(counts(pool, key), st.mapBy.top?.[tile] || 9).map((k) => ({ ...k, tile, key })) }; }).filter((d) => d.children.length); return { title: `${st.noun[1][0].toUpperCase()}${st.noun[1].slice(1)}, by ${label(st.tileKey).toLowerCase()}`, children }; } const key = st.mapKey; return { key, title: `${st.noun[1][0].toUpperCase()}${st.noun[1].slice(1)}, by ${label(key).toLowerCase()}`, children: [{ name: "all", key, children: top(counts(st.cards.filter((x) => passes(x, key)), key), 14) }] }; } function renderMap() { const el = root && $("#map", root); if (!el || !window.d3) return; const W = el.clientWidth, H = el.clientHeight; if (!W || !H) return; const data = mapData(); $("#map-title", root).textContent = data.title; el.innerHTML = ""; const leaves = data.children.reduce((n, g) => n + g.children.length, 0); if (!leaves) { el.innerHTML = `

    Nothing to map.

    `; return; } const h = d3.hierarchy({ name: "all", children: data.children }).sum((d) => (d.children ? 0 : d.value || 0)) .sort((a, b) => (!!a.data.rest - !!b.data.rest) || b.value - a.value); d3.treemap().size([W, H]).paddingInner(2).paddingOuter(data.children.length > 1 ? 1 : 0).round(true).tile(d3.treemapSquarify.ratio(1.35))(h); const surface = cssVar("--surface"); const base = (tile) => cssVar(tile && st.tileMeta.get(tile)?.color ? `--c-${st.tileMeta.get(tile).color}` : st.sum.collection ? `--c-${st.sum.collection.color || st.sum.collection.id}` : "--c-hub"); const svg = d3.select(el).append("svg").attr("viewBox", `0 0 ${W} ${H}`).attr("role", "img").attr("aria-label", data.title); const g = svg.selectAll("g.leaf").data(h.leaves()).join("g") .attr("class", (d) => { const sel = st.sel[d.data.key || d.parent.data.key]; return "cell leaf" + (sel?.size && !sel.has(d.data.name) ? " dim" : ""); }) .attr("transform", (d) => `translate(${d.x0},${d.y0})`); const n = (d) => d.parent.children.length; g.append("rect").attr("width", (d) => Math.max(0, d.x1 - d.x0)).attr("height", (d) => Math.max(0, d.y1 - d.y0)).attr("rx", 4) .attr("fill", (d) => d3.interpolateRgb(surface, base(d.data.tile || d.parent.data.tile))(d.data.other ? 0.42 : 0.95 - 0.38 * (d.data.rank / Math.max(n(d) - 1, 6)))) .on("click", (_, d) => { tip(); if (d.data.rest) return; const tile = d.data.tile || d.parent.data.tile; if (st.mapBy && st.tile == null && tile) st.tile = tile; // a block of one tile opens that tile, filtered to it toggle(d.data.key || d.parent.data.key, d.data.name); }) .on("mousemove", (ev, d) => tip(ev, `${esc(d.data.name)}${d.data.tile && st.tile == null ? ` ${esc(d.data.tile)}` : ""}
    ${fmt.format(d.value)} ${esc(st.noun[1])}`)) .on("mouseleave", () => tip()); g.each(function (d) { const w = d.x1 - d.x0, hh = d.y1 - d.y0; if (w < 44 || hh < 26) return; const color = d3.lab(this.querySelector("rect").getAttribute("fill")).l > 64 ? "#17181c" : "#ffffff"; const max = Math.floor((w - 14) / 6.6); const text = d.data.name.length > max ? d.data.name.slice(0, Math.max(1, max - 1)) + "…" : d.data.name; const t = d3.select(this); t.append("text").attr("class", "lbl").attr("x", 8).attr("y", 18).attr("fill", color).text(text); if (hh > 42) t.append("text").attr("class", "cnt").attr("x", 8).attr("y", 34).attr("fill", color).text(fmt.format(d.value)); }); } function tip(ev, html) { const el = $("#tip"); if (!ev) { el.hidden = true; return; } el.innerHTML = html; el.hidden = false; el.style.left = Math.min(ev.clientX + 14, innerWidth - el.offsetWidth - 8) + "px"; el.style.top = ev.clientY + 16 + "px"; } // ── list ───────────────────────────────────────────────────────────────────── function card(x) { const color = x.color ? `var(--c-${x.color})` : st.sum.collection ? `var(--c-${st.sum.collection.color || st.sum.collection.id})` : "var(--c-hub)"; const chips = x.chips.filter((c) => !st.shared?.has(c) && c !== x.lead); return `
  3. ${x.lead ? `${x.icon ? icon(x.icon, 13) : ""}${esc(x.lead)}` : ""}${x.sub.map((a) => `/${esc(a)}`).join("")} ${x.id || !x.lead ? `${esc(x.id || `#${x.ref.split("/").pop()}`)}` : ""}

    ${highlight(x.title)}

    ${x.brief ? `

    ${highlight(x.brief)}

    ` : ""} ${chips.length || x.stats?.length ? `
    ${chips.slice(0, 6).map((c) => `${esc(c)}`).join("")}${(x.stats || []).map(([n, l]) => `${esc(n)} ${esc(l)}`).join("")}
    ` : ""}
  4. `; } const AUTO_PAGES = 3; // pages that load by themselves as you scroll; after that a button, so the footer stays reachable function renderList(reset) { const list = $("#list", root); if (reset) { list.innerHTML = ""; shown = 0; autoLoaded = 0; } const next = matches.slice(shown, shown + PAGE); list.insertAdjacentHTML("beforeend", next.map(card).join("")); typeset(list); shown += next.length; $("#empty", root).hidden = matches.length > 0; list.hidden = !matches.length; const left = matches.length - shown; $("#more-row", root).hidden = left <= 0 || autoLoaded < AUTO_PAGES; $("#more-count", root).textContent = `Showing ${fmt.format(shown)} of ${fmt.format(matches.length)}`; $("#more", root).textContent = `Show ${fmt.format(Math.min(PAGE, left))} more`; } let scrollWatch = null; function wire(el) { let t; $("#q", el).addEventListener("input", (ev) => { clearTimeout(t); t = setTimeout(() => { st.q = ev.target.value.trim(); st.offset = 0; run(); }, st.sum.inline ? 120 : 350); }); $("#subset", el)?.addEventListener("change", (ev) => { st.subset = ev.target.value; st.offset = 0; st.sel = {}; st.q = ""; writeURL(); load(el); // its own facets and total }); $("#tiles", el)?.addEventListener("click", (ev) => { const b = ev.target.closest(".dom"); if (b) { st.tile = b.dataset.tile || null; st.sel = {}; run(); } }); $("#facets", el).addEventListener("click", (ev) => { const o = ev.target.closest(".opt"); if (o) return toggle(o.dataset.k, o.dataset.v); const m = ev.target.closest("[data-more]"); if (m) { expanded.has(m.dataset.more) ? expanded.delete(m.dataset.more) : expanded.add(m.dataset.more); renderFacets(); } }); $("#facets", el).addEventListener("toggle", (e) => { const d = e.target.closest?.("[data-facet]"); if (d) facetOpen.set(d.dataset.facet, d.open); }, true); $("#active", el).addEventListener("click", (ev) => { const p = ev.target.closest(".pill"); if (!p) return; if (p.dataset.tile != null) { st.tile = null; st.sel = {}; run(); } else toggle(p.dataset.k, p.dataset.v); }); const clearAll = () => { st.sel = {}; st.q = ""; st.tile = null; st.offset = 0; $("#q", el).value = ""; run(); }; $("#clear", el).addEventListener("click", clearAll); $("#clear2", el).addEventListener("click", clearAll); const sheet = (on) => { $("#filters", el).classList.toggle("open", on); $("#sheet-scrim", el).hidden = !on; document.body.classList.toggle("noscroll", on); }; $("#open-filters", el)?.addEventListener("click", () => sheet(true)); $("#close-filters", el).addEventListener("click", () => sheet(false)); $("#sheet-scrim", el).addEventListener("click", () => sheet(false)); $("#show-results", el).addEventListener("click", () => { sheet(false); $("#toolbar", el).scrollIntoView({ block: "start" }); }); $("#clear3", el).addEventListener("click", () => { st.sel = {}; st.tile = null; st.offset = 0; run(); }); $("#map-card", el)?.addEventListener("toggle", () => renderMap()); $("#more", el).addEventListener("click", () => renderList(false)); const page = (d) => { st.offset = Math.max(0, st.offset + d * (st.page?.page || 24)); run(); $("#toolbar", el).scrollIntoView({ block: "start" }); }; $("#prev", el)?.addEventListener("click", () => page(-1)); $("#next", el)?.addEventListener("click", () => page(1)); $("#random", el).addEventListener("click", async () => { if (st.sum.inline) { const pool = matches.length ? matches : st.cards; nav(taskHref(st.spec, pool[Math.floor(Math.random() * pool.length)].ref)); return; } try { const r = await api(`${envApi(st.spec)}/random${st.subset ? `?subset=${encodeURIComponent(st.subset)}` : ""}`); nav(taskHref(st.spec, r.ref)); } catch (e) { toast(e.message); } }); // the next page loads by itself as the end of the list comes into view, a few times scrollWatch?.disconnect(); if (st.sum.inline) { scrollWatch = new IntersectionObserver((es) => { if (!es.some((e) => e.isIntersecting) || !root || shown >= matches.length || autoLoaded >= AUTO_PAGES) return; autoLoaded++; renderList(false); }, { rootMargin: "600px 0px" }); scrollWatch.observe($("#sentinel", el)); } } let rt; // one listener for the page's lifetime: the map redraws at its new size addEventListener("resize", () => { clearTimeout(rt); rt = setTimeout(() => root && st.cards && (st.mapKey || st.mapBy) && renderMap(), 150); });