feat(gui): netwerk tab netwerk layout grafisch - 02
This commit is contained in:
@@ -571,4 +571,32 @@ pre{
|
||||
}
|
||||
|
||||
.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 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user