// 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();
};
})();