/** * Waterfall Chart Renderer * Visualizes network requests timeline with interactive details */ let currentHarData = null; let currentFilter = 'all'; // Load HAR data from URL parameter async function init() { const params = new URLSearchParams(window.location.search); const testId = params.get('id'); if (!testId) { document.getElementById('waterfallCanvas').innerHTML = '

Error: No test ID provided

'; return; } try { const response = await fetch(`/reports/${testId}.har.json`); if (!response.ok) throw new Error('HAR data not found'); currentHarData = await response.json(); renderWaterfall(); setupEventListeners(); } catch (error) { document.getElementById('waterfallCanvas').innerHTML = `

Error loading waterfall data: ${error.message}

`; } } function renderWaterfall() { const canvas = document.getElementById('waterfallCanvas'); const entries = currentHarData.entries; if (!entries || entries.length === 0) { canvas.innerHTML = '

No requests found

'; return; } // Calculate timeline scale const maxTime = Math.max(...entries.map(e => e.timing.endTime)); const scale = 1000 / maxTime; // pixels per second // Filter entries based on active filter const filteredEntries = currentFilter === 'all' ? entries : entries.filter(e => e.resourceType === currentFilter); let html = ''; filteredEntries.forEach((entry, index) => { const label = truncateUrl(entry.url, 50); const timingBars = renderTimingBars(entry.timing, scale); html += `
${label}
${timingBars}
`; }); canvas.innerHTML = html; // Attach click handlers document.querySelectorAll('.waterfall-row').forEach(row => { row.addEventListener('click', () => { const requestId = parseInt(row.dataset.requestId); showRequestDetails(requestId); }); }); } function renderTimingBars(timing, scale) { let html = ''; let currentOffset = timing.startTime * scale; // DNS if (timing.dns > 0) { const width = timing.dns * scale / 1000; html += `
`; currentOffset += width; } // Connect if (timing.connect > 0) { const width = timing.connect * scale / 1000; html += `
`; currentOffset += width; } // SSL if (timing.ssl > 0) { const width = timing.ssl * scale / 1000; html += `
`; currentOffset += width; } // Wait (TTFB) if (timing.wait > 0) { const width = timing.wait * scale / 1000; html += `
`; currentOffset += width; } // Receive const totalWidth = (timing.endTime - timing.startTime) * scale; const receiveWidth = Math.max(totalWidth - (currentOffset - timing.startTime * scale), 0); if (receiveWidth > 0) { html += `
`; } return html; } function showRequestDetails(requestId) { const entry = currentHarData.entries.find(e => e.requestId === requestId); if (!entry) return; document.getElementById('dialogTitle').textContent = `Request #${requestId}`; const content = `

General

URL: ${entry.url}
Domain: ${entry.domain}
Method: ${entry.method}
Status: ${entry.status}
Type: ${entry.resourceType}
Protocol: ${entry.protocol || 'N/A'}
Priority: ${entry.priority || 'N/A'}
${entry.isThirdParty ? '
⚠️ Third-Party Resource
' : ''} ${entry.renderBlocking ? '
🚫 Render Blocking
' : ''}

Timing

${entry.timing.dns > 0 ? `
DNS Lookup:${entry.timing.dns.toFixed(2)} ms
` : ''} ${entry.timing.connect > 0 ? `
Connection:${entry.timing.connect.toFixed(2)} ms
` : ''} ${entry.timing.ssl > 0 ? `
SSL:${entry.timing.ssl.toFixed(2)} ms
` : ''} ${entry.timing.wait > 0 ? `
Time to First Byte:${entry.timing.wait.toFixed(2)} ms
` : ''}
Total Time: ${entry.timing.total.toFixed(2)} ms

Size

Transfer Size: ${formatBytes(entry.size.transferSize)}
Resource Size: ${formatBytes(entry.size.resourceSize)}
${entry.size.compressionRatio < 1 ? `
Compression: ${(entry.size.compressionRatio * 100).toFixed(1)}% (${formatBytes(entry.size.resourceSize - entry.size.transferSize)} saved)
` : ''}
`; document.getElementById('dialogContent').innerHTML = content; document.getElementById('requestDialog').style.display = 'block'; document.getElementById('dialogOverlay').style.display = 'block'; } function setupEventListeners() { // Filter buttons document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentFilter = btn.dataset.type; renderWaterfall(); }); }); // Close dialog document.getElementById('closeDialog').addEventListener('click', closeDialog); document.getElementById('dialogOverlay').addEventListener('click', closeDialog); } function closeDialog() { document.getElementById('requestDialog').style.display = 'none'; document.getElementById('dialogOverlay').style.display = 'none'; } function truncateUrl(url, maxLength) { if (url.length <= maxLength) return url; const parts = url.split('/'); return parts[parts.length - 1].substring(0, maxLength) + '...'; } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Initialize on page load document.addEventListener('DOMContentLoaded', init);