698 lines
22 KiB
JavaScript
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);
|
|
});
|