363 lines
10 KiB
JavaScript
363 lines
10 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; // Changed to false to hide by default
|
|
let updatePieChartThrottled = throttle(updatePieChart, 200);
|
|
let nodesToUpdate = [];
|
|
|
|
function throttle(func, limit) {
|
|
let inThrottle;
|
|
return function() {
|
|
const args = arguments;
|
|
const context = this;
|
|
if (!inThrottle) {
|
|
func.apply(context, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updatePieChart() {
|
|
const pieChartContainer = document.getElementById('pieChartContainer');
|
|
const colorDistributionElement = document.getElementById('colorDistribution');
|
|
|
|
if (!showPieChart) {
|
|
pieChartContainer.style.display = 'none';
|
|
colorDistributionElement.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
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';
|
|
|
|
// Get current node colors
|
|
const currentColorDistribution = {};
|
|
nodes.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]);
|
|
|
|
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] / dataset.data.reduce((a, b) => a + b, 0) * 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();
|
|
}
|
|
|
|
// Update color distribution text
|
|
const totalNodes = nodes.length;
|
|
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();
|
|
if (showPieChart) {
|
|
console.log('Pie chart is now visible');
|
|
} else {
|
|
console.log('Pie chart is 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.2, // Reduced from 1 to 0.2 for a very thin border
|
|
color: {
|
|
border: '#ffffff', // Changed to white for a subtle border
|
|
background: '#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);
|
|
|
|
function updateCounter() {
|
|
const colorCounts = {};
|
|
const totalNodes = nodes.length;
|
|
|
|
nodes.forEach(node => {
|
|
const color = node.color.background;
|
|
colorCounts[color] = (colorCounts[color] || 0) + 1;
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
|
|
|
|
socket.addEventListener('message', (event) => {
|
|
const logData = JSON.parse(event.data);
|
|
const { msg_hash, receivedTime, peerId, newNode } = logData;
|
|
|
|
if (newNode) {
|
|
nodes.add({
|
|
id: peerId,
|
|
title: `Node ${peerId}`,
|
|
color: {
|
|
background: DEFAULT_COLOR,
|
|
highlight: {
|
|
background: 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)) {
|
|
edges.add({
|
|
id: edgeId,
|
|
from: msg_hash,
|
|
to: peerId,
|
|
arrows: 'to',
|
|
color: { color: '#ffffff', opacity: 0.5 }
|
|
});
|
|
}
|
|
});
|
|
|
|
setInterval(() => {
|
|
if (nodesToUpdate.length > 0) {
|
|
nodes.update(nodesToUpdate);
|
|
nodesToUpdate = [];
|
|
updatePieChartThrottled();
|
|
updateCounter();
|
|
}
|
|
}, 50);
|
|
|
|
// 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 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)
|
|
);
|
|
}
|