100 lines
3.2 KiB
HTML
100 lines
3.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Ruby Brain Map</title>
|
|
<script src="https://unpkg.com/vis-network@9.1.2/dist/vis-network.min.js"></script>
|
|
<style>
|
|
html, body {
|
|
margin: 0; padding: 0; height: 100%;
|
|
background: #1e1e1e; color: #ddd;
|
|
font-family: sans-serif;
|
|
}
|
|
#controls {
|
|
position: absolute; top: 10px; right: 10px; z-index: 2;
|
|
display: flex; gap: 8px; align-items: center;
|
|
}
|
|
#network { width: 100%; height: 100%; }
|
|
button, input {
|
|
background: #333; border: 1px solid #555;
|
|
color: #ddd; padding: 4px 8px; border-radius: 4px;
|
|
font-size: 14px; cursor: pointer;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="controls">
|
|
<label>Min degree:
|
|
<input id="min-degree" type="range" min="1" max="10" value="1">
|
|
</label>
|
|
<label>Max nodes:
|
|
<input id="max-nodes" type="number" min="50" max="500" step="50" value="200">
|
|
</label>
|
|
<button id="apply-filters">Apply</button>
|
|
<button id="fit-btn">Fit Graph</button>
|
|
</div>
|
|
<div id="network"></div>
|
|
<script>
|
|
let network, nodesDS, edgesDS;
|
|
const filters = { min_degree:1, max_nodes:200 };
|
|
|
|
document.getElementById('apply-filters').onclick = () => {
|
|
filters.min_degree = +document.getElementById('min-degree').value;
|
|
filters.max_nodes = +document.getElementById('max-nodes').value;
|
|
refreshData();
|
|
};
|
|
|
|
async function initNetwork(){
|
|
nodesDS = new vis.DataSet();
|
|
edgesDS = new vis.DataSet();
|
|
const container = document.getElementById('network');
|
|
const options = {
|
|
nodes: { font:{color:'#ddd'}, shape:'dot', size:8 },
|
|
edges: {
|
|
color:'#555',
|
|
smooth:false, // no curves
|
|
selfReferenceSize:0, // disable self-loops
|
|
arrows:{ to:false, from:false }
|
|
},
|
|
physics:{
|
|
enabled:true,
|
|
stabilization:{iterations:300, fit:true},
|
|
barnesHut:{gravitationalConstant:-500,springLength:150,centralGravity:0.2}
|
|
},
|
|
interaction:{
|
|
hover:true, tooltipDelay:200,
|
|
zoomView:true, dragNodes:true,
|
|
navigationButtons:true
|
|
},
|
|
minZoom:0.05, maxZoom:3
|
|
};
|
|
network = new vis.Network(container,
|
|
{ nodes: nodesDS, edges: edgesDS }, options);
|
|
network.once('stabilizationIterationsDone', ()=>network.setOptions({physics:false}));
|
|
document.getElementById('fit-btn').onclick = ()=>network.fit();
|
|
}
|
|
|
|
async function refreshData(){
|
|
const qs = new URLSearchParams(filters);
|
|
const graphRaw = await fetch(`/data?${qs}&_=${Date.now()}`,{cache:'no-store'}).then(r=>r.json());
|
|
nodesDS.update(graphRaw.nodes);
|
|
edgesDS.update(graphRaw.edges);
|
|
|
|
// prune removed
|
|
const validNodes = new Set(graphRaw.nodes.map(n=>n.id));
|
|
nodesDS.getIds().forEach(id=>validNodes.has(id)||nodesDS.remove(id));
|
|
const validEdges = new Set(graphRaw.edges.map(e=>`${e.from}-${e.to}`));
|
|
edgesDS.get().forEach(e=>{
|
|
if(!validEdges.has(`${e.from}-${e.to}`)) edgesDS.remove(e.id);
|
|
});
|
|
}
|
|
|
|
// start up
|
|
initNetwork().then(()=>{
|
|
refreshData();
|
|
setInterval(refreshData, 5000);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|