benchmark-race / index.html
davanstrien's picture
davanstrien HF Staff
Upload index.html with huggingface_hub
2675fc3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Benchmark Leaderboard Race</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Inter, system-ui, -apple-system, sans-serif;
background: #fafafa;
color: #1a1a2e;
}
header {
max-width: 1100px;
margin: 0 auto;
padding: 24px 24px 0;
}
header h1 {
font-size: 24px;
font-weight: 700;
margin-bottom: 4px;
}
header p {
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
header a { color: #6366f1; text-decoration: none; }
header a:hover { text-decoration: underline; }
#controls {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px 16px;
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
#controls label {
font-size: 13px;
font-weight: 500;
color: #555;
}
#benchmark-select {
font-family: inherit;
font-size: 14px;
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
cursor: pointer;
}
.btn {
font-family: inherit;
font-size: 14px;
font-weight: 600;
padding: 7px 18px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
#play-btn {
background: #6366f1;
color: white;
}
#play-btn:hover { background: #4f46e5; }
#reset-btn {
background: #e5e7eb;
color: #374151;
}
#reset-btn:hover { background: #d1d5db; }
.speed-group {
display: flex;
align-items: center;
gap: 8px;
}
#speed-slider {
width: 120px;
accent-color: #6366f1;
}
#speed-label {
font-size: 12px;
color: #888;
min-width: 40px;
}
#frame-info {
font-size: 12px;
color: #aaa;
margin-left: auto;
}
#chart-container {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px 16px;
}
svg {
width: 100%;
height: auto;
display: block;
}
.bar-name {
font-weight: 600;
fill: white;
pointer-events: none;
}
.bar-name-link {
cursor: default;
pointer-events: none;
}
.clickable .bar-name-link {
cursor: pointer;
pointer-events: auto;
}
.clickable .bar-name-link:hover text {
text-decoration: underline;
fill: #eef;
}
.bar-score {
font-weight: 500;
fill: #333;
pointer-events: none;
}
.date-watermark {
fill: rgba(0, 0, 0, 0.06);
font-size: 64px;
font-weight: 700;
pointer-events: none;
font-family: Inter, system-ui, sans-serif;
}
.axis text {
font-size: 11px;
fill: #888;
}
.axis .domain { stroke: #ddd; }
.axis .tick line { stroke: #eee; }
</style>
</head>
<body>
<header>
<h1>Benchmark Leaderboard Race</h1>
<p>Watch how AI model rankings evolve over time. Scores from
<a href="https://huggingface.co/datasets?benchmark=benchmark:official&sort=trending">HuggingFace Dataset Leaderboards</a>,
release dates from model repo metadata.</p>
</header>
<div id="controls">
<label for="benchmark-select">Benchmark:</label>
<select id="benchmark-select"></select>
<button class="btn" id="play-btn">&#9654; Play</button>
<button class="btn" id="reset-btn">&#8634; Reset</button>
<div class="speed-group">
<label for="speed-slider">Speed:</label>
<input type="range" id="speed-slider" min="100" max="2000" value="600" step="50">
<span id="speed-label">600ms</span>
</div>
<span id="frame-info"></span>
</div>
<div id="chart-container">
<svg id="chart"></svg>
</div>
<script>
(async function () {
// ---- Load data ----
const raw = await d3.json("data.json");
const logos = raw.logos || {};
const colors = raw.colors || {};
// ---- Config ----
const topN = 15;
const margin = { top: 30, right: 90, bottom: 10, left: 6 };
const width = 1050;
// Fit all bars in viewport: subtract header/controls (~140px) from window height
const availableHeight = Math.max(400, window.innerHeight - 160);
const barHeight = Math.min(38, Math.floor((availableHeight - margin.top - margin.bottom) / topN));
const height = margin.top + barHeight * topN + margin.bottom;
// ---- Populate dropdown ----
const select = d3.select("#benchmark-select");
const benchmarkKeys = Object.keys(raw.benchmarks);
select.selectAll("option")
.data(benchmarkKeys)
.join("option")
.attr("value", d => d)
.text(d => raw.benchmarks[d].name);
// Default to sweVerified if available
if (benchmarkKeys.includes("sweVerified")) {
select.property("value", "sweVerified");
}
// ---- SVG setup ----
const svg = d3.select("#chart")
.attr("viewBox", [0, 0, width, height]);
const x = d3.scaleLinear().range([margin.left, width - margin.right]);
const y = d3.scaleBand()
.domain(d3.range(topN))
.range([margin.top, margin.top + barHeight * topN])
.padding(0.12);
// Safe y position — returns off-screen bottom for ranks >= topN
function yPos(rank) {
if (rank >= topN) return margin.top + barHeight * topN;
return y(rank) ?? margin.top + barHeight * topN;
}
// Groups (layered bottom to top)
const watermarkG = svg.append("g");
const axisG = svg.append("g").attr("class", "axis");
const barG = svg.append("g");
const logoG = svg.append("g");
const nameG = svg.append("g");
const scoreG = svg.append("g");
// Dynamic sizes based on bar height
const nameFontSize = Math.max(9, Math.min(13, barHeight * 0.36));
const scoreFontSize = Math.max(8, Math.min(12, barHeight * 0.33));
const watermarkSize = Math.max(36, Math.min(64, height * 0.11));
// Date watermark
const ticker = watermarkG.append("text")
.attr("class", "date-watermark")
.style("font-size", watermarkSize + "px")
.attr("x", width - margin.right - 10)
.attr("y", height - 20)
.attr("text-anchor", "end");
// ---- Build keyframes for a benchmark ----
function buildKeyframes(benchmarkKey) {
const bm = raw.benchmarks[benchmarkKey];
if (!bm) return [];
const models = bm.models
.map(m => ({ ...m, dateObj: new Date(m.date) }))
.sort((a, b) => a.dateObj - b.dateObj);
const dateSet = [...new Set(models.map(m => m.date))];
// dateSet is already sorted because models were sorted
const keyframes = [];
const pool = new Map(); // model_id -> best entry
for (const dateStr of dateSet) {
const newModels = models.filter(m => m.date === dateStr);
for (const m of newModels) {
if (!pool.has(m.model_id) || m.score > pool.get(m.model_id).score) {
pool.set(m.model_id, m);
}
}
const ranked = [...pool.values()]
.sort((a, b) => b.score - a.score)
.slice(0, topN)
.map((m, i) => ({ ...m, rank: i }));
keyframes.push({ date: dateStr, ranked });
}
return keyframes;
}
// ---- State ----
let keyframes = [];
let currentFrame = 0;
let playing = false;
let frameDuration = 600;
function getPrev(frameIdx) {
if (frameIdx <= 0) return new Map();
const map = new Map();
for (const m of keyframes[frameIdx - 1].ranked) {
map.set(m.model_id, m);
}
return map;
}
function getNext(frameIdx) {
if (frameIdx >= keyframes.length - 1) return new Map();
const map = new Map();
for (const m of keyframes[frameIdx + 1].ranked) {
map.set(m.model_id, m);
}
return map;
}
// ---- Render a single frame ----
function renderFrame(frameIdx, duration) {
const kf = keyframes[frameIdx];
if (!kf) return Promise.resolve();
const { date, ranked } = kf;
const prevMap = getPrev(frameIdx);
const nextMap = getNext(frameIdx);
const bw = y.bandwidth();
// Update x scale
const maxScore = d3.max(ranked, d => d.score) || 1;
x.domain([0, maxScore * 1.12]);
// Axis
const t = svg.transition().duration(duration).ease(d3.easeLinear);
axisG.transition(t)
.call(d3.axisTop(x).ticks(6).tickSize(-(height - margin.top - margin.bottom)))
.attr("transform", `translate(0,${margin.top})`)
.call(g => g.select(".domain").remove());
// Date watermark
const dateObj = new Date(date);
const dateLabel = dateObj.toLocaleDateString("en-US", {
year: "numeric", month: "short", day: "numeric"
});
ticker.text(dateLabel);
// Frame info
d3.select("#frame-info").text(`${frameIdx + 1} / ${keyframes.length}`);
// Helper: get enter y (from previous frame or off-screen)
function enterY(d) {
const p = prevMap.get(d.model_id);
return yPos(p ? p.rank : topN);
}
// Helper: get exit y (to next frame or off-screen)
function exitY(d) {
const n = nextMap.get(d.model_id);
return yPos(n ? n.rank : topN);
}
// ---- Bars ----
const bars = barG.selectAll("rect").data(ranked, d => d.model_id);
bars.join(
enter => enter.append("rect")
.attr("fill", d => colors[d.provider] || "#999")
.attr("rx", 3)
.attr("height", bw)
.attr("x", x(0))
.attr("y", d => enterY(d))
.attr("width", d => {
const p = prevMap.get(d.model_id);
return Math.max(0, x(p ? p.score : 0) - x(0));
})
.call(enter => enter.transition(t)
.attr("y", d => yPos(d.rank))
.attr("width", d => Math.max(0, x(d.score) - x(0)))
),
update => update.call(update => update.transition(t)
.attr("y", d => yPos(d.rank))
.attr("width", d => Math.max(0, x(d.score) - x(0)))
),
exit => exit.call(exit => exit.transition(t)
.attr("y", d => exitY(d))
.attr("width", d => {
const n = nextMap.get(d.model_id);
return Math.max(0, x(n ? n.score : 0) - x(0));
})
.remove()
)
);
// ---- Model name labels (inside bar, wrapped in <a> for clickability) ----
const names = nameG.selectAll("a").data(ranked, d => d.model_id);
names.join(
enter => {
const a = enter.append("a")
.attr("class", "bar-name-link")
.attr("href", d => `https://huggingface.co/${d.model_id}`)
.attr("target", "_blank");
a.append("text")
.attr("class", "bar-name")
.style("font-size", nameFontSize + "px")
.attr("x", x(0) + 8)
.attr("y", d => enterY(d) + bw / 2)
.attr("dy", "0.35em")
.text(d => d.short_name);
a.select("text").call(enter => enter.transition(t)
.attr("y", d => yPos(d.rank) + bw / 2)
);
return a;
},
update => update.call(update => {
update.attr("href", d => `https://huggingface.co/${d.model_id}`);
update.select("text").transition(t)
.attr("y", d => yPos(d.rank) + bw / 2);
}),
exit => exit.call(exit => {
exit.select("text").transition(t)
.attr("y", d => exitY(d) + bw / 2);
exit.transition(t).remove();
})
);
// ---- Score labels (at end of bar) ----
const scores = scoreG.selectAll("text").data(ranked, d => d.model_id);
scores.join(
enter => enter.append("text")
.attr("class", "bar-score")
.style("font-size", scoreFontSize + "px")
.attr("x", d => {
const p = prevMap.get(d.model_id);
return x(p ? p.score : 0) + 4;
})
.attr("y", d => enterY(d) + bw / 2)
.attr("dy", "0.35em")
.text(d => d.score.toFixed(1))
.call(enter => enter.transition(t)
.attr("x", d => x(d.score) + 4)
.attr("y", d => yPos(d.rank) + bw / 2)
.tween("text", function (d) {
const p = prevMap.get(d.model_id);
const i = d3.interpolateNumber(p ? p.score : 0, d.score);
return function (t) { this.textContent = i(t).toFixed(1); };
})
),
update => update.call(update => update.transition(t)
.attr("x", d => x(d.score) + 4)
.attr("y", d => yPos(d.rank) + bw / 2)
.tween("text", function (d) {
const prevScore = parseFloat(this.textContent) || 0;
const i = d3.interpolateNumber(prevScore, d.score);
return function (t) { this.textContent = i(t).toFixed(1); };
})
),
exit => exit.call(exit => exit.transition(t)
.attr("x", d => {
const n = nextMap.get(d.model_id);
return x(n ? n.score : 0) + 4;
})
.attr("y", d => exitY(d) + bw / 2)
.remove()
)
);
// ---- Provider logos (at end of bar, after score) ----
const logoSize = bw * 0.65;
const logosData = ranked.filter(d => logos[d.provider]);
const logoEls = logoG.selectAll("image").data(logosData, d => d.model_id);
logoEls.join(
enter => enter.append("image")
.attr("href", d => logos[d.provider])
.attr("width", logoSize)
.attr("height", logoSize)
.attr("x", d => {
const p = prevMap.get(d.model_id);
return x(p ? p.score : 0) + 40;
})
.attr("y", d => enterY(d) + (bw - logoSize) / 2)
.call(enter => enter.transition(t)
.attr("x", d => x(d.score) + 40)
.attr("y", d => yPos(d.rank) + (bw - logoSize) / 2)
),
update => update.call(update => update.transition(t)
.attr("x", d => x(d.score) + 40)
.attr("y", d => yPos(d.rank) + (bw - logoSize) / 2)
),
exit => exit.call(exit => exit.transition(t)
.attr("x", d => {
const n = nextMap.get(d.model_id);
return x(n ? n.score : 0) + 40;
})
.attr("y", d => exitY(d) + (bw - logoSize) / 2)
.remove()
)
);
return t.end().catch(() => {});
}
// ---- Animation ----
function setClickable(enabled) {
nameG.classed("clickable", enabled);
}
async function play() {
if (playing) return;
playing = true;
setClickable(false);
d3.select("#play-btn").html("&#9646;&#9646; Pause");
for (let i = currentFrame; i < keyframes.length; i++) {
if (!playing) break;
currentFrame = i;
await renderFrame(i, frameDuration);
}
playing = false;
setClickable(true);
d3.select("#play-btn").html("&#9654; Play");
}
function pause() {
playing = false;
d3.select("#play-btn").html("&#9654; Play");
}
function clearChart() {
barG.selectAll("*").remove();
nameG.selectAll("*").remove();
scoreG.selectAll("*").remove();
logoG.selectAll("*").remove();
ticker.text("");
setClickable(false);
}
function reset() {
pause();
currentFrame = 0;
clearChart();
d3.select("#frame-info").text("");
if (keyframes.length > 0) {
renderFrame(0, 0);
}
}
function loadBenchmark(key) {
pause();
keyframes = buildKeyframes(key);
currentFrame = 0;
clearChart();
if (keyframes.length > 0) {
renderFrame(0, 0);
}
d3.select("#frame-info").text(keyframes.length > 0 ? `1 / ${keyframes.length}` : "No data");
}
// ---- Event listeners ----
d3.select("#play-btn").on("click", () => {
if (playing) {
pause();
} else {
if (currentFrame >= keyframes.length - 1) {
currentFrame = 0;
}
play();
}
});
d3.select("#reset-btn").on("click", reset);
d3.select("#benchmark-select").on("change", function () {
loadBenchmark(this.value);
});
d3.select("#speed-slider").on("input", function () {
frameDuration = +this.value;
d3.select("#speed-label").text(this.value + "ms");
});
// ---- Initialize: load default benchmark and auto-play ----
const defaultKey = benchmarkKeys.includes("sweVerified") ? "sweVerified" : benchmarkKeys[0];
if (defaultKey) {
loadBenchmark(defaultKey);
// Auto-start animation after a brief delay for the page to settle
setTimeout(() => play(), 400);
}
})();
</script>
</body>
</html>