/** * results.js - Data visualization and reports */ import { api } from '../api.js'; import { ui } from '../ui.js'; let activeCharts = []; // Store chart instances to destroy them before redraw export const results = { async init() { // Bind events ui.elements.csvFileSelect.addEventListener('change', (e) => this.loadCSVData(e.target.value)); ui.elements.chartFileSelect.addEventListener('change', (e) => this.loadChartData(e.target.value)); ui.elements.judgeFileSelect.addEventListener('change', (e) => this.loadJudgeData(e.target.value)); // Initial lists await this.loadAll(); }, async loadAll() { await Promise.all([ this.updateCSVFileList(), this.updateJudgeFileList(), this.loadImages() // Static images just load once ]); }, async updateCSVFileList() { try { const files = await api.get('/api/results/files'); const options = files.length > 0 ? files.map(f => ``).join('') : ''; ui.elements.csvFileSelect.innerHTML = options; ui.elements.chartFileSelect.innerHTML = options; // Prefer loading results.csv by default if it exists const defaultFile = files.find(f => f.endsWith('results.csv')) || files[0]; if (defaultFile) { ui.elements.csvFileSelect.value = defaultFile; ui.elements.chartFileSelect.value = defaultFile; await this.loadCSVData(defaultFile); await this.loadChartData(defaultFile); } } catch (error) { console.error("Failed loading CSV file list", error); } }, async updateJudgeFileList() { try { const files = await api.get('/api/judge/files'); const options = files.length > 0 ? files.map(f => ``).join('') : ''; ui.elements.judgeFileSelect.innerHTML = options; if (files.length > 0) { ui.elements.judgeFileSelect.value = files[0]; await this.loadJudgeData(files[0]); } } catch (error) { console.error("Failed loading Judge file list", error); } }, async loadCSVData(filePath) { if (!filePath) return; ui.elements.csvResults.innerHTML = '
Loading data...
'; try { const data = await api.get(`/api/results?file=${encodeURIComponent(filePath)}`); if (Object.keys(data).length === 0 || data.error) { ui.elements.csvResults.innerHTML = '
Failed to load data.
'; return; } // Since we asked for a specific file, it's the only key const content = Object.values(data)[0]; if (content.error) { ui.elements.csvResults.innerHTML = `
Error: ${content.error}
`; return; } let html = `
${filePath}
${content.columns.map(c => ``).join('')} ${content.data.map(row => ` ${content.columns.map(col => ``).join('')} `).join('')}
${c}
${row[col] ?? ''}
`; ui.elements.csvResults.innerHTML = html; } catch (err) { ui.elements.csvResults.innerHTML = `
Error: ${err.message}
`; } }, async loadChartData(filePath) { if (!filePath) return; // Clean up old charts activeCharts.forEach(chart => chart.destroy()); activeCharts = []; ui.elements.interactiveCharts.innerHTML = '
Generating charts...
'; try { const data = await api.get(`/api/results?file=${encodeURIComponent(filePath)}`); const content = Object.values(data)[0]; if (!content || content.error || !content.data || content.data.length === 0) { ui.elements.interactiveCharts.innerHTML = '
Not enough data to graph.
'; return; } ui.elements.interactiveCharts.innerHTML = ''; // Generate charts based on available columns const cols = content.columns; // 1. Blackbox Categories if (cols.includes('blackbox_category')) { this.renderPieChart('Blackbox Categories', content.data, 'blackbox_category'); } // 2. Glassbox Categories if (cols.includes('glassbox_category')) { this.renderPieChart('Glassbox Categories', content.data, 'glassbox_category'); } // 3. Models vs Blackbox Category if (cols.includes('model') && cols.includes('blackbox_category')) { this.renderBarChart('Deception by Model', content.data, 'model', 'blackbox_category'); } // 4. Fallback if no specific columns exist if (ui.elements.interactiveCharts.innerHTML === '') { ui.elements.interactiveCharts.innerHTML = '
No plottable categorical columns found in this CSV.
'; } } catch (err) { ui.elements.interactiveCharts.innerHTML = `
Chart Error: ${err.message}
`; } }, renderPieChart(title, data, column) { const counts = {}; data.forEach(row => { const val = row[column] || 'Unknown'; counts[val] = (counts[val] || 0) + 1; }); const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; const container = document.createElement('div'); container.className = 'chart-container'; container.innerHTML = ``; ui.elements.interactiveCharts.appendChild(container); const ctx = document.getElementById(canvasId).getContext('2d'); const chart = new Chart(ctx, { type: 'doughnut', data: { labels: Object.keys(counts), datasets: [{ data: Object.values(counts), backgroundColor: ['#9d4edd', '#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#6366f1'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, legend: { labels: { color: '#9ea0a6' }, position: 'right' } } } }); activeCharts.push(chart); }, renderBarChart(title, data, xCol, groupCol) { // Group by X then GroupCol const matrix = {}; const groups = new Set(); data.forEach(row => { const xVal = row[xCol] || 'Unknown'; const gVal = row[groupCol] || 'Unknown'; if (!matrix[xVal]) matrix[xVal] = {}; matrix[xVal][gVal] = (matrix[xVal][gVal] || 0) + 1; groups.add(gVal); }); const labels = Object.keys(matrix); const datasets = Array.from(groups).map((group, i) => { const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd']; return { label: group, data: labels.map(label => matrix[label][group] || 0), backgroundColor: colors[i % colors.length], } }); const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; const container = document.createElement('div'); container.className = 'chart-container'; container.innerHTML = ``; ui.elements.interactiveCharts.appendChild(container); const ctx = document.getElementById(canvasId).getContext('2d'); const chart = new Chart(ctx, { type: 'bar', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true, ticks: { color: '#9ea0a6' }, grid: { color: '#2a2a2e' } }, y: { stacked: true, ticks: { color: '#9ea0a6', stepSize: 1 }, grid: { color: '#2a2a2e' } } }, plugins: { title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, legend: { labels: { color: '#9ea0a6' } } } } }); activeCharts.push(chart); }, async loadImages() { const images = await api.get('/api/results/images'); if (images.length === 0) return; ui.elements.imageResults.innerHTML = images.map(img => `
${img.name}
`).join(''); }, async loadJudgeData(filePath) { if (!filePath) return; ui.elements.judgeResults.innerHTML = '
Loading data...
'; try { const data = await api.get(`/api/judge/results?file=${encodeURIComponent(filePath)}`); if (Object.keys(data).length === 0 || data.message) { ui.elements.judgeResults.innerHTML = '
No data available in this report.
'; return; } const content = Object.values(data)[0]; if (content.error) { ui.elements.judgeResults.innerHTML = `
Error: ${content.error}
`; return; } let html = ''; if (content.type === 'csv') { html += `
${filePath}
${content.columns.map(c => ``).join('')} ${content.data.map(row => ` ${content.columns.map(col => ``).join('')} `).join('')}
${c}
${row[col] ?? ''}
`; } else { // Render JSON as pretty block html += `
${filePath}
${JSON.stringify(content.data, null, 2)}
`; } ui.elements.judgeResults.innerHTML = html; } catch (err) { ui.elements.judgeResults.innerHTML = `
Error: ${err.message}
`; } } };