Ruby/templates/graph.html
2025-05-04 17:32:25 -04:00

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>