Add spike_visualizer/js/visualization.js
This commit is contained in:
parent
abf05c1fe1
commit
f1025216f1
1 changed files with 128 additions and 0 deletions
128
spike_visualizer/js/visualization.js
Normal file
128
spike_visualizer/js/visualization.js
Normal 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);
|
||||||
Loading…
Reference in a new issue