| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AutoFS Leaderboard</title> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <style> |
| :root { |
| --primary-color: #3498db; |
| --secondary-color: #2c3e50; |
| --background-color: #f8f9fa; |
| --text-color: #333; |
| --border-color: #dee2e6; |
| --hover-color: #f1f1f1; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| margin: 0; |
| padding: 20px; |
| background-color: var(--background-color); |
| color: var(--text-color); |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| background: white; |
| padding: 20px; |
| border-radius: 8px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| } |
| |
| header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 20px; |
| border-bottom: 2px solid var(--primary-color); |
| padding-bottom: 10px; |
| } |
| |
| h1 { |
| margin: 0; |
| color: var(--secondary-color); |
| } |
| |
| .controls { |
| display: flex; |
| gap: 10px; |
| align-items: center; |
| } |
| |
| select { |
| padding: 8px 12px; |
| border: 1px solid var(--border-color); |
| border-radius: 4px; |
| font-size: 14px; |
| } |
| |
| table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 10px; |
| } |
| |
| th, td { |
| padding: 12px 15px; |
| text-align: left; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| th { |
| background-color: var(--secondary-color); |
| color: white; |
| cursor: pointer; |
| user-select: none; |
| position: sticky; |
| top: 0; |
| } |
| |
| th:hover { |
| background-color: #34495e; |
| } |
| |
| th .arrow { |
| font-size: 10px; |
| margin-left: 5px; |
| opacity: 0.7; |
| } |
| |
| tr:hover { |
| background-color: var(--hover-color); |
| } |
| |
| .score-bar { |
| height: 6px; |
| background-color: #e9ecef; |
| border-radius: 3px; |
| overflow: hidden; |
| margin-top: 5px; |
| } |
| |
| .score-fill { |
| height: 100%; |
| background-color: var(--primary-color); |
| } |
| |
| .features-cell { |
| max-width: 200px; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| color: #666; |
| font-size: 0.9em; |
| cursor: pointer; |
| } |
| |
| .features-cell:hover { |
| text-decoration: underline; |
| color: var(--primary-color); |
| } |
| |
| |
| .modal { |
| display: none; |
| position: fixed; |
| z-index: 1000; |
| left: 0; |
| top: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0,0,0,0.5); |
| } |
| |
| .modal-content { |
| background-color: white; |
| margin: 10% auto; |
| padding: 20px; |
| border-radius: 8px; |
| width: 50%; |
| max-width: 600px; |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); |
| } |
| |
| .close { |
| color: #aaa; |
| float: right; |
| font-size: 28px; |
| font-weight: bold; |
| cursor: pointer; |
| } |
| |
| .close:hover { |
| color: black; |
| } |
| |
| .feature-tag { |
| display: inline-block; |
| background-color: #e1ecf4; |
| color: #2c3e50; |
| padding: 4px 8px; |
| border-radius: 4px; |
| margin: 2px; |
| font-size: 0.9em; |
| } |
| |
| .loading { |
| text-align: center; |
| padding: 20px; |
| color: #666; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div class="container"> |
| <header> |
| <h1>🏆 AutoFS Leaderboard</h1> |
| <div class="controls"> |
| <label for="dataset-select">Dataset:</label> |
| <select id="dataset-select"> |
| <option value="" disabled selected>Loading...</option> |
| </select> |
| </div> |
| </header> |
|
|
| <div id="loading-indicator" class="loading" style="display: none;">Loading data...</div> |
|
|
| <div class="chart-controls" style="text-align:center; margin-top: 20px; margin-bottom: 15px;"> |
| <label style="margin-right:15px; font-weight:bold;">View Mode:</label> |
| <input type="radio" id="view-overall" name="chart-view" value="overall" checked onchange="updateView()"> |
| <label for="view-overall" style="margin-right:10px;">Overall (Mean)</label> |
| |
| <input type="radio" id="view-classifiers-f1" name="chart-view" value="classifiers-f1" onchange="updateView()"> |
| <label for="view-classifiers-f1" style="margin-right:10px;">F1 by Classifier</label> |
| |
| <input type="radio" id="view-classifiers-auc" name="chart-view" value="classifiers-auc" onchange="updateView()"> |
| <label for="view-classifiers-auc">AUC by Classifier</label> |
| </div> |
|
|
| <div class="charts-container" style="display: flex; gap: 20px; margin-bottom: 20px;"> |
| <div style="flex: 1; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"> |
| <canvas id="scoreChart"></canvas> |
| </div> |
| <div style="flex: 1; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"> |
| <canvas id="timeChart"></canvas> |
| </div> |
| </div> |
|
|
| <table id="result-table"> |
| <thead> |
| |
| </thead> |
| <tbody> |
| |
| </tbody> |
| </table> |
| </div> |
|
|
| |
| <div id="details-modal" class="modal"> |
| <div class="modal-content"> |
| <span class="close">×</span> |
| <h2 id="modal-title">Algorithm Details</h2> |
| <div id="modal-body"></div> |
| </div> |
| </div> |
|
|
| <script> |
| let currentResults = []; |
| let sortDirection = 1; |
| let lastSortKey = ''; |
| |
| const VIEW_CONFIG = { |
| 'overall': [ |
| { key: 'mean_f1', label: 'Mean F1' }, |
| { key: 'mean_auc', label: 'Mean AUC' } |
| ], |
| 'classifiers-f1': [ |
| { key: 'metrics.nb.f1', label: 'NB F1' }, |
| { key: 'metrics.svm.f1', label: 'SVM F1' }, |
| { key: 'metrics.rf.f1', label: 'RF F1' } |
| ], |
| 'classifiers-auc': [ |
| { key: 'metrics.nb.auc', label: 'NB AUC' }, |
| { key: 'metrics.svm.auc', label: 'SVM AUC' }, |
| { key: 'metrics.rf.auc', label: 'RF AUC' } |
| ] |
| }; |
| |
| const tableHead = document.querySelector("#result-table thead"); |
| const tableBody = document.querySelector("#result-table tbody"); |
| const datasetSelect = document.getElementById("dataset-select"); |
| const loadingIndicator = document.getElementById("loading-indicator"); |
| const modal = document.getElementById("details-modal"); |
| const closeModal = document.querySelector(".close"); |
| |
| |
| closeModal.onclick = () => modal.style.display = "none"; |
| window.onclick = (event) => { |
| if (event.target == modal) modal.style.display = "none"; |
| } |
| |
| |
| let scoreChartInstance = null; |
| let timeChartInstance = null; |
| |
| function updateCharts(results) { |
| if (!Array.isArray(results) || results.length === 0) return; |
| |
| |
| const topResults = results.slice(0, 15); |
| const labels = topResults.map(r => r.algorithm || 'Unknown'); |
| const times = topResults.map(r => r.time || 0); |
| |
| const viewMode = document.querySelector('input[name="chart-view"]:checked').value; |
| let datasets = []; |
| |
| if (viewMode === 'overall') { |
| const f1Scores = topResults.map(r => r.mean_f1 || 0); |
| const aucScores = topResults.map(r => r.mean_auc || 0); |
| datasets = [ |
| { |
| label: 'Mean F1', |
| data: f1Scores, |
| backgroundColor: 'rgba(52, 152, 219, 0.7)', |
| borderColor: 'rgba(52, 152, 219, 1)', |
| borderWidth: 1 |
| }, |
| { |
| label: 'Mean AUC', |
| data: aucScores, |
| backgroundColor: 'rgba(46, 204, 113, 0.7)', |
| borderColor: 'rgba(46, 204, 113, 1)', |
| borderWidth: 1 |
| } |
| ]; |
| } else if (viewMode === 'classifiers-f1') { |
| const classifiers = ['nb', 'svm', 'rf']; |
| const colors = ['rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)']; |
| const borderColors = ['rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)']; |
| |
| datasets = classifiers.map((cls, idx) => ({ |
| label: cls.toUpperCase() + ' F1', |
| data: topResults.map(r => (r.metrics && r.metrics[cls]) ? r.metrics[cls].f1 : 0), |
| backgroundColor: colors[idx], |
| borderColor: borderColors[idx], |
| borderWidth: 1 |
| })); |
| } else if (viewMode === 'classifiers-auc') { |
| const classifiers = ['nb', 'svm', 'rf']; |
| const colors = ['rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)']; |
| const borderColors = ['rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)']; |
| |
| datasets = classifiers.map((cls, idx) => ({ |
| label: cls.toUpperCase() + ' AUC', |
| data: topResults.map(r => (r.metrics && r.metrics[cls]) ? r.metrics[cls].auc : 0), |
| backgroundColor: colors[idx], |
| borderColor: borderColors[idx], |
| borderWidth: 1 |
| })); |
| } |
| |
| |
| const scoreCtx = document.getElementById('scoreChart').getContext('2d'); |
| if (scoreChartInstance) scoreChartInstance.destroy(); |
| |
| scoreChartInstance = new Chart(scoreCtx, { |
| type: 'bar', |
| data: { |
| labels: labels, |
| datasets: datasets |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| title: { |
| display: true, |
| text: viewMode === 'overall' ? 'Top Algorithms Performance (Mean)' : |
| (viewMode === 'classifiers-f1' ? 'F1-Score by Classifier' : 'AUC by Classifier') |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: false, |
| |
| } |
| } |
| } |
| }); |
| |
| |
| const timeCtx = document.getElementById('timeChart').getContext('2d'); |
| if (timeChartInstance) timeChartInstance.destroy(); |
| |
| timeChartInstance = new Chart(timeCtx, { |
| type: 'line', |
| data: { |
| labels: labels, |
| datasets: [{ |
| label: 'Time (s)', |
| data: times, |
| backgroundColor: 'rgba(231, 76, 60, 0.2)', |
| borderColor: 'rgba(231, 76, 60, 1)', |
| borderWidth: 2, |
| tension: 0.3, |
| fill: true |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| title: { display: true, text: 'Execution Time' } |
| }, |
| scales: { |
| y: { beginAtZero: true } |
| } |
| } |
| }); |
| } |
| |
| function showDetails(result) { |
| const title = document.getElementById("modal-title"); |
| const body = document.getElementById("modal-body"); |
| |
| title.textContent = `${result.algorithm} Details`; |
| |
| let featuresHtml = result.selected_features.map(f => |
| `<span class="feature-tag">${f}</span>` |
| ).join(''); |
| |
| let metricsHtml = '<div style="margin-top: 15px;"><h3>Metrics Breakdown</h3>'; |
| for (const [clf, m] of Object.entries(result.metrics || {})) { |
| metricsHtml += ` |
| <div style="margin-bottom: 10px;"> |
| <strong>${clf.toUpperCase()}:</strong> |
| F1: ${m.f1.toFixed(4)}, AUC: ${m.auc.toFixed(4)} |
| </div>`; |
| } |
| metricsHtml += '</div>'; |
| |
| body.innerHTML = ` |
| <p><strong>Time:</strong> ${result.time.toFixed(4)}s</p> |
| <p><strong>Num Features:</strong> ${result.num_features}</p> |
| <p><strong>Selected Features (${result.selected_features.length}):</strong></p> |
| <div>${featuresHtml}</div> |
| ${metricsHtml} |
| `; |
| |
| modal.style.display = "block"; |
| } |
| |
| function getValue(obj, path) { |
| if (!path) return undefined; |
| return path.split('.').reduce((acc, part) => (acc && acc[part] !== undefined) ? acc[part] : undefined, obj); |
| } |
| |
| function safeFixed(value, digits=4) { |
| if (value === undefined || value === null) return 'N/A'; |
| return Number(value).toFixed(digits); |
| } |
| |
| function renderTableHeader() { |
| const viewMode = document.querySelector('input[name="chart-view"]:checked').value; |
| const dynamicCols = VIEW_CONFIG[viewMode] || VIEW_CONFIG['overall']; |
| |
| let html = '<tr>'; |
| html += '<th data-key="rank" style="width: 60px;">#</th>'; |
| html += '<th data-key="algorithm">Algorithm <span class="arrow">↕</span></th>'; |
| |
| dynamicCols.forEach(col => { |
| html += `<th data-key="${col.key}">${col.label} <span class="arrow">↕</span></th>`; |
| }); |
| |
| html += '<th data-key="time">Time (s) <span class="arrow">↕</span></th>'; |
| html += '<th data-key="selected_features">Selected Features</th>'; |
| html += '</tr>'; |
| |
| tableHead.innerHTML = html; |
| |
| |
| tableHead.querySelectorAll('th[data-key]').forEach(th => { |
| th.addEventListener('click', () => sortTable(th.dataset.key)); |
| }); |
| } |
| |
| function updateTable(results) { |
| tableBody.innerHTML = ""; |
| |
| |
| if (!Array.isArray(results)) { |
| tableBody.innerHTML = '<tr><td colspan="10" style="text-align:center; color:red;">Error: Invalid data format</td></tr>'; |
| return; |
| } |
| |
| if (results.length === 0) { |
| tableBody.innerHTML = '<tr><td colspan="10" style="text-align:center;">No results found</td></tr>'; |
| return; |
| } |
| |
| const viewMode = document.querySelector('input[name="chart-view"]:checked').value; |
| const dynamicCols = VIEW_CONFIG[viewMode] || VIEW_CONFIG['overall']; |
| |
| results.forEach((r, idx) => { |
| const row = document.createElement("tr"); |
| |
| |
| const featurePreview = (r.selected_features && Array.isArray(r.selected_features)) |
| ? r.selected_features.slice(0, 5).join(', ') + (r.selected_features.length > 5 ? '...' : '') |
| : 'N/A'; |
| |
| let html = `<td>${idx + 1}</td>`; |
| html += `<td><strong>${r.algorithm || 'Unknown'}</strong></td>`; |
| |
| dynamicCols.forEach(col => { |
| const val = getValue(r, col.key); |
| const score = val !== undefined ? val : 0; |
| html += ` |
| <td> |
| ${safeFixed(val)} |
| <div class="score-bar"><div class="score-fill" style="width: ${Math.min(score * 100, 100)}%"></div></div> |
| </td>`; |
| }); |
| |
| const time = r.time || 0; |
| html += `<td>${safeFixed(time, 2)}</td>`; |
| html += ` |
| <td class="features-cell" onclick="showDetails(currentResults[${idx}])" title="Click for details"> |
| ${featurePreview} <span style="font-size:0.8em; color:#999;">(Click for details)</span> |
| </td>`; |
| |
| row.innerHTML = html; |
| tableBody.appendChild(row); |
| }); |
| } |
| |
| function sortTable(key) { |
| if (lastSortKey === key) { |
| sortDirection *= -1; |
| } else { |
| sortDirection = key === 'time' || key === 'rank' ? 1 : -1; |
| lastSortKey = key; |
| } |
| |
| |
| |
| document.querySelectorAll('th .arrow').forEach(span => span.textContent = '↕'); |
| const activeHeader = document.querySelector(`th[data-key="${key}"] .arrow`); |
| if (activeHeader) activeHeader.textContent = sortDirection === 1 ? '↑' : '↓'; |
| |
| const sorted = [...currentResults].sort((a, b) => { |
| let valA = getValue(a, key); |
| let valB = getValue(b, key); |
| |
| if (key === 'rank') return 0; |
| |
| if (valA === undefined) valA = -Infinity; |
| if (valB === undefined) valB = -Infinity; |
| |
| if (valA < valB) return -1 * sortDirection; |
| if (valA > valB) return 1 * sortDirection; |
| return 0; |
| }); |
| |
| |
| |
| |
| currentResults = sorted; |
| updateTable(sorted); |
| } |
| |
| function updateView() { |
| renderTableHeader(); |
| updateTable(currentResults); |
| updateCharts(currentResults); |
| } |
| |
| function fetchResults(dataset) { |
| loadingIndicator.style.display = 'block'; |
| tableBody.innerHTML = ''; |
| |
| console.log("Fetching results for:", dataset); |
| fetch(`/api/results?dataset=${dataset}`) |
| .then(res => { |
| if (!res.ok) throw new Error("Network response was not ok"); |
| return res.json(); |
| }) |
| .then(data => { |
| console.log("Data received:", data); |
| currentResults = data; |
| updateView(); |
| loadingIndicator.style.display = 'none'; |
| }) |
| .catch(err => { |
| console.error("Error fetching results:", err); |
| loadingIndicator.textContent = "Error loading data. Make sure the server is running."; |
| }); |
| } |
| |
| |
| document.addEventListener("DOMContentLoaded", () => { |
| |
| document.querySelectorAll('th[data-key]').forEach(th => { |
| th.addEventListener('click', () => sortTable(th.dataset.key)); |
| }); |
| |
| |
| fetch("/api/datasets") |
| .then(res => res.json()) |
| .then(datasets => { |
| datasetSelect.innerHTML = ""; |
| datasets.forEach(ds => { |
| const option = document.createElement("option"); |
| option.value = ds; |
| option.textContent = ds; |
| datasetSelect.appendChild(option); |
| }); |
| |
| if (datasets.includes("Authorship")) { |
| datasetSelect.value = "Authorship"; |
| fetchResults("Authorship"); |
| } else if (datasets.length > 0) { |
| datasetSelect.value = datasets[0]; |
| fetchResults(datasets[0]); |
| } |
| }) |
| .catch(err => { |
| console.error("Error fetching datasets:", err); |
| datasetSelect.innerHTML = "<option>Error loading datasets</option>"; |
| }); |
| |
| datasetSelect.addEventListener('change', (e) => { |
| fetchResults(e.target.value); |
| }); |
| }); |
| </script> |
|
|
| </body> |
| </html> |
|
|