diff --git a/spike_visualizer/js/visualization.js b/spike_visualizer/js/visualization.js new file mode 100644 index 0000000..ecf28d2 --- /dev/null +++ b/spike_visualizer/js/visualization.js @@ -0,0 +1,128 @@ +'use strict'; + +(function(global){ + const Visualization = {}; + + // Internal helpers + function clearSVG(svg){ + if(!svg) return; + svg.selectAll('*').remove(); + } + + function createScales(data, width, height, xField, yField){ + const xExtent = d3.extent(data, d => d[xField]); + const yExtent = d3.extent(data, d => d[yField]); + + return { + x: d3.scaleTime().domain(xExtent).range([50, width - 20]), + y: d3.scaleLinear().domain(yExtent).range([height - 40, 20]) + }; + } + + // --- renderTimeline --- + Visualization.renderTimeline = function(spikeData, svgContainer){ + if(!svgContainer || !Array.isArray(spikeData)) return; + clearSVG(svgContainer); + + const width = +svgContainer.attr('width') || 800; + const height = +svgContainer.attr('height') || 400; + + const grouped = d3.group(spikeData, d => d.cpu_id); + const cpus = Array.from(grouped.keys()); + const timeExtent = d3.extent(spikeData, d => d.timestamp); + + const xScale = d3.scaleTime().domain(timeExtent).range([80, width - 40]); + const yScale = d3.scaleBand().domain(cpus).range([40, height - 40]).padding(0.3); + + const colorScale = d3.scaleSequential(d3.interpolateRdYlBu) + .domain(d3.extent(spikeData, d => d.reorder_score)); + + // Axes + svgContainer.append('g') + .attr('transform', `translate(0,${height - 40})`) + .call(d3.axisBottom(xScale).ticks(6)); + + svgContainer.append('g') + .attr('transform', 'translate(80,0)') + .call(d3.axisLeft(yScale)); + + const tooltip = d3.select('body').append('div') + .attr('class', 'tooltip') + .style('opacity', 0) + .style('position', 'absolute') + .style('pointer-events', 'none'); + + svgContainer.selectAll('.spike-dot') + .data(spikeData) + .enter() + .append('circle') + .attr('class', 'spike-dot') + .attr('cx', d => xScale(d.timestamp)) + .attr('cy', d => yScale(d.cpu_id) + yScale.bandwidth() / 2) + .attr('r', 4) + .attr('fill', d => colorScale(d.reorder_score)) + .attr('stroke', d => d.migration_flag ? '#444' : 'none') + .on('mouseover', function(event, d){ + tooltip.transition().duration(200).style('opacity', 0.95); + tooltip.html(`CPU: ${d.cpu_id}
Reorder: ${d.reorder_score}
Publish: ${d.publish_rate}`) + .style('left', (event.pageX + 5) + 'px') + .style('top', (event.pageY - 28) + 'px'); + }) + .on('mouseout', function(){ + tooltip.transition().duration(300).style('opacity', 0); + }); + }; + + // --- renderScatter --- + Visualization.renderScatter = function(spikeData, filterState){ + const container = d3.select('#chart-container'); + if(container.empty() || !Array.isArray(spikeData)) return; + + container.selectAll('*').remove(); + const width = 800; + const height = 400; + + const svg = container.append('svg').attr('width', width).attr('height', height); + const scales = createScales(spikeData, width, height, 'timestamp', 'reorder_score'); + + svg.append('g') + .attr('transform', `translate(0,${height - 40})`) + .call(d3.axisBottom(scales.x).ticks(6)); + + svg.append('g') + .attr('transform', 'translate(50,0)') + .call(d3.axisLeft(scales.y)); + + const filtered = spikeData.filter(d => { + let include = true; + if(filterState && filterState.cpu_id) include = include && d.cpu_id === filterState.cpu_id; + if(filterState && filterState.min_score != null) include = include && d.reorder_score >= filterState.min_score; + if(filterState && filterState.max_score != null) include = include && d.reorder_score <= filterState.max_score; + return include; + }); + + svg.selectAll('.scatter-dot') + .data(filtered) + .enter() + .append('circle') + .attr('class', 'scatter-dot') + .attr('cx', d => scales.x(d.timestamp)) + .attr('cy', d => scales.y(d.reorder_score)) + .attr('r', 4) + .attr('fill', '#4287f5') + .attr('opacity', 0.75); + }; + + // --- updateView --- + Visualization.updateView = function(filters, newData){ + const svg = d3.select('#timeline'); + if(filters && filters.mode === 'scatter') { + Visualization.renderScatter(newData, filters); + } else { + Visualization.renderTimeline(newData, svg); + } + }; + + global.Visualization = Visualization; + +})(window); \ No newline at end of file