/**
* 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 += `
`;
});
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);