/** * 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 = ''; // Add time scale header with grid lines html += '
'; for (let sec = 0; sec <= Math.ceil(maxTime); sec++) { const pos = sec * scale; html += `
${sec}s
`; html += `
`; } html += '
'; filteredEntries.forEach((entry, index) => { const label = truncateUrl(entry.url, 30); const timingBars = renderTimingBars(entry.timing, scale); const statusColor = getStatusColor(entry.status); const typeBadge = getResourceTypeBadge(entry.resourceType); const sizeKB = (entry.size.transferSize / 1024).toFixed(1); const timeMS = entry.timing.total.toFixed(0); html += `
${entry.requestId}
${entry.status}
${typeBadge}
${label}
${timingBars}
${sizeKB} KB
${timeMS} ms
`; }); canvas.innerHTML = html; // Attach click handlers document.querySelectorAll('.waterfall-row').forEach(row => { row.addEventListener('click', () => { const requestId = parseInt(row.dataset.requestId); showRequestDetails(requestId); }); }); } function getStatusColor(status) { if (status >= 200 && status < 300) return '#4CAF50'; // Green if (status >= 300 && status < 400) return '#FF9800'; // Orange if (status >= 400 && status < 500) return '#F44336'; // Red if (status >= 500) return '#B71C1C'; // Dark Red return '#9E9E9E'; // Grey for others } function getResourceTypeBadge(type) { const badges = { 'Document': 'HTML', 'Stylesheet': 'CSS', 'Script': 'JS', 'Image': 'IMG', 'Font': 'FONT', 'XHR': 'XHR', 'Fetch': 'API' }; return badges[type] || 'OTHER'; } 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);