Spaces:
Running
Running
| // 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 '<span class="sort-arrow">\u2195</span>'; | |
| const arrow = sortAsc ? "\u2191" : "\u2193"; | |
| return `<span class="sort-arrow active">${arrow}</span>`; | |
| } | |
| 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(); | |
| }; | |
| })(); | |