mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-28 05:14:12 +00:00
feat: Enhance graph view synchronization and node rendering in web viewer
- Added functionality to synchronize the view with the URL hash, allowing direct access to the graph tab. - Implemented event listeners for hash changes to switch between graph and map views seamlessly. - Updated the loadData function to conditionally apply filters based on rendering options, improving performance during live updates. - Enhanced node styling and interaction settings for better visual feedback and user experience in the graph view.
This commit is contained in:
@@ -755,6 +755,18 @@
|
||||
await loadStats();
|
||||
// Then load data (which will apply filters)
|
||||
await loadData();
|
||||
// Open graph tab if URL has #graph (e.g. /mesh#graph)
|
||||
if (window.location.hash === '#graph') {
|
||||
switchView('graph');
|
||||
}
|
||||
// Keep view in sync with URL hash (back/forward, direct link)
|
||||
window.addEventListener('hashchange', function() {
|
||||
if (window.location.hash === '#graph' && currentView !== 'graph') {
|
||||
switchView('graph');
|
||||
} else if (window.location.hash !== '#graph' && currentView === 'graph') {
|
||||
switchView('map');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Load statistics
|
||||
@@ -789,9 +801,10 @@
|
||||
}
|
||||
|
||||
// Load nodes and edges
|
||||
async function loadData() {
|
||||
// Preserve path highlight state before reloading
|
||||
const preservedPath = highlightedPath;
|
||||
// Options: { skipRender: true } - refresh allNodes/allEdges but do not call applyFilters().
|
||||
// Used by socket handlers so graph view is not re-rendered on live updates (avoids chaotic re-stabilization).
|
||||
async function loadData(options) {
|
||||
const skipRender = options && options.skipRender === true;
|
||||
|
||||
try {
|
||||
// Load nodes
|
||||
@@ -821,9 +834,11 @@
|
||||
edgeMap[key] = edge;
|
||||
});
|
||||
|
||||
// Apply filters (will use the min observations value set by loadStats)
|
||||
// Note: applyFilters() will preserve and re-apply path highlights via renderMap()
|
||||
applyFilters();
|
||||
if (!skipRender) {
|
||||
// Apply filters (will use the min observations value set by loadStats)
|
||||
// Note: applyFilters() will preserve and re-apply path highlights via renderMap()
|
||||
applyFilters();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
throw error; // Re-throw so callers can handle it
|
||||
@@ -936,10 +951,8 @@
|
||||
// Highlights are re-applied inside renderMap() if preservedHighlightedNode or preservedPath exists
|
||||
} else {
|
||||
renderGraph();
|
||||
// Re-apply highlights in graph view if we had a highlighted node
|
||||
if (preservedHighlightedNode && nodeMap[preservedHighlightedNode] &&
|
||||
filteredNodes.some(n => n.prefix === preservedHighlightedNode)) {
|
||||
// Use requestAnimationFrame for smoother transition
|
||||
// Re-apply highlights in graph view if we had a highlighted node (preservedHighlightedNode is the node object)
|
||||
if (preservedHighlightedNode && filteredNodes.some(n => getNodeId(n) === getNodeId(preservedHighlightedNode))) {
|
||||
requestAnimationFrame(() => {
|
||||
highlightNodeConnectionsGraph(preservedHighlightedNode);
|
||||
});
|
||||
@@ -957,6 +970,16 @@
|
||||
// Switch between views
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
// Update URL hash so /mesh#graph links open directly to graph tab
|
||||
if (view === 'graph') {
|
||||
if (window.location.hash !== '#graph') {
|
||||
history.replaceState(null, '', window.location.pathname + '#graph');
|
||||
}
|
||||
} else {
|
||||
if (window.location.hash) {
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
if (view === 'map') {
|
||||
document.getElementById('btn-view-map').classList.add('active');
|
||||
@@ -1455,8 +1478,12 @@
|
||||
background: node.role === 'roomserver' ? '#198754' : '#0d6efd',
|
||||
border: node.is_starred ? '#ffc107' : '#fff',
|
||||
highlight: {
|
||||
background: node.role === 'roomserver' ? '#20c997' : '#0dcaf0',
|
||||
border: '#ffc107'
|
||||
background: node.role === 'roomserver' ? '#10b981' : '#f59e0b',
|
||||
border: '#d97706'
|
||||
},
|
||||
hover: {
|
||||
background: node.role === 'roomserver' ? '#10b981' : '#f59e0b',
|
||||
border: '#d97706'
|
||||
}
|
||||
},
|
||||
size: getNodeSize(node),
|
||||
@@ -1526,10 +1553,21 @@
|
||||
labelHighlightBold: true,
|
||||
// Reduce label overlap by adjusting label distance
|
||||
margin: 8,
|
||||
// Global fallback so hover/highlight are never default gray (#848484)
|
||||
color: {
|
||||
highlight: { border: '#d97706', background: '#f59e0b' },
|
||||
hover: { border: '#d97706', background: '#f59e0b' }
|
||||
},
|
||||
chosen: {
|
||||
node: function(values, id, selected, hovering) {
|
||||
if (hovering || selected) {
|
||||
values.font = { size: 15, color: '#ffc107', strokeWidth: 3, strokeColor: '#000000' };
|
||||
values.color = {
|
||||
background: '#f59e0b',
|
||||
border: '#d97706',
|
||||
highlight: { background: '#f59e0b', border: '#d97706' },
|
||||
hover: { background: '#f59e0b', border: '#d97706' }
|
||||
};
|
||||
values.font = { size: 15, color: '#fef3c7', strokeWidth: 3, strokeColor: '#92400e' };
|
||||
values.size = values.size * 1.2; // Slightly enlarge on hover/select
|
||||
}
|
||||
}
|
||||
@@ -1540,20 +1578,22 @@
|
||||
type: 'continuous',
|
||||
roundness: 0.5
|
||||
}
|
||||
// No global highlight/hover so our direction colors (blue/green/purple) show when a node is selected
|
||||
},
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'forceAtlas2Based',
|
||||
stabilization: {
|
||||
iterations: 200,
|
||||
iterations: 600,
|
||||
fit: true
|
||||
},
|
||||
barnesHut: {
|
||||
gravitationalConstant: -4000,
|
||||
centralGravity: 0.15,
|
||||
springLength: 150,
|
||||
springConstant: 0.025,
|
||||
damping: 0.12,
|
||||
avoidOverlap: 1.2 // Add more spacing to avoid node and label overlap
|
||||
forceAtlas2Based: {
|
||||
gravitationalConstant: -1200,
|
||||
centralGravity: 0.005,
|
||||
springLength: 220,
|
||||
springConstant: 0.08,
|
||||
damping: 0.4,
|
||||
avoidOverlap: 1.0
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
@@ -1561,6 +1601,7 @@
|
||||
tooltipDelay: 200,
|
||||
zoomView: true,
|
||||
dragView: true,
|
||||
dragNodes: false,
|
||||
selectConnectedEdges: false
|
||||
},
|
||||
layout: {
|
||||
@@ -1576,13 +1617,12 @@
|
||||
|
||||
// Instead of destroying and recreating, update the existing network to prevent flashing
|
||||
if (graphNetwork) {
|
||||
// Update data without recreating the network
|
||||
// Update data and re-enable physics so the new graph runs stabilization again
|
||||
graphNetwork.setData(data);
|
||||
// Only update non-physics options to avoid disrupting layout
|
||||
// Skip physics options to prevent layout reset and flashing
|
||||
const updateOptions = {
|
||||
nodes: options.nodes,
|
||||
edges: options.edges,
|
||||
physics: options.physics,
|
||||
interaction: options.interaction,
|
||||
layout: options.layout
|
||||
};
|
||||
@@ -1591,15 +1631,127 @@
|
||||
// Only create new network if it doesn't exist
|
||||
graphNetwork = new vis.Network(container, data, options);
|
||||
|
||||
// Freeze layout after stabilization so the graph settles instead of drifting
|
||||
graphNetwork.on('stabilizationIterationsDone', () => {
|
||||
graphNetwork.setOptions({ physics: false });
|
||||
});
|
||||
|
||||
// Apply direction colors to edges connected to a node (for hover and click)
|
||||
function applyEdgeHighlightForNodeId(focusNodeId) {
|
||||
if (!graphNetwork || !focusNodeId) return;
|
||||
const edgeIdsToHighlight = [];
|
||||
const edgeStyles = {};
|
||||
const bidirectionalEdgeIds = new Set();
|
||||
filteredEdges.forEach(edge => {
|
||||
let fromNode = null;
|
||||
if (edge.from_public_key && nodeMapByKey[edge.from_public_key]) {
|
||||
fromNode = nodeMapByKey[edge.from_public_key];
|
||||
} else {
|
||||
fromNode = nodeMap[edge.from_prefix];
|
||||
}
|
||||
let toNode = null;
|
||||
if (edge.to_public_key && nodeMapByKey[edge.to_public_key]) {
|
||||
toNode = nodeMapByKey[edge.to_public_key];
|
||||
} else {
|
||||
toNode = nodeMap[edge.to_prefix];
|
||||
}
|
||||
if (!fromNode || !toNode) return;
|
||||
const fromNodeId = getNodeId(fromNode);
|
||||
const toNodeId = getNodeId(toNode);
|
||||
const edgeId = `${fromNodeId}-${toNodeId}`;
|
||||
if (fromNodeId === focusNodeId || toNodeId === focusNodeId) {
|
||||
edgeIdsToHighlight.push(edgeId);
|
||||
const reverseEdgeId = `${toNodeId}-${fromNodeId}`;
|
||||
if (filteredEdges.some(e => {
|
||||
let revFromNode = (e.from_public_key && nodeMapByKey[e.from_public_key]) ? nodeMapByKey[e.from_public_key] : nodeMap[e.from_prefix];
|
||||
let revToNode = (e.to_public_key && nodeMapByKey[e.to_public_key]) ? nodeMapByKey[e.to_public_key] : nodeMap[e.to_prefix];
|
||||
if (!revFromNode || !revToNode) return false;
|
||||
return getNodeId(revFromNode) === toNodeId && getNodeId(revToNode) === fromNodeId;
|
||||
})) {
|
||||
bidirectionalEdgeIds.add(edgeId);
|
||||
bidirectionalEdgeIds.add(reverseEdgeId);
|
||||
}
|
||||
let highlightColor;
|
||||
if (bidirectionalEdgeIds.has(edgeId)) {
|
||||
highlightColor = '#9333ea';
|
||||
} else if (fromNodeId === focusNodeId) {
|
||||
highlightColor = '#3b82f6';
|
||||
} else {
|
||||
highlightColor = '#10b981';
|
||||
}
|
||||
const baseWidth = Math.max(1, Math.log10(edge.observation_count) * 2);
|
||||
edgeStyles[edgeId] = {
|
||||
color: {
|
||||
color: highlightColor,
|
||||
opacity: 1.0,
|
||||
highlight: highlightColor,
|
||||
hover: highlightColor
|
||||
},
|
||||
width: Math.max(3, baseWidth + 2)
|
||||
};
|
||||
}
|
||||
});
|
||||
const currentEdges = graphNetwork.body.data.edges.get();
|
||||
const edgeUpdates = currentEdges.map(edge => {
|
||||
const edgeId = edge.id;
|
||||
if (edgeIdsToHighlight.includes(edgeId)) {
|
||||
return { id: edgeId, ...edgeStyles[edgeId] };
|
||||
}
|
||||
return {
|
||||
id: edgeId,
|
||||
color: { color: '#848484', opacity: 0.2 },
|
||||
width: 1
|
||||
};
|
||||
});
|
||||
graphNetwork.body.data.edges.update(edgeUpdates);
|
||||
}
|
||||
|
||||
// Restore edges after hover ends: re-apply selected node state or default edge colors
|
||||
function restoreGraphEdgesAfterHover() {
|
||||
if (!graphNetwork || currentView !== 'graph') return;
|
||||
if (highlightedNodeObject) {
|
||||
highlightNodeConnectionsGraph(highlightedNodeObject);
|
||||
} else {
|
||||
const currentEdges = graphNetwork.body.data.edges.get();
|
||||
const edgeUpdates = [];
|
||||
for (const edge of currentEdges) {
|
||||
const edgeId = edge.id;
|
||||
const originalEdge = filteredEdges.find(e => {
|
||||
let fromNode = (e.from_public_key && nodeMapByKey[e.from_public_key]) ? nodeMapByKey[e.from_public_key] : nodeMap[e.from_prefix];
|
||||
let toNode = (e.to_public_key && nodeMapByKey[e.to_public_key]) ? nodeMapByKey[e.to_public_key] : nodeMap[e.to_prefix];
|
||||
if (!fromNode || !toNode) return false;
|
||||
return `${getNodeId(fromNode)}-${getNodeId(toNode)}` === edgeId;
|
||||
});
|
||||
if (originalEdge) {
|
||||
edgeUpdates.push({
|
||||
id: edgeId,
|
||||
color: { color: getEdgeColor(originalEdge), opacity: getEdgeOpacity(originalEdge) },
|
||||
width: Math.max(1, Math.log10(originalEdge.observation_count) * 2)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (edgeUpdates.length) graphNetwork.body.data.edges.update(edgeUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers - only set up once when network is first created
|
||||
graphNetwork.on('hoverNode', (params) => {
|
||||
if (params.node) applyEdgeHighlightForNodeId(params.node);
|
||||
});
|
||||
graphNetwork.on('blurNode', () => {
|
||||
restoreGraphEdgesAfterHover();
|
||||
});
|
||||
graphNetwork.on('click', (params) => {
|
||||
if (params.nodes.length > 0) {
|
||||
const nodeId = params.nodes[0]; // This is now the unique ID (prefix-lat-lon)
|
||||
// Find node by matching the unique ID
|
||||
const nodeId = params.nodes[0];
|
||||
const node = filteredNodes.find(n => getNodeId(n) === nodeId);
|
||||
if (node) {
|
||||
highlightNodeConnectionsGraph(node.prefix);
|
||||
// For graph view, still show modal since there's no popup equivalent
|
||||
// Click on the currently highlighted node = clear (fixes two-click-to-clear)
|
||||
if (highlightedNodeObject && getNodeId(highlightedNodeObject) === nodeId) {
|
||||
clearHighlights();
|
||||
return;
|
||||
}
|
||||
highlightNodeConnectionsGraph(node);
|
||||
showNodeDetails(node);
|
||||
}
|
||||
} else if (params.edges.length > 0) {
|
||||
@@ -1841,21 +1993,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight node connections in graph view
|
||||
function highlightNodeConnectionsGraph(nodePrefix) {
|
||||
if (!graphNetwork || !nodePrefix) return;
|
||||
// Highlight node connections in graph view.
|
||||
// nodeOrIdOrPrefix: node object (preferred), unique node id string (getNodeId), or prefix string (fallback).
|
||||
function highlightNodeConnectionsGraph(nodeOrIdOrPrefix) {
|
||||
if (!graphNetwork || nodeOrIdOrPrefix == null) return;
|
||||
|
||||
// Get the actual node object(s) with this prefix
|
||||
const nodesWithPrefix = filteredNodes.filter(n => n.prefix === nodePrefix);
|
||||
if (nodesWithPrefix.length === 0) return;
|
||||
let clickedNode = null;
|
||||
if (typeof nodeOrIdOrPrefix === 'object' && nodeOrIdOrPrefix !== null && 'prefix' in nodeOrIdOrPrefix && 'latitude' in nodeOrIdOrPrefix) {
|
||||
clickedNode = nodeOrIdOrPrefix;
|
||||
} else if (typeof nodeOrIdOrPrefix === 'string') {
|
||||
const byId = filteredNodes.find(n => getNodeId(n) === nodeOrIdOrPrefix);
|
||||
if (byId) {
|
||||
clickedNode = byId;
|
||||
} else {
|
||||
const nodesWithPrefix = filteredNodes.filter(n => n.prefix === nodeOrIdOrPrefix);
|
||||
if (nodesWithPrefix.length > 0) clickedNode = nodesWithPrefix[0];
|
||||
}
|
||||
}
|
||||
if (!clickedNode) return;
|
||||
|
||||
// For now, highlight the first node with this prefix (could be enhanced to highlight all)
|
||||
const clickedNode = nodesWithPrefix[0];
|
||||
const clickedNodeId = getNodeId(clickedNode);
|
||||
|
||||
// Clear previous highlights
|
||||
clearHighlights();
|
||||
highlightedNode = nodePrefix;
|
||||
// Don't call clearHighlights() so switching to another node works (direct state transition)
|
||||
highlightedNode = clickedNode.prefix;
|
||||
highlightedNodeObject = clickedNode;
|
||||
|
||||
// Show legend
|
||||
@@ -1868,6 +2028,7 @@
|
||||
const edgeIdsToHighlight = [];
|
||||
const edgeStyles = {};
|
||||
const bidirectionalEdgeIds = new Set();
|
||||
const connectedNodeIds = new Set([clickedNodeId]);
|
||||
|
||||
filteredEdges.forEach(edge => {
|
||||
// Resolve nodes for this edge
|
||||
@@ -1893,6 +2054,8 @@
|
||||
// Check if this edge connects to the clicked node
|
||||
if (fromNodeId === clickedNodeId || toNodeId === clickedNodeId) {
|
||||
edgeIdsToHighlight.push(edgeId);
|
||||
connectedNodeIds.add(fromNodeId);
|
||||
connectedNodeIds.add(toNodeId);
|
||||
|
||||
// Check for bidirectional
|
||||
const reverseEdgeId = `${toNodeId}-${fromNodeId}`;
|
||||
@@ -1916,7 +2079,7 @@
|
||||
bidirectionalEdgeIds.add(reverseEdgeId);
|
||||
}
|
||||
|
||||
// Determine color based on direction
|
||||
// Use connection direction colors: blue outgoing, green incoming, purple bidirectional
|
||||
let highlightColor;
|
||||
if (bidirectionalEdgeIds.has(edgeId)) {
|
||||
highlightColor = '#9333ea'; // Purple for bidirectional
|
||||
@@ -1925,40 +2088,68 @@
|
||||
} else {
|
||||
highlightColor = '#10b981'; // Green for incoming
|
||||
}
|
||||
|
||||
const baseWidth = Math.max(1, Math.log10(edge.observation_count) * 2);
|
||||
edgeStyles[edgeId] = {
|
||||
color: { color: highlightColor, opacity: 1.0 },
|
||||
width: Math.max(3, Math.log10(edge.observation_count) * 2 + 2)
|
||||
color: {
|
||||
color: highlightColor,
|
||||
opacity: 1.0,
|
||||
highlight: highlightColor,
|
||||
hover: highlightColor
|
||||
},
|
||||
width: Math.max(3, baseWidth + 2)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Update edges individually using vis-network's updateEdge method
|
||||
// Batch all edge updates in one call to avoid per-item redraws (was ~18s for large graphs)
|
||||
const currentEdges = graphNetwork.body.data.edges.get();
|
||||
|
||||
currentEdges.forEach(edge => {
|
||||
const edgeUpdates = currentEdges.map(edge => {
|
||||
const edgeId = edge.id;
|
||||
if (edgeIdsToHighlight.includes(edgeId)) {
|
||||
// Highlight this edge
|
||||
graphNetwork.updateEdge(edgeId, edgeStyles[edgeId]);
|
||||
} else {
|
||||
// Dim non-highlighted edges
|
||||
graphNetwork.updateEdge(edgeId, {
|
||||
color: { color: '#848484', opacity: 0.2 },
|
||||
width: 1
|
||||
});
|
||||
return { id: edgeId, ...edgeStyles[edgeId] };
|
||||
}
|
||||
return {
|
||||
id: edgeId,
|
||||
color: { color: '#848484', opacity: 0.2 },
|
||||
width: 1
|
||||
};
|
||||
});
|
||||
graphNetwork.body.data.edges.update(edgeUpdates);
|
||||
|
||||
// Highlight the node with a border
|
||||
graphNetwork.selectNodes([clickedNodeId]);
|
||||
|
||||
// Update node style to make it stand out
|
||||
graphNetwork.updateNode(clickedNodeId, {
|
||||
borderWidth: 4,
|
||||
borderColor: '#ffc107',
|
||||
size: getNodeSize(clickedNode) + 2
|
||||
// Update all nodes in one batch: one update per graph node so labels aren't overwritten by duplicates
|
||||
const nodeHighlightColor = clickedNode.role === 'roomserver' ? '#10b981' : '#f59e0b';
|
||||
const graphNodeIds = graphNetwork.body.data.nodes.getIds();
|
||||
const nodeUpdates = graphNodeIds.map(nid => {
|
||||
const n = filteredNodes.find(fn => getNodeId(fn) === nid);
|
||||
if (!n) return { id: nid, label: '', title: '' };
|
||||
const isClicked = nid === clickedNodeId;
|
||||
const isLinked = connectedNodeIds.has(nid);
|
||||
const bg = n.role === 'roomserver' ? '#198754' : '#0d6efd';
|
||||
const border = n.is_starred ? '#ffc107' : '#fff';
|
||||
const hlBg = n.role === 'roomserver' ? '#10b981' : '#f59e0b';
|
||||
const label = isLinked
|
||||
? (n.name.length > 22 ? n.name.substring(0, 22) + '... (' + n.prefix + ')' : n.name + ' (' + n.prefix + ')')
|
||||
: '';
|
||||
const title = n.name + ' (' + n.prefix + ')';
|
||||
const update = {
|
||||
id: nid,
|
||||
label: label,
|
||||
title: title,
|
||||
color: isClicked
|
||||
? { border: '#d97706', background: nodeHighlightColor, highlight: { border: '#d97706', background: nodeHighlightColor }, hover: { border: '#d97706', background: nodeHighlightColor } }
|
||||
: { background: bg, border: border, highlight: { background: hlBg, border: '#d97706' }, hover: { background: hlBg, border: '#d97706' } },
|
||||
borderWidth: isClicked ? 4 : getBorderWeight(getNodeSize(n), n.is_starred),
|
||||
size: isClicked ? getNodeSize(clickedNode) + 2 : getNodeSize(n)
|
||||
};
|
||||
// Force label to render for linked nodes (vis-network can skip if font not set on update)
|
||||
if (isLinked) {
|
||||
update.font = { size: 13, color: '#ffffff', strokeWidth: 2, strokeColor: '#000000' };
|
||||
}
|
||||
return update;
|
||||
});
|
||||
graphNetwork.body.data.nodes.update(nodeUpdates);
|
||||
}
|
||||
|
||||
// Clear all highlights
|
||||
@@ -2015,11 +2206,11 @@
|
||||
if (graphNetwork && currentView === 'graph') {
|
||||
graphNetwork.unselectAll();
|
||||
|
||||
// Restore all edges to original colors
|
||||
// Batch edge and node restores in single update() calls to avoid per-item redraws
|
||||
const currentEdges = graphNetwork.body.data.edges.get();
|
||||
currentEdges.forEach(edge => {
|
||||
const edgeId = edge.id; // This is now fromNodeId-toNodeId format
|
||||
// Find the edge by matching unique node IDs
|
||||
const edgeUpdates = [];
|
||||
for (const edge of currentEdges) {
|
||||
const edgeId = edge.id;
|
||||
const originalEdge = filteredEdges.find(e => {
|
||||
let fromNode = null;
|
||||
if (e.from_public_key && nodeMapByKey[e.from_public_key]) {
|
||||
@@ -2038,28 +2229,40 @@
|
||||
return expectedEdgeId === edgeId;
|
||||
});
|
||||
if (originalEdge) {
|
||||
const originalColor = getEdgeColor(originalEdge);
|
||||
const originalOpacity = getEdgeOpacity(originalEdge);
|
||||
const originalWidth = Math.max(1, Math.log10(originalEdge.observation_count) * 2);
|
||||
graphNetwork.updateEdge(edgeId, {
|
||||
color: { color: originalColor, opacity: originalOpacity },
|
||||
width: originalWidth
|
||||
edgeUpdates.push({
|
||||
id: edgeId,
|
||||
color: { color: getEdgeColor(originalEdge), opacity: getEdgeOpacity(originalEdge) },
|
||||
width: Math.max(1, Math.log10(originalEdge.observation_count) * 2)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (edgeUpdates.length) graphNetwork.body.data.edges.update(edgeUpdates);
|
||||
|
||||
// Restore node styles - use unique node IDs
|
||||
filteredNodes.forEach(node => {
|
||||
const nodeId = getNodeId(node);
|
||||
const graphNodeIds = graphNetwork.body.data.nodes.getIds();
|
||||
const nodeUpdates = graphNodeIds.map(nodeId => {
|
||||
const node = filteredNodes.find(fn => getNodeId(fn) === nodeId);
|
||||
if (!node) return { id: nodeId, label: '', font: { size: 12 } };
|
||||
const nodeSize = getNodeSize(node);
|
||||
// Scale border width proportionally with node size for graph view too
|
||||
const borderWidth = getBorderWeight(nodeSize, node.is_starred);
|
||||
graphNetwork.updateNode(nodeId, {
|
||||
const bg = node.role === 'roomserver' ? '#198754' : '#0d6efd';
|
||||
const border = node.is_starred ? '#ffc107' : '#fff';
|
||||
const hlBg = node.role === 'roomserver' ? '#10b981' : '#f59e0b';
|
||||
return {
|
||||
id: nodeId,
|
||||
label: getDefaultNodeLabel(node),
|
||||
title: node.name + ' (' + node.prefix + ')',
|
||||
font: { size: 12 },
|
||||
color: {
|
||||
background: bg,
|
||||
border: border,
|
||||
highlight: { background: hlBg, border: '#d97706' },
|
||||
hover: { background: hlBg, border: '#d97706' }
|
||||
},
|
||||
borderWidth: borderWidth,
|
||||
borderColor: node.is_starred ? '#ffc107' : '#fff',
|
||||
size: nodeSize
|
||||
});
|
||||
};
|
||||
});
|
||||
graphNetwork.body.data.nodes.update(nodeUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2070,9 +2273,8 @@
|
||||
const degree = filteredEdges.filter(e =>
|
||||
e.from_prefix === node.prefix || e.to_prefix === node.prefix
|
||||
).length;
|
||||
// Smaller base size and less aggressive scaling
|
||||
// Returns a value between 4 and 12 based on connection count
|
||||
return Math.max(4, Math.min(12, 4 + degree * 0.5));
|
||||
// Base size 6, scale with degree, cap at 16
|
||||
return Math.max(6, Math.min(16, 6 + degree * 0.5));
|
||||
}
|
||||
|
||||
// Helper function to calculate border weight based on node size
|
||||
@@ -2083,6 +2285,17 @@
|
||||
return isStarred ? baseWeight * 1.5 : baseWeight;
|
||||
}
|
||||
|
||||
// Default label for a node (matches renderGraph shortLabel logic) for restore on clear
|
||||
function getDefaultNodeLabel(node) {
|
||||
if (node.name.length > 25) {
|
||||
return node.prefix.toUpperCase() + ': ' + (node.name.length > 15 ? node.name.substring(0, 15) + '...' : node.name);
|
||||
}
|
||||
if (node.name.length > 20) {
|
||||
return node.name.substring(0, 20) + '...';
|
||||
}
|
||||
return node.name;
|
||||
}
|
||||
|
||||
function getEdgeColor(edge) {
|
||||
if (!edge.last_seen) return '#6c757d'; // Gray for unknown
|
||||
|
||||
@@ -2180,53 +2393,28 @@
|
||||
}
|
||||
|
||||
// Socket.IO setup for real-time updates
|
||||
// Live updates are applied only when the map view is active. When the graph view is active,
|
||||
// we refresh data in the background but do not re-render the graph, to avoid the chaotic
|
||||
// re-stabilization layout (nodes clumping, edges tangling) that occurs when vis-network
|
||||
// runs physics again. User can refresh or switch to map and back to see updated graph.
|
||||
function setupSocketIO() {
|
||||
const socket = io();
|
||||
|
||||
// Subscribe to mesh graph updates
|
||||
socket.emit('subscribe_mesh');
|
||||
|
||||
socket.on('mesh_edge_added', (data) => {
|
||||
console.log('New edge added:', data);
|
||||
// Preserve highlighted path and node before reloading
|
||||
const preservedPath = highlightedPath;
|
||||
const preservedNode = highlightedNodeObject;
|
||||
loadStats(); // Update stats first
|
||||
loadData().then(() => {
|
||||
// Re-apply path highlight after data loads to keep edges dimmed
|
||||
// renderMap() will handle re-applying the highlight, but we need to ensure it happens
|
||||
// The highlight will be re-applied by renderMap() via preservedPath
|
||||
// No need for additional setTimeout here - renderMap() handles it
|
||||
function onMeshUpdate(data, label) {
|
||||
console.log(label, data);
|
||||
loadStats();
|
||||
loadData({ skipRender: true }).then(() => {
|
||||
if (currentView === 'map') {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('mesh_edge_updated', (data) => {
|
||||
console.log('Edge updated:', data);
|
||||
// Preserve highlighted path and node before reloading
|
||||
const preservedPath = highlightedPath;
|
||||
const preservedNode = highlightedNodeObject;
|
||||
loadStats(); // Update stats first
|
||||
loadData().then(() => {
|
||||
// Re-apply path highlight after data loads to keep edges dimmed
|
||||
// renderMap() will handle re-applying the highlight, but we need to ensure it happens
|
||||
// The highlight will be re-applied by renderMap() via preservedPath
|
||||
// No need for additional setTimeout here - renderMap() handles it
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('mesh_node_added', (data) => {
|
||||
console.log('New node added:', data);
|
||||
// Preserve highlighted path and node before reloading
|
||||
const preservedPath = highlightedPath;
|
||||
const preservedNode = highlightedNodeObject;
|
||||
loadStats(); // Update stats first
|
||||
loadData().then(() => {
|
||||
// Re-apply path highlight after data loads to keep edges dimmed
|
||||
// renderMap() will handle re-applying the highlight, but we need to ensure it happens
|
||||
// The highlight will be re-applied by renderMap() via preservedPath
|
||||
// No need for additional setTimeout here - renderMap() handles it
|
||||
});
|
||||
});
|
||||
socket.on('mesh_edge_added', (data) => onMeshUpdate(data, 'New edge added:'));
|
||||
socket.on('mesh_edge_updated', (data) => onMeshUpdate(data, 'Edge updated:'));
|
||||
socket.on('mesh_node_added', (data) => onMeshUpdate(data, 'New node added:'));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user