|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Sampler/Scheduler Performance Matrix</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<style> |
|
|
.heatmap-cell { |
|
|
transition: all 0.2s ease; |
|
|
position: relative; |
|
|
} |
|
|
.heatmap-cell:hover { |
|
|
transform: scale(1.05); |
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.2); |
|
|
z-index: 10; |
|
|
} |
|
|
.heatmap-cell:hover::after { |
|
|
content: attr(data-value); |
|
|
position: absolute; |
|
|
top: -30px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
background: #333; |
|
|
color: white; |
|
|
padding: 4px 8px; |
|
|
border-radius: 4px; |
|
|
font-size: 12px; |
|
|
white-space: nowrap; |
|
|
z-index: 20; |
|
|
} |
|
|
.sticky-header { |
|
|
position: sticky; |
|
|
top: 0; |
|
|
z-index: 20; |
|
|
background-color: white; |
|
|
} |
|
|
.sticky-col { |
|
|
position: sticky; |
|
|
left: 0; |
|
|
z-index: 10; |
|
|
background-color: white; |
|
|
} |
|
|
.scroll-container { |
|
|
max-height: 80vh; |
|
|
overflow: auto; |
|
|
} |
|
|
.color-scale { |
|
|
background: linear-gradient(90deg, #f0fff4, #9ae6b4, #48bb78, #f6e05e, #f6ad55, #f56565); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 font-sans"> |
|
|
<div class="container mx-auto px-4 py-8"> |
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"> |
|
|
<div> |
|
|
<h1 class="text-3xl font-bold text-gray-800 mb-2">Sampler/Scheduler Performance Matrix</h1> |
|
|
<p class="text-gray-600">Visualizing execution speed across different combinations</p> |
|
|
</div> |
|
|
<div class="flex items-center gap-4"> |
|
|
<div class="relative"> |
|
|
<input type="text" id="searchInput" placeholder="Search samplers..." |
|
|
class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
|
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
|
|
</div> |
|
|
<div class="flex items-center gap-2"> |
|
|
<span class="text-sm text-gray-600">Performance:</span> |
|
|
<div class="flex items-center"> |
|
|
<span class="text-xs mr-1">Fast</span> |
|
|
<div class="w-24 h-4 rounded-full color-scale"></div> |
|
|
<span class="text-xs ml-1">Slow</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-md overflow-hidden"> |
|
|
<div class="p-4 border-b"> |
|
|
<div class="flex justify-between items-center"> |
|
|
<h2 class="text-xl font-semibold text-gray-700">Performance Metrics (iterations/sec)</h2> |
|
|
<div class="flex items-center gap-2"> |
|
|
<button id="toggleHeatmap" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm hover:bg-blue-200 transition"> |
|
|
<i class="fas fa-fire mr-1"></i> Toggle Heatmap |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="scroll-container"> |
|
|
<table class="w-full"> |
|
|
<thead> |
|
|
<tr class="sticky-header"> |
|
|
<th class="sticky-col p-3 text-left font-medium text-gray-700 bg-white border-r min-w-[180px]">Sampler \ Scheduler</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">beta</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">beta57</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">ddim_uniform</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">exponential</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">kl_optimal</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">karras</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">linear_quadratic</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">normal</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">sgm_uniform</th> |
|
|
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">simple</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="matrixBody"> |
|
|
|
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-8 bg-white rounded-xl shadow-md p-6"> |
|
|
<h2 class="text-xl font-semibold text-gray-700 mb-4">Performance Insights</h2> |
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
|
|
<div class="bg-blue-50 p-4 rounded-lg"> |
|
|
<div class="flex items-center mb-2"> |
|
|
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3"> |
|
|
<i class="fas fa-trophy text-blue-500"></i> |
|
|
</div> |
|
|
<h3 class="font-medium text-gray-700">Top Performer</h3> |
|
|
</div> |
|
|
<p class="text-gray-600 text-sm" id="topPerformer">Loading...</p> |
|
|
</div> |
|
|
<div class="bg-green-50 p-4 rounded-lg"> |
|
|
<div class="flex items-center mb-2"> |
|
|
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3"> |
|
|
<i class="fas fa-star text-green-500"></i> |
|
|
</div> |
|
|
<h3 class="font-medium text-gray-700">Most Consistent</h3> |
|
|
</div> |
|
|
<p class="text-gray-600 text-sm" id="mostConsistent">Loading...</p> |
|
|
</div> |
|
|
<div class="bg-red-50 p-4 rounded-lg"> |
|
|
<div class="flex items-center mb-2"> |
|
|
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center mr-3"> |
|
|
<i class="fas fa-snail text-red-500"></i> |
|
|
</div> |
|
|
<h3 class="font-medium text-gray-700">Slowest</h3> |
|
|
</div> |
|
|
<p class="text-gray-600 text-sm" id="slowest">Loading...</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
let originalData = {}; |
|
|
let normalizedData = {}; |
|
|
let sortedSamplers = []; |
|
|
let sortedSchedulers = []; |
|
|
let minValue = Infinity; |
|
|
let maxValue = -Infinity; |
|
|
let heatmapEnabled = true; |
|
|
const BASE_SAMPLER = 'euler'; |
|
|
const BASE_SCHEDULER = 'simple'; |
|
|
let baseNormalizedValue = 1.0; |
|
|
|
|
|
|
|
|
async function fetchTextList(url) { |
|
|
try { |
|
|
const response = await fetch(url); |
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status} for ${url}`); |
|
|
} |
|
|
const text = await response.text(); |
|
|
return text.split('\n').map(s => s.trim()).filter(Boolean); |
|
|
} catch (error) { |
|
|
console.error(`Failed to fetch list from ${url}:`, error); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function normalizeData(rawData) { |
|
|
if (!rawData || typeof rawData !== 'object' || Object.keys(rawData).length === 0) { |
|
|
console.error("Cannot normalize empty or invalid data."); |
|
|
return {}; |
|
|
} |
|
|
|
|
|
const baseValue = rawData[BASE_SAMPLER]?.[BASE_SCHEDULER]; |
|
|
|
|
|
if (baseValue === undefined || baseValue === null || baseValue <= 0) { |
|
|
console.error(`Base value for normalization (${BASE_SAMPLER}/${BASE_SCHEDULER}) not found or invalid. Skipping normalization.`); |
|
|
return JSON.parse(JSON.stringify(rawData)); |
|
|
} |
|
|
|
|
|
console.log(`Normalizing data using base ${BASE_SAMPLER}/${BASE_SCHEDULER} = ${baseValue}`); |
|
|
baseNormalizedValue = 1.0; |
|
|
|
|
|
const normalized = {}; |
|
|
minValue = Infinity; |
|
|
maxValue = -Infinity; |
|
|
|
|
|
for (const sampler in rawData) { |
|
|
normalized[sampler] = {}; |
|
|
for (const scheduler in rawData[sampler]) { |
|
|
const originalValue = rawData[sampler][scheduler]; |
|
|
if (originalValue !== undefined && originalValue !== null) { |
|
|
const normValue = originalValue / baseValue; |
|
|
normalized[sampler][scheduler] = normValue; |
|
|
|
|
|
|
|
|
if (sampler !== BASE_SAMPLER || scheduler !== BASE_SCHEDULER) { |
|
|
if (normValue < minValue) minValue = normValue; |
|
|
|
|
|
if (normValue > maxValue) maxValue = normValue; |
|
|
} |
|
|
} else { |
|
|
normalized[sampler][scheduler] = undefined; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (!isFinite(minValue) || !isFinite(maxValue) || minValue >= maxValue) { |
|
|
console.warn("Could not determine valid min/max range for heatmap scaling (excluding base). Using default range [0.5, 1.5] for colors."); |
|
|
|
|
|
minValue = Math.min(0.5, baseNormalizedValue * 0.5); |
|
|
maxValue = Math.max(1.5, baseNormalizedValue * 1.5); |
|
|
} else { |
|
|
|
|
|
|
|
|
minValue = Math.min(minValue, baseNormalizedValue * 0.9); |
|
|
maxValue = Math.max(maxValue, baseNormalizedValue * 1.1); |
|
|
} |
|
|
console.log(`Normalized range for heatmap (excluding base): [${minValue.toFixed(2)}, ${maxValue.toFixed(2)}]`); |
|
|
|
|
|
return normalized; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getColor(value, sampler, scheduler) { |
|
|
|
|
|
if (sampler === BASE_SAMPLER && scheduler === BASE_SCHEDULER) { |
|
|
return 'bg-white border border-gray-400'; |
|
|
} |
|
|
|
|
|
if (!heatmapEnabled || value === undefined || value === null || !isFinite(value)) { |
|
|
return 'bg-gray-100'; |
|
|
} |
|
|
if (!isFinite(minValue) || !isFinite(maxValue) || minValue >= maxValue) { |
|
|
return 'bg-white'; |
|
|
} |
|
|
|
|
|
|
|
|
const range = maxValue - minValue; |
|
|
const normalizedForColor = range > 0 ? (value - minValue) / range : 0.5; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (normalizedForColor <= 0.05) return 'bg-emerald-400'; |
|
|
if (normalizedForColor <= 0.15) return 'bg-emerald-300'; |
|
|
if (normalizedForColor <= 0.30) return 'bg-green-300'; |
|
|
if (normalizedForColor <= 0.45) return 'bg-lime-300'; |
|
|
if (normalizedForColor <= 0.60) return 'bg-yellow-300'; |
|
|
if (normalizedForColor <= 0.75) return 'bg-amber-300'; |
|
|
if (normalizedForColor <= 0.85) return 'bg-orange-400'; |
|
|
if (normalizedForColor <= 0.95) return 'bg-red-400'; |
|
|
return 'bg-red-500'; |
|
|
} |
|
|
|
|
|
|
|
|
function generateTable() { |
|
|
const matrixBody = document.getElementById('matrixBody'); |
|
|
if (!matrixBody || Object.keys(normalizedData).length === 0 || sortedSamplers.length === 0 || sortedSchedulers.length === 0) { |
|
|
console.warn("Cannot generate table: Missing data or sorting lists."); |
|
|
if (matrixBody) matrixBody.innerHTML = '<tr><td colspan="11" class="text-center p-4 text-gray-500">Data or sorting order missing.</td></tr>'; |
|
|
return; |
|
|
} |
|
|
matrixBody.innerHTML = ''; |
|
|
|
|
|
|
|
|
const headerRow = document.querySelector('thead tr'); |
|
|
|
|
|
while (headerRow.children.length > 1) { |
|
|
headerRow.removeChild(headerRow.lastChild); |
|
|
} |
|
|
|
|
|
sortedSchedulers.forEach(scheduler => { |
|
|
const th = document.createElement('th'); |
|
|
th.className = "p-3 text-center font-medium text-gray-700 bg-white border-b min-w-[100px]"; |
|
|
th.textContent = scheduler; |
|
|
headerRow.appendChild(th); |
|
|
}); |
|
|
|
|
|
|
|
|
sortedSamplers.forEach(sampler => { |
|
|
|
|
|
if (!normalizedData[sampler]) { |
|
|
console.warn(`Sampler "${sampler}" from samplers.txt not found in data.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
const row = document.createElement('tr'); |
|
|
row.className = 'sampler-row hover:bg-gray-50'; |
|
|
|
|
|
|
|
|
const samplerCell = document.createElement('td'); |
|
|
samplerCell.className = 'sticky-col p-3 text-left font-medium text-gray-700 bg-white border-r'; |
|
|
samplerCell.textContent = sampler; |
|
|
row.appendChild(samplerCell); |
|
|
|
|
|
|
|
|
sortedSchedulers.forEach(scheduler => { |
|
|
const value = normalizedData[sampler]?.[scheduler]; |
|
|
const cell = document.createElement('td'); |
|
|
const displayValue = (value !== undefined && value !== null && isFinite(value)) ? value.toFixed(2) : '-'; |
|
|
|
|
|
|
|
|
cell.className = `heatmap-cell p-3 text-center border-b ${getColor(value, sampler, scheduler)}`; |
|
|
cell.textContent = displayValue; |
|
|
|
|
|
|
|
|
if (value !== undefined && value !== null && isFinite(value)) { |
|
|
const originalValue = originalData[sampler]?.[scheduler]; |
|
|
const originalDisplay = originalValue !== undefined ? ` (${originalValue.toFixed(2)}s)` : ''; |
|
|
cell.setAttribute('data-value', `${sampler} + ${scheduler}: ${displayValue} (norm)${originalDisplay}`); |
|
|
} else { |
|
|
cell.setAttribute('data-value', `${sampler} + ${scheduler}: N/A`); |
|
|
} |
|
|
row.appendChild(cell); |
|
|
}); |
|
|
|
|
|
matrixBody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function calculateInsights() { |
|
|
if (Object.keys(normalizedData).length === 0) return; |
|
|
|
|
|
const allCombinations = []; |
|
|
const consistency = {}; |
|
|
|
|
|
|
|
|
for (const sampler in normalizedData) { |
|
|
|
|
|
|
|
|
|
|
|
let samplerValues = []; |
|
|
let samplerSum = 0; |
|
|
let samplerCount = 0; |
|
|
|
|
|
for (const scheduler in normalizedData[sampler]) { |
|
|
|
|
|
|
|
|
|
|
|
const value = normalizedData[sampler][scheduler]; |
|
|
if (value === undefined || value === null || !isFinite(value)) continue; |
|
|
|
|
|
allCombinations.push({ sampler, scheduler, value }); |
|
|
|
|
|
|
|
|
|
|
|
samplerValues.push(value); |
|
|
samplerSum += value; |
|
|
samplerCount++; |
|
|
|
|
|
} |
|
|
|
|
|
if (samplerCount > 0) { |
|
|
const mean = samplerSum / samplerCount; |
|
|
let variance = 0; |
|
|
samplerValues.forEach(v => { variance += Math.pow(v - mean, 2); }); |
|
|
variance /= samplerCount; |
|
|
const stdDev = Math.sqrt(variance); |
|
|
const cv = (mean > 0) ? (stdDev / mean) : (stdDev === 0 ? 0 : Infinity); |
|
|
consistency[sampler] = { stdDev, mean, cv }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
allCombinations.sort((a, b) => a.value - b.value); |
|
|
const top5Fastest = allCombinations.slice(0, 5); |
|
|
|
|
|
|
|
|
const top5Slowest = allCombinations.slice(-5).reverse(); |
|
|
|
|
|
|
|
|
let mostConsistent = { cv: Infinity, sampler: '', stdDev: NaN }; |
|
|
for (const sampler in consistency) { |
|
|
if (consistency[sampler].cv < mostConsistent.cv) { |
|
|
mostConsistent = { |
|
|
cv: consistency[sampler].cv, |
|
|
sampler: sampler, |
|
|
stdDev: consistency[sampler].stdDev |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const topPerformerEl = document.getElementById('topPerformer'); |
|
|
const mostConsistentEl = document.getElementById('mostConsistent'); |
|
|
const slowestEl = document.getElementById('slowest'); |
|
|
|
|
|
if (topPerformerEl) { |
|
|
topPerformerEl.innerHTML = top5Fastest.map(item => |
|
|
`<span>${item.sampler} + ${item.scheduler}: <b>${item.value.toFixed(2)}</b></span>` |
|
|
).join('<br>'); |
|
|
if (top5Fastest.length === 0) topPerformerEl.textContent = 'N/A'; |
|
|
} |
|
|
|
|
|
if (mostConsistentEl) { |
|
|
mostConsistentEl.textContent = isFinite(mostConsistent.cv) |
|
|
? `${mostConsistent.sampler} (Norm. CV: ${mostConsistent.cv.toFixed(2)})` |
|
|
: 'N/A'; |
|
|
} |
|
|
|
|
|
if (slowestEl) { |
|
|
slowestEl.innerHTML = top5Slowest.map(item => |
|
|
`<span>${item.sampler} + ${item.scheduler}: <b>${item.value.toFixed(2)}</b></span>` |
|
|
).join('<br>'); |
|
|
if (top5Slowest.length === 0) slowestEl.textContent = 'N/A'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function toggleHeatmap() { |
|
|
heatmapEnabled = !heatmapEnabled; |
|
|
const cells = document.querySelectorAll('.heatmap-cell'); |
|
|
cells.forEach(cell => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const valueText = cell.textContent; |
|
|
let value = NaN; |
|
|
if (valueText && valueText !== '-' && !isNaN(parseFloat(valueText))) { |
|
|
value = parseFloat(valueText); |
|
|
} |
|
|
|
|
|
|
|
|
let sampler = ''; |
|
|
let scheduler = ''; |
|
|
const row = cell.closest('tr'); |
|
|
if (row) { |
|
|
sampler = row.cells[0]?.textContent || ''; |
|
|
const colIndex = Array.from(cell.parentNode.children).indexOf(cell); |
|
|
scheduler = document.querySelector(`thead th:nth-child(${colIndex + 1})`)?.textContent || ''; |
|
|
} |
|
|
|
|
|
|
|
|
cell.classList.remove( |
|
|
'bg-emerald-400', 'bg-emerald-300', 'bg-green-300', 'bg-lime-300', |
|
|
'bg-yellow-300', 'bg-amber-300', 'bg-orange-400', 'bg-red-400', 'bg-red-500', |
|
|
'bg-gray-100', 'bg-white', 'border', 'border-gray-400' |
|
|
); |
|
|
|
|
|
|
|
|
cell.classList.add(getColor(value, sampler, scheduler)); |
|
|
}); |
|
|
|
|
|
|
|
|
const toggleButton = document.getElementById('toggleHeatmap'); |
|
|
if (toggleButton) { |
|
|
if (heatmapEnabled) { |
|
|
toggleButton.innerHTML = '<i class="fas fa-ban mr-1"></i> Disable Heatmap'; |
|
|
toggleButton.classList.remove('bg-red-100', 'text-red-600', 'hover:bg-red-200'); |
|
|
toggleButton.classList.add('bg-blue-100', 'text-blue-600', 'hover:bg-blue-200'); |
|
|
} else { |
|
|
toggleButton.innerHTML = '<i class="fas fa-fire mr-1"></i> Enable Heatmap'; |
|
|
toggleButton.classList.remove('bg-blue-100', 'text-blue-600', 'hover:bg-blue-200'); |
|
|
toggleButton.classList.add('bg-red-100', 'text-red-600', 'hover:bg-red-200'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setupSearch() { |
|
|
const searchInput = document.getElementById('searchInput'); |
|
|
if (!searchInput) return; |
|
|
|
|
|
searchInput.addEventListener('input', () => { |
|
|
const searchTerm = searchInput.value.toLowerCase().trim(); |
|
|
const rows = document.querySelectorAll('#matrixBody .sampler-row'); |
|
|
|
|
|
rows.forEach(row => { |
|
|
const samplerNameCell = row.cells[0]; |
|
|
if (samplerNameCell) { |
|
|
const samplerName = samplerNameCell.textContent.toLowerCase(); |
|
|
if (samplerName.includes(searchTerm)) { |
|
|
row.style.display = ''; |
|
|
} else { |
|
|
row.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async function initializeApp() { |
|
|
try { |
|
|
|
|
|
const [samplerList, schedulerList, jsonDataResponse] = await Promise.all([ |
|
|
fetchTextList('samplers.txt'), |
|
|
fetchTextList('schedulers.txt'), |
|
|
fetch('performance_data.json') |
|
|
]); |
|
|
|
|
|
sortedSamplers = samplerList; |
|
|
sortedSchedulers = schedulerList; |
|
|
|
|
|
if (!jsonDataResponse.ok) { |
|
|
throw new Error(`HTTP error! status: ${jsonDataResponse.status} for performance_data.json`); |
|
|
} |
|
|
originalData = await jsonDataResponse.json(); |
|
|
|
|
|
|
|
|
if (sortedSamplers.length === 0) console.warn("Sampler sorting list is empty."); |
|
|
if (sortedSchedulers.length === 0) console.warn("Scheduler sorting list is empty."); |
|
|
if (Object.keys(originalData).length === 0) { |
|
|
throw new Error("Fetched performance data is empty."); |
|
|
} |
|
|
|
|
|
|
|
|
normalizedData = normalizeData(originalData); |
|
|
|
|
|
if (Object.keys(normalizedData).length === 0) { |
|
|
|
|
|
if (Object.keys(originalData).length > 0) { |
|
|
console.warn("Normalization failed, using original data structure but results might be unexpected."); |
|
|
normalizedData = originalData; |
|
|
} else { |
|
|
throw new Error("Normalization failed and original data is empty."); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
calculateInsights(); |
|
|
|
|
|
|
|
|
generateTable(); |
|
|
|
|
|
|
|
|
setupSearch(); |
|
|
|
|
|
|
|
|
const toggleButton = document.getElementById('toggleHeatmap'); |
|
|
if (toggleButton) { |
|
|
if (heatmapEnabled) { |
|
|
toggleButton.innerHTML = '<i class="fas fa-ban mr-1"></i> Disable Heatmap'; |
|
|
toggleButton.classList.add('bg-blue-100', 'text-blue-600', 'hover:bg-blue-200'); |
|
|
} else { |
|
|
toggleButton.innerHTML = '<i class="fas fa-fire mr-1"></i> Enable Heatmap'; |
|
|
toggleButton.classList.add('bg-red-100', 'text-red-600', 'hover:bg-red-200'); |
|
|
} |
|
|
toggleButton.addEventListener('click', toggleHeatmap); |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Failed to initialize app:", error); |
|
|
const matrixBody = document.getElementById('matrixBody'); |
|
|
if (matrixBody) { |
|
|
matrixBody.innerHTML = `<tr><td colspan="11" class="text-center p-4 text-red-500">Error initializing: ${error.message}</td></tr>`; |
|
|
} |
|
|
document.getElementById('topPerformer').textContent = 'Error'; |
|
|
document.getElementById('mostConsistent').textContent = 'Error'; |
|
|
document.getElementById('slowest').textContent = 'Error'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', initializeApp); |
|
|
</script> |
|
|
</body> |
|
|
</html> |