feat: Add colors and weighting to graph, condense labels

The colors indicate the role, and the edges are weighted by how many traceroutes contributed to that edge. This helps to communicate the quality of the connection. The colors and node size based on role make it easier to see the composition of the graph. This also moves the look-and-feel pieces into the template only, and attempts to centralize the color definitions.
This commit is contained in:
Alec Perkins
2025-09-10 17:49:28 -04:00
parent 1e7fccae82
commit a93f8d7212
2 changed files with 96 additions and 28 deletions
+85 -13
View File
@@ -62,6 +62,14 @@
border: 1px solid #ccc;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
font-size: 14px;
display: flex;
color: #333;
}
.legend-category {
margin-right: 10px;
code {
color: inherit;
}
}
.legend-box {
display: inline-block;
@@ -69,6 +77,9 @@
height: 12px;
margin-right: 5px;
border-radius: 3px;
&.circle {
border-radius: 6px;
}
}
{% endblock %}
@@ -90,29 +101,83 @@
</div>
<div id="legend">
<div><span class="legend-box" style="background-color:#ff5733;"></span> Traceroute</div>
<div><span class="legend-box" style="background-color:#3388ff;"></span> Neighbor</div>
<div class="legend-category">
<div><span class="legend-box" style="background-color: #ff5733"></span> Traceroute</div>
<div><span class="legend-box" style="background-color: #049acd"></span> Neighbor</div>
</div>
<div class="legend-category">
<div><span class="legend-box circle" style="background-color: #ff5733"></span> <code>ROUTER</code></div>
<div><span class="legend-box circle" style="background-color: #b65224"></span> <code>ROUTER_LATE</code></div>
</div>
<div class="legend-category">
<div><span class="legend-box circle" style="background-color: #007bff"></span> <code>CLIENT</code></div>
<div><span class="legend-box circle" style="background-color: #00c3ff"></span> <code>CLIENT_MUTE</code></div>
</div>
<div class="legend-category">
<div><span class="legend-box circle" style="background-color: #ffbf00"></span> Other</div>
<div><span class="legend-box circle" style="background-color: #6c757d"></span> Unknown</div>
</div>
</div>
<script>
const chart = echarts.init(document.getElementById('mynetwork'));
const colors = {
edge: {
traceroute: '#ff5733',
neighbor: '#049acd',
},
role: {
ROUTER: '#ff5733',
ROUTER_LATE: '#b65224',
CLIENT: '#007bff',
CLIENT_MUTE: '#00c3ff',
other: '#ffbf00',
unknown: '#6c757d',
},
selection: '#ff8c00',
};
function getRoleColor(role) {
if (!role) return colors.role.unknown;
return colors.role[role] || colors.role.other;
}
function getSymbolSize (role) {
switch (role) {
case 'ROUTER': return 30;
case 'ROUTER_LATE': return 30;
case 'CLIENT': return 15;
case 'CLIENT_MUTE': return 7;
default: return 15; // Unknown or other roles
}
}
function getLabel (role, short_name, long_name) {
if (role === 'ROUTER') return long_name;
if (role === 'ROUTER_LATE') return long_name;
if (role === 'CLIENT') return short_name;
if (role === 'CLIENT_MUTE') return short_name;
return short_name || '';
}
// --- Nodes ---
const nodes = [
{% for node in nodes %}
{
name: "{{ node.node_id }}", // node_id as string
value: {{ node.long_name | tojson }}, // display label
value: getLabel({{node.role | tojson}}, {{node.short_name | tojson }}, {{node.long_name | tojson}}), // display label
symbol: 'circle',
symbolSize: 15,
itemStyle: { color:'#007bff', opacity:1 },
symbolSize: getSymbolSize({{node.role | tojson}}),
itemStyle: { color: getRoleColor({{node.role | tojson}}), opacity:1 },
label: { show:true, position:'right', color:'#333', fontSize:12, formatter: (p)=>p.data.value },
long_name: {{ node.long_name | tojson }},
short_name: {{ node.short_name | tojson }},
role: {{ node.role | tojson }},
hw_model: {{ node.hw_model | tojson }},
channel: {{ node.channel | tojson }}
}{% if not loop.last %},{% endif %}
},
{% endfor %}
];
@@ -122,8 +187,12 @@ const edges = [
{
source: "{{ edge.from }}", // edge source as string
target: "{{ edge.to }}", // edge target as string
originalColor: "{{ edge.originalColor }}"
}{% if not loop.last %},{% endif %}
originalColor: colors.edge[{{edge.type | tojson}}],
lineStyle: {
color: colors.edge[{{edge.type | tojson}}],
width: {{edge.weight | tojson}},
},
},
{% endfor %}
];
@@ -156,24 +225,27 @@ function filterByChannel() {
function updateChart() {
const updatedNodes = filteredNodes.map(node=>{
let opacity=1, color='#007bff';
let opacity=1, color=getRoleColor(node.role), borderColor='transparent', borderWidth=6;
if(lastSelectedNode){
const connected = filteredEdges.some(e=>
(e.source===node.name && e.target===lastSelectedNode) ||
(e.target===node.name && e.source===lastSelectedNode)
);
if(node.name===lastSelectedNode){ color='#ff8c00'; opacity=1; }
if(node.name === lastSelectedNode) {
opacity=1;
borderColor=colors.selection;
}
else if(connected) opacity=1;
else opacity=0.4;
}
return {...node, itemStyle:{color,opacity}};
return {...node, itemStyle:{...node.itemStyle, color,opacity, borderColor, borderWidth}};
});
const updatedEdges = filteredEdges.map(edge=>{
let opacity=0.1, width=2;
let opacity=0.1, width=edge.lineStyle.width;
if(lastSelectedNode){
const connected = edge.source===lastSelectedNode || edge.target===lastSelectedNode;
opacity=connected?1:0.05; width=connected?2:1;
opacity=connected?1:0.05; width=edge.lineStyle.width;
}
return {...edge, lineStyle:{color:edge.originalColor||'#d3d3d3', width, opacity}};
});
+11 -15
View File
@@ -4,7 +4,7 @@ from datetime import timedelta
import json
import os
import ssl
from collections import Counter
from collections import Counter, defaultdict
from dataclasses import dataclass
import pydot
from google.protobuf import text_format
@@ -1370,8 +1370,7 @@ async def chat_updates(request):
async def nodegraph(request):
nodes = await store.get_nodes(days_active=3) # Fetch nodes for the given channel
node_ids = set()
edges_set = set() # Track unique edges
edge_type = {} # Store type of each edge
edges_map = defaultdict(lambda: { "weight": 0, "type": None }) # weight is based on the number of traceroutes and neighbor info packets
used_nodes = set() # This will track nodes involved in edges (including traceroutes)
since = datetime.timedelta(hours=48)
traceroutes = []
@@ -1396,8 +1395,8 @@ async def nodegraph(request):
# Add traceroute edges with their type and update used_nodes
for i in range(len(path) - 1):
edge_pair = (path[i], path[i + 1])
edges_set.add(edge_pair)
edge_type[edge_pair] = "traceroute"
edges_map[edge_pair]["weight"] += 1
edges_map[edge_pair]["type"] = "traceroute"
used_nodes.add(path[i]) # Add all nodes in the traceroute path
used_nodes.add(path[i + 1]) # Add all nodes in the traceroute path
@@ -1412,24 +1411,21 @@ async def nodegraph(request):
used_nodes.add(node.node_id)
edge_pair = (node.node_id, packet.from_node_id)
if edge_pair not in edges_set:
edges_set.add(edge_pair)
edge_type[edge_pair] = "neighbor"
edges_map[edge_pair]["weight"] += 1
edges_map[edge_pair]["type"] = "neighbor" # Overrides an existing traceroute pairing with neighbor
except Exception as e:
print(f"Error decoding NeighborInfo packet: {e}")
# Convert edges_set to a list of dicts with colors
# Convert edges_map to a list of dicts with colors
max_weight = max(i['weight'] for i in edges_map.values()) if edges_map else 1
edges = [
{
"from": frm,
"to": to,
"originalColor": "#ff5733" if edge_type[(frm, to)] == "traceroute" else "#3388ff", # Red for traceroute, Blue for neighbor
"lineStyle": {
"color": "#ff5733" if edge_type[(frm, to)] == "traceroute" else "#3388ff",
"width": 2
}
"type": info["type"],
"weight": max([info['weight'] / float(max_weight) * 10, 1]),
}
for frm, to in edges_set
for (frm, to), info in edges_map.items()
]
# Filter nodes to only include those involved in edges (including traceroutes)