Files
FPGABench/app.js
2026-06-13 15:59:39 +01:00

698 lines
22 KiB
JavaScript

// FPGABench Model Dataset
// Metrics:
// - feasible: Feasible@1 (adapted pass@1) as a decimal fraction (0.0 to 1.0)
// - moq: MOQ@1 (mean design quality index) normalized around 1.0 (higher is better)
const modelsData = [
{
id: "gpt-5",
name: "GPT-5",
developer: "OpenAI",
type: "Proprietary",
feasible: 25 / 52,
moq: 1.15,
lutCount: 2180,
frequency: 212.5,
power: 0.24,
link: "https://openai.com"
},
{
id: "gemini-3-pro",
name: "Gemini 3 Pro",
developer: "Google",
type: "Proprietary",
feasible: 22 / 52,
moq: 1.05,
lutCount: 2250,
frequency: 205.0,
power: 0.26,
link: "https://deepmind.google"
},
{
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5",
developer: "Anthropic",
type: "Proprietary",
feasible: 19 / 52,
moq: 1.11,
lutCount: 2210,
frequency: 208.4,
power: 0.25,
link: "https://anthropic.com"
},
{
id: "kimi-k2-5",
name: "Kimi K2.5",
developer: "Moonshot AI",
type: "Proprietary",
feasible: 17 / 52,
moq: 1.23,
lutCount: 2020,
frequency: 228.2,
power: 0.21,
link: "https://www.moonshot.cn"
},
{
id: "deepseek-v3",
name: "DeepSeek V3",
developer: "DeepSeek",
type: "Open Source",
feasible: 9 / 52,
moq: 1.22,
lutCount: 2050,
frequency: 224.8,
power: 0.22,
link: "https://github.com/deepseek-ai"
},
{
id: "qwen3-coder",
name: "Qwen3 Coder",
developer: "Alibaba Cloud",
type: "Open Source",
feasible: 8 / 52,
moq: 1.07,
lutCount: 2280,
frequency: 201.5,
power: 0.27,
link: "https://github.com/QwenLM"
},
{
id: "meta-llama-3-1",
name: "Meta Llama 3.1",
developer: "Meta",
type: "Open Source",
feasible: 0 / 52,
moq: null,
lutCount: null,
frequency: null,
power: null,
link: "https://llama.meta.com"
},
{
id: "qwen-2-5-coder",
name: "Qwen 2.5 Coder",
developer: "Alibaba Cloud",
type: "Open Source",
feasible: 0 / 52,
moq: null,
lutCount: null,
frequency: null,
power: null,
link: "https://github.com/QwenLM"
}
];
// App State Management
let currentSortColumn = "rank";
let currentSortDirection = "asc"; // "asc" or "desc"
let searchFilter = "";
let licenseFilter = "all";
let frontierChart = null;
// Pareto Frontier Calculation
// A point is on the frontier if it is not dominated by any other point.
// Dominates = both feasibility and MOQ are larger/equal, and at least one is strictly larger.
function calculateParetoFrontier(data) {
const frontier = [];
const validData = data.filter(d => d.moq !== null && d.moq !== undefined);
for (const item of validData) {
let dominated = false;
for (const other of validData) {
if (other.id === item.id) continue;
if (other.feasible >= item.feasible && other.moq >= item.moq) {
if (other.feasible > item.feasible || other.moq > item.moq) {
dominated = true;
break;
}
}
}
if (!dominated) {
frontier.push(item);
}
}
// Sort by feasibility ascending to draw a continuous curve from left to right
return frontier.sort((a, b) => a.feasible - b.feasible);
}
// Compute overall ranks based on Feasible@1 (primary) and MOQ@1 (secondary tie-breaker)
function computeModelRanks() {
const sorted = [...modelsData].sort((a, b) => {
if (b.feasible !== a.feasible) {
return b.feasible - a.feasible;
}
const moqA = a.moq !== null ? a.moq : -1;
const moqB = b.moq !== null ? b.moq : -1;
return moqB - moqA;
});
sorted.forEach((model, index) => {
model.computedRank = index + 1;
});
}
// Set initial global stats on page load
function populateGlobalStats() {
document.getElementById("total-models-val").textContent = modelsData.length;
const maxFeasible = Math.max(...modelsData.map(m => m.feasible));
document.getElementById("top-feasible-val").textContent = `${(maxFeasible * 100).toFixed(1)}%`;
const validMoqs = modelsData.map(m => m.moq).filter(moq => moq !== null && moq !== undefined);
const maxMOQ = validMoqs.length > 0 ? Math.max(...validMoqs) : 0;
document.getElementById("top-moq-val").textContent = maxMOQ > 0 ? maxMOQ.toFixed(2) : "—";
}
// Render Leaderboard Table
function renderLeaderboard() {
const tbody = document.getElementById("leaderboard-body");
tbody.innerHTML = "";
// Apply filters
let filteredData = modelsData.filter(model => {
const matchesSearch = model.name.toLowerCase().includes(searchFilter.toLowerCase()) ||
model.developer.toLowerCase().includes(searchFilter.toLowerCase());
const matchesLicense = licenseFilter === "all" || model.type === licenseFilter;
return matchesSearch && matchesLicense;
});
// Apply sorting
filteredData.sort((a, b) => {
let valA, valB;
if (currentSortColumn === "rank") {
valA = a.computedRank;
valB = b.computedRank;
} else {
valA = a[currentSortColumn];
valB = b[currentSortColumn];
}
if (typeof valA === "string") {
return currentSortDirection === "asc"
? valA.localeCompare(valB)
: valB.localeCompare(valA);
} else {
return currentSortDirection === "asc"
? valA - valB
: valB - valA;
}
});
if (filteredData.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" style="text-align: center; padding: 3rem; color: var(--text-muted);">
No models matched your search or filter criteria.
</td>
</tr>
`;
return;
}
filteredData.forEach(model => {
const rankClass = model.computedRank === 1 ? "rank-1" : model.computedRank === 2 ? "rank-2" : model.computedRank === 3 ? "rank-3" : "rank-other";
const licenseBadgeClass = model.type === "Open Source" ? "badge-open" : "badge-proprietary";
// Create base data row
const row = document.createElement("tr");
row.className = `model-row ${rankClass}`;
row.id = `row-${model.id}`;
row.setAttribute("data-model-id", model.id);
// Feasible percent & MOQ bar fills
const feasiblePct = (model.feasible * 100).toFixed(1);
const feasibleBarWidth = model.feasible * 100;
const isMoqValid = model.moq !== null && model.moq !== undefined;
const moqText = isMoqValid ? model.moq.toFixed(2) : "—";
const moqBarWidth = isMoqValid ? Math.min(model.moq * 65, 100) : 0;
row.innerHTML = `
<td class="col-rank">
<span class="rank-badge">${model.computedRank}</span>
</td>
<td class="col-name">${model.name}</td>
<td class="col-dev">${model.developer}</td>
<td class="col-type">
<span class="badge-type ${licenseBadgeClass}">${model.type}</span>
</td>
<td class="col-feasible">
<div class="metric-value-container">
<span>${feasiblePct}%</span>
<div class="metric-bar-bg" aria-hidden="true">
<div class="metric-bar-fill feasible-fill" style="width: ${feasibleBarWidth}%"></div>
</div>
</div>
</td>
<td class="col-moq">
<div class="metric-value-container">
<span>${moqText}</span>
<div class="metric-bar-bg" aria-hidden="true">
<div class="metric-bar-fill moq-fill" style="width: ${moqBarWidth}%"></div>
</div>
</div>
</td>
<td class="col-action">
<button class="btn-toggle-details" aria-expanded="false" aria-label="Toggle details for ${model.name}">
<span class="toggle-indicator">▼</span>
</button>
</td>
`;
// Create details/expandable row
const detailsRow = document.createElement("tr");
detailsRow.className = "details-row";
detailsRow.id = `details-${model.id}`;
const lutText = (model.lutCount !== null && model.lutCount !== undefined) ? model.lutCount.toLocaleString() : "—";
const freqText = (model.frequency !== null && model.frequency !== undefined) ? `${model.frequency.toFixed(1)} MHz` : "—";
const powerText = (model.power !== null && model.power !== undefined) ? `${model.power.toFixed(2)} W` : "—";
detailsRow.innerHTML = `
<td colspan="7">
<div class="details-content" id="details-content-${model.id}">
<div class="details-inner">
<div class="detail-metric-card">
<span class="detail-metric-label">Estimated LUT Usage</span>
<span class="detail-metric-value">${lutText}</span>
<span class="detail-metric-sub">Lower is better (Area optimization)</span>
</div>
<div class="detail-metric-card">
<span class="detail-metric-label">Max Freq (Fmax)</span>
<span class="detail-metric-value">${freqText}</span>
<span class="detail-metric-sub">Higher is better (Performance optimization)</span>
</div>
<div class="detail-metric-card">
<span class="detail-metric-label">Dynamic Power</span>
<span class="detail-metric-value">${powerText}</span>
<span class="detail-metric-sub">Lower is better (Power optimization)</span>
</div>
<div class="detail-action-card">
<p>Hardware synthesizability verified post-routing using FPGABench automation.</p>
<a href="${model.link}" target="_blank" rel="noopener noreferrer" class="detail-link-btn">
Visit Model Website →
</a>
</div>
</div>
</div>
</td>
`;
tbody.appendChild(row);
tbody.appendChild(detailsRow);
// Setup expandable row click listener
const toggleBtn = row.querySelector(".btn-toggle-details");
row.addEventListener("click", (e) => {
// Don't expand if user clicked a link
if (e.target.tagName === "A" || e.target.closest("a")) return;
toggleDetails(model.id, toggleBtn);
});
// Hover mapping: Hovering table highlights scatter dot
row.addEventListener("mouseenter", () => {
highlightChartPoint(model.id, true);
});
row.addEventListener("mouseleave", () => {
highlightChartPoint(model.id, false);
});
});
}
// Toggle detail row expansion
function toggleDetails(modelId, button) {
const content = document.getElementById(`details-content-${modelId}`);
const isExpanded = button.classList.contains("expanded");
// Collapse all other detail rows first for accordion feel (clean UI)
document.querySelectorAll(".btn-toggle-details.expanded").forEach(btn => {
if (btn !== button) {
const mid = btn.closest(".model-row").getAttribute("data-model-id");
const otherContent = document.getElementById(`details-content-${mid}`);
otherContent.style.height = "0px";
btn.classList.remove("expanded");
btn.setAttribute("aria-expanded", "false");
}
});
if (isExpanded) {
content.style.height = "0px";
button.classList.remove("expanded");
button.setAttribute("aria-expanded", "false");
} else {
// Set to scrollHeight to animate correctly
content.style.height = content.scrollHeight + "px";
button.classList.add("expanded");
button.setAttribute("aria-expanded", "true");
}
}
// Handle dynamic sorting clicking on columns
function setupSorting() {
const headers = document.querySelectorAll("th.sortable");
headers.forEach(header => {
header.addEventListener("click", () => {
const sortColumn = header.getAttribute("data-sort");
if (currentSortColumn === sortColumn) {
// Toggle direction
currentSortDirection = currentSortDirection === "asc" ? "desc" : "asc";
} else {
currentSortColumn = sortColumn;
// Default directions based on column type
currentSortDirection = (sortColumn === "feasible" || sortColumn === "moq") ? "desc" : "asc";
}
// Update UI classes on header
headers.forEach(h => {
h.classList.remove("sorted-asc", "sorted-desc");
const indicator = h.querySelector(".sort-indicator");
indicator.textContent = "↕";
});
header.classList.add(currentSortDirection === "asc" ? "sorted-asc" : "sorted-desc");
header.querySelector(".sort-indicator").textContent = currentSortDirection === "asc" ? "↑" : "↓";
renderLeaderboard();
});
});
}
// Setup search input and filter dropdown listeners
function setupFilters() {
const searchInput = document.getElementById("model-search");
const licenseFilterSelect = document.getElementById("type-filter");
searchInput.addEventListener("input", (e) => {
searchFilter = e.target.value;
renderLeaderboard();
updateChartOpacity(); // Dim models in chart that don't match filters
});
licenseFilterSelect.addEventListener("change", (e) => {
licenseFilter = e.target.value;
renderLeaderboard();
updateChartOpacity(); // Dim models in chart that don't match filters
});
}
// --- Chart.js Setup & Integration ---
function initFrontierChart() {
const ctx = document.getElementById("frontierChart").getContext("2d");
// Calculate Pareto Frontier curve data
const frontierPoints = calculateParetoFrontier(modelsData);
// Format scatter data (filter out models with no MOQ)
const validModels = modelsData.filter(model => model.moq !== null && model.moq !== undefined);
const scatterData = validModels.map(model => ({
x: model.feasible * 100, // X-axis: Feasible@1 in %
y: model.moq, // Y-axis: MOQ@1 index
id: model.id,
name: model.name,
dev: model.developer,
type: model.type
}));
// Format line curve data (draw a smooth spline through the frontier points)
// To make the line look smooth and go slightly past edges, we sort it.
const lineData = frontierPoints.map(model => ({
x: model.feasible * 100,
y: model.moq
}));
frontierChart = new Chart(ctx, {
type: "scatter",
data: {
datasets: [
{
label: "Pareto Frontier",
data: lineData,
type: "line",
borderColor: "#7c3aed", // Purple frontier line
borderWidth: 3,
tension: 0.35, // Smooth curves
fill: false,
pointRadius: 0, // Hide points on the line dataset, rely on the scatter dataset
order: 1,
hoverRadius: 0
},
{
label: "Models",
data: scatterData,
backgroundColor: (context) => {
const index = context.dataIndex;
const model = context.dataset.data[index];
if (!model) return "rgba(2, 132, 199, 0.75)";
// Check if point is on the Pareto frontier to style differently
const isOnFrontier = frontierPoints.some(f => f.id === model.id);
return isOnFrontier ? "#7c3aed" : "#0284c7"; // Purple for frontier, Sky Blue for others
},
borderColor: "#ffffff",
borderWidth: 2,
pointRadius: 8,
pointHoverRadius: 12,
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "#7c3aed",
pointHoverBorderWidth: 3,
order: 0
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false // Using custom styled legend in HTML instead
},
tooltip: {
backgroundColor: "#ffffff",
titleColor: "#0f172a",
bodyColor: "#334155",
titleFont: {
family: "Space Grotesk",
size: 14,
weight: "bold"
},
bodyFont: {
family: "Plus Jakarta Sans",
size: 13
},
borderColor: "rgba(15, 23, 42, 0.08)",
borderWidth: 1,
padding: 12,
cornerRadius: 8,
displayColors: false,
callbacks: {
title: (context) => {
const model = context[0].raw;
return model.name || "Pareto Frontier Line";
},
label: (context) => {
const model = context.raw;
if (model.id) {
return [
`Developer: ${model.dev}`,
`Feasible@1: ${model.x.toFixed(1)}%`,
`MOQ@1: ${model.y.toFixed(2)}`
];
}
return `Frontier MOQ: ${model.y.toFixed(2)} at Feasible: ${model.x.toFixed(1)}%`;
}
}
}
},
scales: {
x: {
title: {
display: true,
text: "Feasible@1 Success Rate (%)",
color: "#334155",
font: {
family: "Space Grotesk",
size: 13,
weight: "bold"
}
},
grid: {
color: "rgba(15, 23, 42, 0.05)"
},
ticks: {
color: "#475569",
font: { family: "JetBrains Mono" },
callback: (value) => `${value}%`
},
min: 0,
max: 60
},
y: {
title: {
display: true,
text: "MOQ@1 Design Quality Index (Normalized)",
color: "#334155",
font: {
family: "Space Grotesk",
size: 13,
weight: "bold"
}
},
grid: {
color: "rgba(15, 23, 42, 0.05)"
},
ticks: {
color: "#475569",
font: { family: "JetBrains Mono" }
},
min: 1.0,
max: 1.26
}
},
onHover: (event, activeElements) => {
// Change cursor to pointer if hovering over points
event.chart.canvas.style.cursor = activeElements.length ? "pointer" : "default";
// Link Hover: Highlight table row when hovering chart point
if (activeElements.length > 0) {
const index = activeElements[0].index;
const datasetIndex = activeElements[0].datasetIndex;
if (datasetIndex === 1) { // Models scatter dataset
const model = event.chart.data.datasets[1].data[index];
highlightTableRow(model.id);
}
} else {
clearTableHighlights();
}
}
}
});
}
// Sync highlight: Table row hover -> Chart point highlight
function highlightChartPoint(modelId, highlight) {
if (!frontierChart) return;
const dataset = frontierChart.data.datasets[1];
const modelIndex = dataset.data.findIndex(p => p.id === modelId);
if (modelIndex !== -1) {
const meta = frontierChart.getDatasetMeta(1);
const point = meta.data[modelIndex];
if (highlight) {
// Force active hover styling in Chart.js
frontierChart.tooltip.setActiveElements([{ datasetIndex: 1, index: modelIndex }], {
x: point.x,
y: point.y
});
// Set visual state
point.options.radius = 12;
point.options.backgroundColor = "#fff";
point.options.borderColor = "#8b5cf6";
point.options.borderWidth = 3;
} else {
// Reset active elements
frontierChart.tooltip.setActiveElements([], { x: 0, y: 0 });
// Reset visual state
point.options.radius = undefined; // defaults back
point.options.backgroundColor = undefined;
point.options.borderColor = undefined;
point.options.borderWidth = undefined;
}
frontierChart.update("none"); // Update without animation loops for performance
}
}
// Sync highlight: Chart point hover -> Table row highlight
let lastHighlightedRow = null;
function highlightTableRow(modelId) {
clearTableHighlights();
const row = document.getElementById(`row-${modelId}`);
if (row) {
row.classList.add("row-active");
lastHighlightedRow = row;
// Smoothly scroll the highlighted row into view if it's outside the scroll container view
// (Only if it's not already in view, to prevent jarring screen jumps)
const tableWrapper = row.closest(".table-wrapper");
const rowTop = row.offsetTop;
const rowBottom = rowTop + row.offsetHeight;
const scrollContainerTop = tableWrapper.scrollTop;
const scrollContainerBottom = scrollContainerTop + tableWrapper.offsetHeight;
if (rowTop < scrollContainerTop) {
tableWrapper.scrollTo({ top: rowTop - 20, behavior: "smooth" });
} else if (rowBottom > scrollContainerBottom) {
tableWrapper.scrollTo({ top: rowBottom - tableWrapper.offsetHeight + 20, behavior: "smooth" });
}
}
}
function clearTableHighlights() {
if (lastHighlightedRow) {
lastHighlightedRow.classList.remove("row-active");
lastHighlightedRow = null;
}
}
// Update chart point opacities based on active filters (dim non-matching models)
function updateChartOpacity() {
if (!frontierChart) return;
const filteredIds = modelsData
.filter(model => {
const matchesSearch = model.name.toLowerCase().includes(searchFilter.toLowerCase()) ||
model.developer.toLowerCase().includes(searchFilter.toLowerCase());
const matchesLicense = licenseFilter === "all" || model.type === licenseFilter;
return matchesSearch && matchesLicense;
})
.map(model => model.id);
// Dynamically update point background opacity
const frontierPoints = calculateParetoFrontier(modelsData);
frontierChart.data.datasets[1].backgroundColor = (context) => {
const index = context.dataIndex;
const model = context.dataset.data[index];
if (!model) return "rgba(2, 132, 199, 0.75)";
const isFiltered = filteredIds.includes(model.id);
const isOnFrontier = frontierPoints.some(f => f.id === model.id);
const baseColor = isOnFrontier ? "124, 58, 237" : "2, 132, 199"; // purple vs blue RGB
const opacity = isFiltered ? "1.0" : "0.15";
return `rgba(${baseColor}, ${opacity})`;
};
frontierChart.data.datasets[1].borderColor = (context) => {
const index = context.dataIndex;
const model = context.dataset.data[index];
if (!model) return "rgba(15, 23, 42, 0.2)";
const isFiltered = filteredIds.includes(model.id);
const opacity = isFiltered ? "1.0" : "0.15";
return `rgba(255, 255, 255, ${opacity})`;
};
frontierChart.update();
}
// --- Page Initialization ---
document.addEventListener("DOMContentLoaded", () => {
computeModelRanks();
populateGlobalStats();
renderLeaderboard();
setupSorting();
setupFilters();
// Initialize Chart after a brief timeout to let grid layout render properly
setTimeout(() => {
initFrontierChart();
}, 100);
});