diff --git a/admin.html b/admin.html index d1c3a35..7812d5e 100644 --- a/admin.html +++ b/admin.html @@ -1347,12 +1347,13 @@ function renderIntelGraph() { // deep-copy so d3 doesnt mutate our stored arrays on re-render const nodesCopy = nodes.map(n => ({ ...n })); - // merge parallel edges (same src+tgt pair) into one — concat labels, take max count + // merge parallel + reciprocal edges into one — sort the pair so A→B and B→A share a key const edgeMap = new Map(); for (const l of links) { const src = typeof l.source === 'object' ? l.source.key : l.source; const tgt = typeof l.target === 'object' ? l.target.key : l.target; - const key = `${src}||${tgt}`; + const [a, b] = src < tgt ? [src, tgt] : [tgt, src]; + const key = `${a}||${b}`; if (edgeMap.has(key)) { const existing = edgeMap.get(key); if (!existing.types.includes(l.type)) existing.types.push(l.type); @@ -1402,18 +1403,10 @@ function renderIntelGraph() { const maxDegree = Math.max(...Object.values(degree), 1); const simulation = d3.forceSimulation(nodesCopy) - .force('link', d3.forceLink(linksCopy).id(d => d.key).distance(d => { - const src = typeof d.source === 'object' ? d.source.key : d.source; - const tgt = typeof d.target === 'object' ? d.target.key : d.target; - const deg = Math.max(degree[src] || 1, degree[tgt] || 1); - return 80 + deg * 8; - })) - .force('charge', d3.forceManyBody().strength(d => { - const deg = degree[d.key] || 1; - return -300 - deg * 40; - })) + .force('link', d3.forceLink(linksCopy).id(d => d.key).distance(60)) + .force('charge', d3.forceManyBody().strength(-120)) .force('center', d3.forceCenter(width / 2, height / 2)) - .force('collide', d3.forceCollide(d => d.tracked ? 50 : 36)) + .force('collide', d3.forceCollide(d => d.tracked ? 48 : 34).strength(1)) .alphaDecay(0.015);