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