Files

496 lines
14 KiB
JavaScript

const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(`${protocol}//${window.location.host}`);
socket.addEventListener('open', () => {
console.log('WebSocket connection established');
});
socket.addEventListener('close', () => {
console.log('WebSocket connection closed');
});
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
const nodes = new vis.DataSet();
const edges = new vis.DataSet();
const container = document.getElementById('network');
const data = {
nodes: nodes,
edges: edges
};
const DEFAULT_COLOR = '#e0e0e0'; // Very light grey
const MIN_COLOR_DISTANCE = 100; // Minimum color distance for new colors
let pieChart = null;
let colorDistribution = {};
let showPieChart = false; // Hide pie chart by default
let showOutlierHalos = true; // Show outlier halos by default
let nodesToUpdate = [];
let edgesToUpdate = [];
// Create explanation text
const explanationText = document.createElement('div');
explanationText.id = 'explanationText';
explanationText.style.position = 'fixed';
explanationText.style.bottom = '10px';
explanationText.style.left = '10px';
explanationText.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
explanationText.style.color = 'white';
explanationText.style.padding = '10px';
explanationText.style.borderRadius = '5px';
explanationText.style.fontFamily = 'Arial, sans-serif';
explanationText.style.fontSize = '14px';
explanationText.style.zIndex = '1000';
explanationText.innerHTML = `
<div>DST Visualiser</div>
<div>Toggle outlier halos: H</div>
<div>Toggle pie chart: P</div>
`;
document.body.appendChild(explanationText);
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
}
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function updatePieChart() {
if (!showPieChart) {
document.getElementById('pieChartContainer').style.display = 'none';
document.getElementById('colorDistribution').style.display = 'none';
return;
}
const pieChartContainer = document.getElementById('pieChartContainer');
const colorDistributionElement = document.getElementById('colorDistribution');
pieChartContainer.style.display = 'block';
colorDistributionElement.style.display = 'block';
pieChartContainer.style.zIndex = '10000';
pieChartContainer.style.width = '400px';
pieChartContainer.style.height = '450px';
pieChartContainer.style.backgroundColor = 'white';
pieChartContainer.style.padding = '10px';
pieChartContainer.style.borderRadius = '10px';
const currentColorDistribution = {};
const allNodes = nodes.get();
allNodes.forEach(node => {
const color = node.color.background;
currentColorDistribution[color] = (currentColorDistribution[color] || 0) + 1;
});
const colors = Object.keys(currentColorDistribution);
const data = colors.map(color => currentColorDistribution[color]);
const totalNodes = data.reduce((a, b) => a + b, 0);
if (!pieChart) {
const ctx = document.getElementById('pieChart').getContext('2d');
pieChart = new Chart(ctx, {
type: 'pie',
data: {
labels: colors,
datasets: [{
data: data,
backgroundColor: colors
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right',
labels: {
color: 'black'
}
},
tooltip: {
enabled: false
}
}
},
plugins: [{
afterDraw: function(chart) {
var ctx = chart.ctx;
ctx.save();
var centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
var centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
chart.data.datasets.forEach(function(dataset, datasetIndex) {
var meta = chart.getDatasetMeta(datasetIndex);
if (!meta.hidden) {
meta.data.forEach(function(element, index) {
var model = element.getProps(['startAngle', 'endAngle', 'circumference', 'outerRadius', 'x', 'y']);
var midAngle = model.startAngle + (model.endAngle - model.startAngle) / 2;
var x = centerX + Math.cos(midAngle) * (model.outerRadius * 0.7);
var y = centerY + Math.sin(midAngle) * (model.outerRadius * 0.7);
ctx.font = 'bold 12px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var percent = ((dataset.data[index] / totalNodes) * 100).toFixed(1) + '%';
ctx.fillText(percent, x, y);
});
}
});
ctx.restore();
}
}]
});
} else {
pieChart.data.labels = colors;
pieChart.data.datasets[0].data = data;
pieChart.data.datasets[0].backgroundColor = colors;
pieChart.update();
}
const distributionText = colors.map(color => {
const count = currentColorDistribution[color];
const percentage = ((count / totalNodes) * 100).toFixed(1);
return `${count}/${totalNodes} (${percentage}%) nodes are ${color}`;
}).join('<br>');
colorDistributionElement.innerHTML = distributionText;
colorDistributionElement.style.fontSize = '12px';
colorDistributionElement.style.marginTop = '10px';
colorDistributionElement.style.color = 'black';
}
// Update this to not show the pie chart when the page loads
window.addEventListener('load', () => {
updatePieChart(); // This will hide the pie chart initially
resizeAndCenterNetwork(); // Ensure the network is resized and centered when the page loads
});
document.addEventListener('keydown', (event) => {
if (event.key === 'p' || event.key === 'P') {
showPieChart = !showPieChart;
updatePieChart();
console.log(showPieChart ? 'Pie chart is now visible' : 'Pie chart is now hidden');
} else if (event.key === 'h' || event.key === 'H') {
showOutlierHalos = !showOutlierHalos;
updateCounter(); // This will update the halos
console.log(showOutlierHalos ? 'Outlier halos are now visible' : 'Outlier halos are now hidden');
}
});
const options = {
width: '100%',
height: '100%',
physics: {
enabled: true,
solver: 'forceAtlas2Based',
forceAtlas2Based: {
gravitationalConstant: -50,
centralGravity: 0.01,
springConstant: 0.08,
springLength: 80, // Reduced from 100 to 80 (20% closer)
damping: 0.4,
avoidOverlap: 0
},
stabilization: {
enabled: true,
iterations: 1000,
updateInterval: 100,
onlyDynamicEdges: false,
fit: true
}
},
nodes: {
shape: 'circle',
size: 28,
borderWidth: 0,
borderWidthSelected: 2,
color: {
background: '#e0e0e0',
border: '#e0e0e0'
}
},
edges: {
width: 1,
color: { color: '#ffffff', opacity: 0.5 }
}
};
// Set the background color to black
document.body.style.backgroundColor = '#000000';
// Create counter display
const counterDisplay = document.createElement('div');
counterDisplay.id = 'counterDisplay';
counterDisplay.style.position = 'fixed';
counterDisplay.style.top = '10px';
counterDisplay.style.right = '10px';
counterDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
counterDisplay.style.color = 'white';
counterDisplay.style.padding = '10px';
counterDisplay.style.borderRadius = '5px';
counterDisplay.style.fontFamily = 'Arial, sans-serif';
counterDisplay.style.fontSize = '14px';
counterDisplay.style.zIndex = '1000';
document.body.appendChild(counterDisplay);
let smallGroups = new Set();
let colorCounts = {};
let totalNodes = 0;
function calculateSmallGroups() {
colorCounts = {};
totalNodes = nodes.length;
nodes.get().forEach(node => {
const color = node.color.background;
colorCounts[color] = (colorCounts[color] || 0) + 1;
});
smallGroups = new Set();
Object.entries(colorCounts).forEach(([color, count]) => {
const percentage = ((count / totalNodes) * 100);
if (percentage < 5) {
smallGroups.add(color);
}
});
}
const updateCounter = debounce(() => {
let counterHTML = '';
Object.entries(colorCounts).forEach(([color, count]) => {
const percentage = ((count / totalNodes) * 100).toFixed(1);
counterHTML += `<div><span style="color:${color};">■</span> ${count} (${percentage}%)</div>`;
});
counterHTML += `<div>Total: ${totalNodes}</div>`;
counterDisplay.innerHTML = counterHTML;
// Update node appearances based on small groups
nodes.get().forEach(node => {
if (showOutlierHalos && smallGroups.has(node.color.background)) {
const haloColor = getContrastingColor(node.color.background);
nodesToUpdate.push({
id: node.id,
borderWidth: 3,
borderWidthSelected: 0,
color: {
...node.color,
border: haloColor
}
});
} else if (node.borderWidth !== 0 || node.color.border !== node.color.background) {
nodesToUpdate.push({
id: node.id,
borderWidth: 0,
borderWidthSelected: 2,
color: {
...node.color,
border: node.color.background
}
});
}
});
}, 100);
const network = new vis.Network(container, data, options);
// Function to calculate the center of all nodes
function calculateNetworkCenter() {
const nodePositions = network.getPositions();
const nodeIds = Object.keys(nodePositions);
if (nodeIds.length === 0) return { x: 0, y: 0 };
const sum = nodeIds.reduce((acc, id) => {
acc.x += nodePositions[id].x;
acc.y += nodePositions[id].y;
return acc;
}, { x: 0, y: 0 });
return {
x: sum.x / nodeIds.length,
y: sum.y / nodeIds.length
};
}
// Ensure the network takes up the full browser window and center the view
function resizeAndCenterNetwork() {
container.style.width = '100vw';
container.style.height = '100vh';
const center = calculateNetworkCenter();
network.moveTo({
position: center,
scale: 1,
animation: {
duration: 1000,
easingFunction: 'easeInOutQuad'
}
});
}
window.addEventListener('resize', resizeAndCenterNetwork);
// Call this function once to center the network initially
// Removed the stabilizationIterationsDone event listener to avoid redundant calls to resizeAndCenterNetwork
let lastPieChartUpdateTime = 0;
const pieChartUpdateInterval = 1000; // Update pie chart every second
function updateNodes() {
if (nodesToUpdate.length > 0) {
nodes.update(nodesToUpdate);
nodesToUpdate = [];
}
if (edgesToUpdate.length > 0) {
edges.update(edgesToUpdate);
edgesToUpdate = [];
}
calculateSmallGroups();
updateCounter();
const currentTime = performance.now();
if (currentTime - lastPieChartUpdateTime >= pieChartUpdateInterval) {
updatePieChart();
lastPieChartUpdateTime = currentTime;
}
requestAnimationFrame(updateNodes);
}
updateNodes();
const processUpdates = (batchedUpdates) => {
batchedUpdates.forEach(logData => {
const { msg_hash, receivedTime, peerId, newNode } = logData;
if (newNode) {
nodesToUpdate.push({
id: peerId,
title: `Node ${peerId}`,
borderWidth: 0,
borderWidthSelected: 2,
color: {
background: DEFAULT_COLOR,
border: DEFAULT_COLOR,
highlight: {
background: DEFAULT_COLOR,
border: DEFAULT_COLOR
}
}
});
}
const colorHex = generateColorFromHash(msg_hash);
nodesToUpdate.push({
id: peerId,
color: {
background: colorHex,
highlight: {
background: colorHex
}
}
});
// Add or update edge
const edgeId = `${msg_hash}-${peerId}`;
if (!edges.get(edgeId)) {
edgesToUpdate.push({
id: edgeId,
from: msg_hash,
to: peerId,
arrows: 'to',
color: { color: '#ffffff', opacity: 0.5 }
});
}
});
};
socket.addEventListener('message', (event) => {
const batchedUpdates = JSON.parse(event.data);
processUpdates(batchedUpdates);
});
// Initial counter update
updateCounter();
function generateColorFromHash(hash) {
const colorInt = parseInt(hash.slice(2), 16);
return '#' + (colorInt % 0xFFFFFF).toString(16).padStart(6, '0');
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function rgbToHex(rgb) {
return "#" + ((1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b).toString(16).slice(1);
}
function getContrastingColor(hexColor) {
const nodeColor = hexToRgb(hexColor);
const options = [
{ name: 'cyan', color: { r: 0, g: 255, b: 255 } },
{ name: 'bright pink', color: { r: 255, g: 20, b: 147 } },
{ name: 'bright green', color: { r: 0, g: 255, b: 0 } },
{ name: 'white', color: { r: 255, g: 255, b: 255 } }
];
let bestColor = options[0];
let maxDistance = 0;
options.forEach(option => {
const distance = colorDistance(nodeColor, option.color);
if (distance > maxDistance) {
maxDistance = distance;
bestColor = option;
}
});
return rgbToHex(bestColor.color);
}
function colorDistance(rgb1, rgb2) {
return Math.sqrt(
Math.pow(rgb1.r - rgb2.r, 2) +
Math.pow(rgb1.g - rgb2.g, 2) +
Math.pow(rgb1.b - rgb2.b, 2)
);
}