merge parallel edges in graph visualization; update link representation to include combined types and max count
This commit is contained in:
parent
3c436b0992
commit
0bc62b0a9b
1 changed files with 31 additions and 43 deletions
74
admin.html
74
admin.html
|
|
@ -1378,17 +1378,6 @@ function renderIntelGraph() {
|
|||
// svg defs — arrow marker + glow filter
|
||||
const defs = svg.append('defs');
|
||||
|
||||
defs.append('marker')
|
||||
.attr('id', 'ig-arrow')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 28)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 5)
|
||||
.attr('markerHeight', 5)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', '#475569');
|
||||
|
||||
const glow = defs.append('filter')
|
||||
.attr('id', 'ig-glow')
|
||||
|
|
@ -1417,54 +1406,51 @@ function renderIntelGraph() {
|
|||
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);
|
||||
// busier nodes push their neighbors further away
|
||||
return 180 + deg * 18;
|
||||
return 80 + deg * 8;
|
||||
}))
|
||||
.force('charge', d3.forceManyBody().strength(d => {
|
||||
const deg = degree[d.key] || 1;
|
||||
return -1400 - deg * 200;
|
||||
return -300 - deg * 40;
|
||||
}))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
// collision radius covers circle + name text below it
|
||||
.force('collide', d3.forceCollide(d => d.tracked ? 72 : 52))
|
||||
.force('collide', d3.forceCollide(d => d.tracked ? 50 : 36))
|
||||
.alphaDecay(0.015);
|
||||
|
||||
|
||||
// links
|
||||
// links — use <path> so textPath can align labels to the line
|
||||
const linkG = g.append('g');
|
||||
|
||||
const linkLines = linkG.selectAll('line')
|
||||
const linkPaths = linkG.selectAll('path.ig-edge')
|
||||
.data(linksCopy)
|
||||
.join('line')
|
||||
.join('path')
|
||||
.attr('class', 'ig-edge')
|
||||
.attr('id', (d, i) => `ig-ep-${i}`)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#334155')
|
||||
.attr('stroke-width', d => 1 + Math.pow(d.count / maxCount, 0.55) * 6)
|
||||
.attr('stroke-opacity', d => 0.2 + (d.count / maxCount) * 0.7)
|
||||
.attr('marker-end', 'url(#ig-arrow)');
|
||||
|
||||
// hover label — one floating text element that follows mouse
|
||||
const hoverLabel = svg.append('text')
|
||||
.attr('font-size', 11)
|
||||
.attr('fill', '#94a3b8')
|
||||
.attr('pointer-events', 'none')
|
||||
.attr('opacity', 0);
|
||||
|
||||
linkLines
|
||||
.on('mouseenter', function(ev, d) {
|
||||
d3.select(this).attr('stroke', '#60a5fa').attr('stroke-opacity', 1);
|
||||
hoverLabel.text(`${d.type} ×${d.count}`).attr('opacity', 1);
|
||||
})
|
||||
.on('mousemove', function(ev) {
|
||||
const [mx, my] = d3.pointer(ev, svgEl);
|
||||
hoverLabel.attr('x', mx + 10).attr('y', my - 6);
|
||||
})
|
||||
.on('mouseleave', function(ev, d) {
|
||||
d3.select(this)
|
||||
.attr('stroke', '#334155')
|
||||
d3.select(this).attr('stroke', '#334155')
|
||||
.attr('stroke-opacity', 0.2 + (d.count / maxCount) * 0.7);
|
||||
hoverLabel.attr('opacity', 0);
|
||||
});
|
||||
|
||||
const linkLabels = { attr: () => {} }; // no-op so tick handler doesnt break
|
||||
// labels along each edge via textPath
|
||||
const linkLabels = linkG.selectAll('text.ig-elabel')
|
||||
.data(linksCopy)
|
||||
.join('text')
|
||||
.attr('class', 'ig-elabel')
|
||||
.attr('font-size', 9)
|
||||
.attr('fill', '#475569')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('pointer-events', 'none')
|
||||
.append('textPath')
|
||||
.attr('href', (d, i) => `#ig-ep-${i}`)
|
||||
.attr('startOffset', '50%')
|
||||
.text(d => d.type);
|
||||
|
||||
|
||||
// nodes
|
||||
|
|
@ -1554,11 +1540,13 @@ function renderIntelGraph() {
|
|||
|
||||
|
||||
simulation.on('tick', () => {
|
||||
linkLines
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
linkPaths.attr('d', d => {
|
||||
// always draw left-to-right so textPath reads correctly
|
||||
const [sx, sy, tx, ty] = d.source.x < d.target.x
|
||||
? [d.source.x, d.source.y, d.target.x, d.target.y]
|
||||
: [d.target.x, d.target.y, d.source.x, d.source.y];
|
||||
return `M${sx},${sy} L${tx},${ty}`;
|
||||
});
|
||||
|
||||
nodeGroups.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue