Add spike_visualizer/js/visualization.js

This commit is contained in:
Mika 2026-01-12 11:39:20 +00:00
parent abf05c1fe1
commit f1025216f1

View file

@ -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}<br>Reorder: ${d.reorder_score}<br>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);