feat(gui): netwerk tab netwerk layout grafisch - 02
This commit is contained in:
@@ -572,3 +572,31 @@ pre{
|
|||||||
|
|
||||||
.mapLegend .legendSwatch.net{ background: rgba(80,160,255,0.35); }
|
.mapLegend .legendSwatch.net{ background: rgba(80,160,255,0.35); }
|
||||||
.mapLegend .legendSwatch.ctr{ background: rgba(150,230,150,0.30); }
|
.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; }
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
const model = buildGlobalGraphModel();
|
const model = buildGlobalGraphModel();
|
||||||
const s = document.getElementById('networksMapStatus');
|
const s = document.getElementById('networksMapStatus');
|
||||||
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
|
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
|
||||||
|
renderGraph(model);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[networks] buildGlobalGraphModel failed', e);
|
console.error('[networks] buildGlobalGraphModel failed', e);
|
||||||
}
|
}
|
||||||
@@ -527,6 +528,120 @@
|
|||||||
return { nodes, links, meta };
|
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() {
|
function renderNetworks() {
|
||||||
const tbody = document.getElementById('networksTbody');
|
const tbody = document.getElementById('networksTbody');
|
||||||
const rel = document.getElementById('networksRelations');
|
const rel = document.getElementById('networksRelations');
|
||||||
@@ -688,6 +803,7 @@
|
|||||||
const model = buildGlobalGraphModel();
|
const model = buildGlobalGraphModel();
|
||||||
const s = document.getElementById('networksMapStatus');
|
const s = document.getElementById('networksMapStatus');
|
||||||
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
|
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
|
||||||
|
renderGraph(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1208,6 +1208,7 @@
|
|||||||
setInterval(() => { pingApi(); }, 20000);
|
setInterval(() => { pingApi(); }, 20000);
|
||||||
})();
|
})();
|
||||||
</script>
|
</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/networks.js"></script>
|
||||||
<script src="assets/js/tabs/images.js"></script>
|
<script src="assets/js/tabs/images.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user