'use strict';
async function initApp() {
const summaryContainer = document.getElementById('summary');
const chartContainer = document.getElementById('chart');
const filterPanel = document.getElementById('filter-panel');
if (!summaryContainer || !chartContainer || !filterPanel) {
console.error('Required DOM elements missing.');
return;
}
try {
const dataset = await fetchSpikeData();
renderSummary(dataset, summaryContainer);
renderChart(dataset, chartContainer);
bindUIEvents(dataset);
} catch (err) {
console.error('Failed to initialize app:', err);
}
}
async function fetchSpikeData(params = {}) {
const query = new URLSearchParams(params).toString();
const response = await fetch('/api/spikes' + (query ? '?' + query : ''));
if (!response.ok) {
throw new Error('API request failed with status ' + response.status);
}
return response.json();
}
function renderSummary(data, container) {
if (!Array.isArray(data)) return;
const total = data.length;
const avgReorder = total ? (data.reduce((sum, d) => sum + (d.reorder_score || 0), 0) / total).toFixed(2) : 0;
const avgPublish = total ? (data.reduce((sum, d) => sum + (d.publish_rate || 0), 0) / total).toFixed(2) : 0;
container.innerHTML = `
Total Spikes: ${total}
Average Reorder Score: ${avgReorder}
Average Publish Rate: ${avgPublish}
`;
}
function renderChart(data, container) {
if (typeof d3 === 'undefined') {
container.textContent = 'Visualization library (d3.js) not loaded.';
return;
}
container.innerHTML = '';
const width = container.clientWidth || 800;
const height = 300;
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.timestamp))
.range([40, width - 20]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.reorder_score || 0)])
.range([height - 40, 20]);
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => xScale(d.timestamp))
.attr('cy', d => yScale(d.reorder_score))
.attr('r', 4)
.attr('fill', d => d.migration_flag ? '#d95f02' : '#1b9e77')
.on('mouseover', (event, d) => {
showTooltip(event, d, container);
})
.on('mouseout', () => {
hideTooltip(container);
});
}
function showTooltip(event, data, container) {
let tooltip = container.querySelector('.tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.className = 'tooltip';
container.appendChild(tooltip);
}
tooltip.innerHTML = `CPU ${data.cpu_id}
Score: ${data.reorder_score}
Rate: ${data.publish_rate}
Migration: ${data.migration_flag}`;
tooltip.style.visibility = 'visible';
tooltip.style.left = `${event.offsetX + 10}px`;
tooltip.style.top = `${event.offsetY - 20}px`;
}
function hideTooltip(container) {
const tooltip = container.querySelector('.tooltip');
if (tooltip) {
tooltip.style.visibility = 'hidden';
}
}
function bindUIEvents(initialData) {
const cpuFilter = document.getElementById('cpu-filter');
const reorderFilter = document.getElementById('reorder-filter');
const timeFilterStart = document.getElementById('start-time');
const timeFilterEnd = document.getElementById('end-time');
const chartContainer = document.getElementById('chart');
const summaryContainer = document.getElementById('summary');
if (!cpuFilter || !chartContainer) return;
const updateChart = async () => {
const params = {};
if (cpuFilter.value) params.cpu_id = cpuFilter.value;
if (timeFilterStart.value) params.start_time = timeFilterStart.value;
if (timeFilterEnd.value) params.end_time = timeFilterEnd.value;
try {
const newData = await fetchSpikeData(params);
renderSummary(newData, summaryContainer);
renderChart(newData, chartContainer);
} catch (err) {
console.error('Failed to update chart:', err);
}
};
[cpuFilter, reorderFilter, timeFilterStart, timeFilterEnd].forEach(el => {
if (el) el.addEventListener('change', updateChart);
});
}
window.addEventListener('load', initApp);