'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);