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