feat(gui): netwerk tab netwerk layout grafisch - 02

This commit is contained in:
kodi
2026-02-22 19:59:45 +01:00
parent 18ee367e1d
commit 0337f1438f
3 changed files with 146 additions and 1 deletions
+28
View File
@@ -572,3 +572,31 @@ pre{
.mapLegend .legendSwatch.net{ background: rgba(80,160,255,0.35); }
.mapLegend .legendSwatch.ctr{ background: rgba(150,230,150,0.30); }
/* ===== Netwerken kaart (D3) ===== */
.mapHost svg { width: 100%; height: 100%; display:block; }
.graphLink{
stroke: rgba(255,255,255,0.22);
stroke-width: 1.2;
}
.graphLink.shared{
stroke-dasharray: 6 4;
stroke: rgba(255,255,255,0.28);
}
.graphNode circle{
stroke: rgba(255,255,255,0.18);
stroke-width: 1;
}
.graphNode.network circle{ fill: rgba(80,160,255,0.35); }
.graphNode.container circle{ fill: rgba(150,230,150,0.30); }
.graphLabel{
fill: rgba(255,255,255,0.82);
font-size: 12px;
pointer-events: none;
}
.graphDim{ opacity: 0.18; }
.graphActive{ opacity: 1; stroke: rgba(255,255,255,0.55); stroke-width: 2; }
+116
View File
@@ -76,6 +76,7 @@
const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
renderGraph(model);
} catch (e) {
console.error('[networks] buildGlobalGraphModel failed', e);
}
@@ -527,6 +528,120 @@
return { nodes, links, meta };
}
let graphCtx = null;
function renderGraph(model) {
const host = document.getElementById('networksMapHost');
if (!host) return;
// leeg host (placeholder weg)
host.innerHTML = '';
const w = Math.max(600, host.clientWidth || 600);
const h = Math.max(420, host.clientHeight || 420);
const svg = d3.select(host).append('svg')
.attr('viewBox', `0 0 ${w} ${h}`);
const g = svg.append('g');
// zoom/pan
svg.call(
d3.zoom()
.scaleExtent([0.2, 2.5])
.on('zoom', (ev) => g.attr('transform', ev.transform))
);
// links
const link = g.append('g')
.selectAll('line')
.data(model.links)
.enter()
.append('line')
.attr('class', d => d.type === 'shared' ? 'graphLink shared' : 'graphLink');
// nodes
const node = g.append('g')
.selectAll('g')
.data(model.nodes)
.enter()
.append('g')
.attr('class', d => `graphNode ${d.type}`);
node.append('circle')
.attr('r', d => d.type === 'network' ? 10 : 8);
node.append('text')
.attr('class', 'graphLabel')
.attr('x', 12)
.attr('y', 4)
.text(d => d.label || d.key);
// drag
const drag = d3.drag()
.on('start', (ev, d) => {
if (!ev.active) graphCtx.sim.alphaTarget(0.2).restart();
d.fx = d.x; d.fy = d.y;
})
.on('drag', (ev, d) => {
d.fx = ev.x; d.fy = ev.y;
})
.on('end', (ev, d) => {
if (!ev.active) graphCtx.sim.alphaTarget(0);
d.fx = null; d.fy = null;
});
node.call(drag);
// hover highlight (connected)
node.on('mouseenter', (ev, d) => {
node.classed('graphDim', true);
link.classed('graphDim', true);
d3.select(ev.currentTarget).classed('graphDim', false);
link.each(function(l) {
const sid = l.source?.id || l.source;
const tid = l.target?.id || l.target;
const hit = (sid === d.id || tid === d.id);
if (hit) {
d3.select(this).classed('graphDim', false).classed('graphActive', true);
} else {
d3.select(this).classed('graphActive', false);
}
});
});
node.on('mouseleave', () => {
node.classed('graphDim', false);
link.classed('graphDim', false).classed('graphActive', false);
});
node.on('click', (ev, d) => {
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Geselecteerd: ${d.type} ${d.key}`;
});
// simulation
const sim = d3.forceSimulation(model.nodes)
.force('link', d3.forceLink(model.links).id(d => d.id).distance(80))
.force('charge', d3.forceManyBody().strength(-220))
.force('center', d3.forceCenter(w / 2, h / 2))
.force('collide', d3.forceCollide().radius(d => d.type === 'network' ? 18 : 16))
.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
graphCtx = { svg, g, sim, model };
}
function renderNetworks() {
const tbody = document.getElementById('networksTbody');
const rel = document.getElementById('networksRelations');
@@ -688,6 +803,7 @@
const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
renderGraph(model);
}
}
+1
View File
@@ -1208,6 +1208,7 @@
setInterval(() => { pingApi(); }, 20000);
})();
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
<script src="assets/js/tabs/networks.js"></script>
<script src="assets/js/tabs/images.js"></script>
</body>