diff --git a/public/analytics.js b/public/analytics.js
index a02ec40b..7ada19f8 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -69,7 +69,8 @@
-
+
+
@@ -112,6 +113,7 @@
case 'topology': renderTopology(el, d.topoData); break;
case 'channels': renderChannels(el, d.chanData); break;
case 'hashsizes': renderHashSizes(el, d.hashData); break;
+ case 'collisions': renderCollisionTab(el, d.hashData); break;
case 'subpaths': renderSubpaths(el); break;
}
// Auto-apply column resizing to all analytics tables
@@ -705,10 +707,14 @@
+ `;
+ }
+ function renderCollisionTab(el, data) {
+ el.innerHTML = `
1-Byte Hash Usage Matrix
-
Each cell shows how many packets used that 1-byte hop prefix (row=high nibble, col=low nibble). Darker = more traffic.
+
Click a cell to see which nodes share that prefix. Darker = more traffic.
@@ -1027,6 +1033,29 @@
${data.totalMatches.toLocaleString()} occurrences
+ ${nodesWithLoc.length >= 2 ? `
+
📏 Hop Distances
+ ${(() => {
+ const dists = [];
+ let total = 0;
+ for (let i = 0; i < data.nodes.length - 1; i++) {
+ const a = data.nodes[i], b = data.nodes[i+1];
+ if (a.lat && a.lon && b.lat && b.lon && !(a.lat===0&&a.lon===0) && !(b.lat===0&&b.lon===0)) {
+ const dLat = (a.lat - b.lat) * 111;
+ const dLon = (a.lon - b.lon) * 85;
+ const km = Math.sqrt(dLat*dLat + dLon*dLon);
+ total += km;
+ const cls = km > 200 ? 'color:#ef4444;font-weight:bold' : km > 50 ? 'color:#f59e0b' : 'color:#22c55e';
+ dists.push(`
${km < 1 ? (km*1000).toFixed(0)+'m' : km.toFixed(1)+'km'} ${esc(a.name)} → ${esc(b.name)}
`);
+ } else {
+ dists.push(`
? ${esc(a.name)} → ${esc(b.name)} (no coords)
`);
+ }
+ }
+ if (dists.length > 1) dists.push(`
Total: ${total < 1 ? (total*1000).toFixed(0)+'m' : total.toFixed(1)+'km'}
`);
+ return dists.join('');
+ })()}
+
` : ''}
+
${hasMap ? '' : ''}