// Leaderboard: interactive heatmap table with sorting and filtering. // Depends on BENCHMARK_DATA, GAMES, MODALITIES, TIERS from leaderboard-data.js. (function () { "use strict"; // ---- State ---- let activeFilters = { tier: "all", game: "all", modality: "all" }; let sortCol = "avg"; // column key or "avg" let sortAsc = false; // descending by default // ---- Color scale (light theme, bolder) ---- function heatColor(val, min, max) { if (val === null || val === undefined) return null; const range = max - min; const t = range > 0 ? (val - min) / range : 0.5; let r, g, b; if (t < 0.5) { const s = t / 0.5; r = 220 + (240 - 220) * s; g = 130 + (210 - 130) * s; b = 120 + (130 - 120) * s; } else { const s = (t - 0.5) / 0.5; r = 240 - (240 - 130) * s; g = 210 - (210 - 195) * s; b = 130 - (130 - 100) * s; } return `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`; } function textColorForBg() { return "#1a1a1a"; } // ---- Compute visible columns ---- function getVisibleColumns() { const cols = []; const games = activeFilters.game === "all" ? GAMES : [activeFilters.game]; const mods = activeFilters.modality === "all" ? MODALITIES : [activeFilters.modality]; for (const g of games) { for (const m of mods) { cols.push({ game: g, modality: m, key: g + "|" + m }); } } return cols; } // ---- Compute visible rows ---- function getVisibleRows() { const tiers = activeFilters.tier === "all" ? TIERS : [activeFilters.tier]; return BENCHMARK_DATA.filter(d => tiers.includes(d.tier)); } // ---- Compute row average ---- function rowAvg(row, cols) { let sum = 0, n = 0; for (const c of cols) { const v = row.scores[c.key]; if (v !== null && v !== undefined) { sum += v; n++; } } return n > 0 ? sum / n : null; } // ---- Build and render table ---- function renderLeaderboard() { const cols = getVisibleColumns(); let rows = getVisibleRows(); // Pre-compute averages const avgMap = new Map(); for (const r of rows) avgMap.set(r, rowAvg(r, cols)); // Sort: always group by tier first, then by selected column within each tier rows = rows.slice().sort((a, b) => { const tierA = TIERS.indexOf(a.tier); const tierB = TIERS.indexOf(b.tier); if (tierA !== tierB) return tierA - tierB; if (sortCol) { let va, vb; if (sortCol === "avg") { va = avgMap.get(a); vb = avgMap.get(b); } else { va = a.scores[sortCol]; vb = b.scores[sortCol]; } if (va === null || va === undefined) va = -Infinity; if (vb === null || vb === undefined) vb = -Infinity; return sortAsc ? va - vb : vb - va; } return 0; }); // Find min/max for heatmap scaling let allVals = []; for (const r of rows) { for (const c of cols) { const v = r.scores[c.key]; if (v !== null && v !== undefined) allVals.push(v); } const avg = avgMap.get(r); if (avg !== null) allVals.push(avg); } const minVal = allVals.length > 0 ? Math.min(...allVals) : 0; const maxVal = allVals.length > 0 ? Math.max(...allVals) : 1; // --- Build header --- const thead = document.getElementById("leaderboard-thead"); thead.innerHTML = ""; // Game header row (only if showing multiple modalities per game) const showGameRow = activeFilters.modality === "all"; if (showGameRow) { const gameRow = document.createElement("tr"); gameRow.className = "game-header"; // Model corner const corner = document.createElement("th"); corner.textContent = ""; corner.rowSpan = 2; gameRow.appendChild(corner); // Avg column header (left-most after model) const avgTh = document.createElement("th"); avgTh.className = "avg-col"; avgTh.rowSpan = 2; avgTh.style.cursor = "pointer"; avgTh.onclick = () => { toggleSort("avg"); }; avgTh.innerHTML = 'Avg ' + sortIndicator("avg"); gameRow.appendChild(avgTh); const visibleGames = activeFilters.game === "all" ? GAMES : [activeFilters.game]; for (const g of visibleGames) { const th = document.createElement("th"); th.textContent = GAME_LABELS[g] || g; th.colSpan = MODALITIES.length; gameRow.appendChild(th); } thead.appendChild(gameRow); } // Modality header row const modRow = document.createElement("tr"); modRow.className = "mod-header"; if (!showGameRow) { const corner = document.createElement("th"); corner.textContent = "Model"; modRow.appendChild(corner); // Avg header (left-most after model) const avgTh = document.createElement("th"); avgTh.className = "avg-col"; avgTh.innerHTML = 'Avg ' + sortIndicator("avg"); avgTh.style.cursor = "pointer"; avgTh.onclick = () => { toggleSort("avg"); }; modRow.appendChild(avgTh); } for (const c of cols) { const th = document.createElement("th"); const label = showGameRow ? c.modality : (GAME_LABELS[c.game] || c.game) + " / " + c.modality; th.innerHTML = label + " " + sortIndicator(c.key); th.onclick = () => { toggleSort(c.key); }; modRow.appendChild(th); } thead.appendChild(modRow); // --- Build body --- const tbody = document.getElementById("leaderboard-tbody"); tbody.innerHTML = ""; const showTierGroups = activeFilters.tier === "all"; let lastTier = null; const totalCols = 2 + cols.length; // model + avg + score columns for (const r of rows) { // Insert tier group header row when tier changes if (showTierGroups && r.tier !== lastTier) { const sepTr = document.createElement("tr"); sepTr.className = "tier-separator"; const sepTd = document.createElement("td"); sepTd.colSpan = totalCols; sepTd.textContent = TIER_LABELS[r.tier] || r.tier; sepTr.appendChild(sepTd); tbody.appendChild(sepTr); lastTier = r.tier; } const tr = document.createElement("tr"); // Model cell const modelTd = document.createElement("td"); modelTd.className = "model-cell"; modelTd.textContent = r.model; tr.appendChild(modelTd); // Avg cell (left-most after model) const avgTd = document.createElement("td"); avgTd.className = "score-cell avg-col"; const avg = avgMap.get(r); if (avg === null) { avgTd.textContent = "\u2014"; avgTd.classList.add("null-cell"); } else { avgTd.textContent = (avg * 100).toFixed(1); const bg = heatColor(avg, minVal, maxVal); if (bg) { avgTd.style.backgroundColor = bg; avgTd.style.color = textColorForBg(); } } tr.appendChild(avgTd); // Score cells for (const c of cols) { const td = document.createElement("td"); td.className = "score-cell"; const v = r.scores[c.key]; if (v === null || v === undefined) { td.textContent = "\u2014"; td.classList.add("null-cell"); } else { td.textContent = (v * 100).toFixed(1); const bg = heatColor(v, minVal, maxVal); if (bg) { td.style.backgroundColor = bg; td.style.color = textColorForBg(); } } tr.appendChild(td); } tbody.appendChild(tr); } } // ---- Sort helpers ---- function sortIndicator(key) { if (sortCol !== key) return '\u2195'; const arrow = sortAsc ? "\u2191" : "\u2193"; return `${arrow}`; } function toggleSort(key) { if (sortCol === key) { sortAsc = !sortAsc; } else { sortCol = key; sortAsc = false; // default descending for scores } renderLeaderboard(); } // ---- Filter button wiring ---- function initFilters() { document.querySelectorAll("#leaderboard-filters .filter-buttons").forEach(group => { const filterType = group.dataset.filter; group.querySelectorAll(".filter-btn").forEach(btn => { btn.addEventListener("click", () => { group.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active")); btn.classList.add("active"); activeFilters[filterType] = btn.dataset.value; sortCol = null; // reset sort on filter change renderLeaderboard(); }); }); }); } // ---- Public init ---- window.initLeaderboard = function () { initFilters(); renderLeaderboard(); }; })();