From 60990acc6e9606fa84ad1cf8aafd45ad00f14421 Mon Sep 17 00:00:00 2001 From: Mika Date: Mon, 12 Jan 2026 11:39:19 +0000 Subject: [PATCH] Add spike_visualizer/js/app.js --- spike_visualizer/js/app.js | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 spike_visualizer/js/app.js diff --git a/spike_visualizer/js/app.js b/spike_visualizer/js/app.js new file mode 100644 index 0000000..02b5fb4 --- /dev/null +++ b/spike_visualizer/js/app.js @@ -0,0 +1,133 @@ +'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); \ No newline at end of file