<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Directed Graph Visualization</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900">
<svg></svg>
<div
class="absolute top-4 right-4 w-80 p-4 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
<h2 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Node Details</h2>
<div id="node-details" class="text-gray-800 dark:text-gray-300">
<p>Click on a node to see more details.</p>
</div>
</div>
<div
class="absolute top-4 left-4 w-80 p-4 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
<h2 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Settings</h2>
<label class="block mb-2 text-gray-900 dark:text-gray-100">
<input type="radio" name="linkType" value="unidirectional" checked class="mr-2"> Unidirectional Links
</label>
<label class="block text-gray-900 dark:text-gray-100">
<input type="radio" name="linkType" value="bidirectional" class="mr-2"> Bidirectional Links
</label>
</div>
<div class="absolute top-4 left-96 flex flex-col gap-2">
<button
class="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-800"
id="zoom-in">Zoom In</button>
<button
class="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-800"
id="zoom-out">Zoom Out</button>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const svg = d3.select('svg');
const zoom = d3.zoom().scaleExtent([0.5, 5]).on('zoom', (event) => {
svg.select('g').attr('transform', event.transform);
});
svg.call(zoom);
let linkType = 'unidirectional';
const data = {{graph| json | safe}}
const width = window.innerWidth;
const height = window.innerHeight;
svg.append('defs').selectAll('marker')
.data(['end'])
.enter().append('marker')
.attr('id', d => `arrow-${d}`)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('class', 'link-arrow');
const g = svg.append('g');
const simulation = d3.forceSimulation(data.nodes)
.force('link', d3.forceLink(data.edges).id(d => d.id))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
let link = g.append('g').attr('class', 'links')
.selectAll('line').data(data.edges)
.enter().append('line')
.attr('class', 'link')
.attr('stroke', '#999') .attr('stroke-width', 1);
const node = g.append('g').attr('class', 'nodes')
.selectAll('g').data(data.nodes)
.enter().append('g');
node.append('circle').attr('class', 'node')
.attr('r', 5)
.attr('fill', 'grey')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('mouseover', handleMouseOver)
.on('mouseout', handleMouseOut)
.on('click', handleClick);
node.append('text').attr('x', 8).attr('y', 3)
.text(d => d.label);
simulation.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})`);
});
function updateLinks() {
link.attr('marker-end', linkType === 'unidirectional' ? 'url(#arrow-end)' : null);
}
document.querySelectorAll('input[name="linkType"]').forEach(input => {
input.addEventListener('change', () => {
linkType = document.querySelector('input[name="linkType"]:checked').value;
updateLinks();
});
});
document.getElementById('zoom-in').addEventListener('click', () => svg.transition().duration(300).call(zoom.scaleBy, 1.5));
document.getElementById('zoom-out').addEventListener('click', () => svg.transition().duration(300).call(zoom.scaleBy, 0.5));
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x; d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null; d.fy = null;
}
function handleMouseOver(event, d) {
d3.select(this).select('circle').attr('r', 10);
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr('stroke', 'red').attr('stroke-width', 2);
}
function handleMouseOut(event, d) {
d3.select(this).select('circle').attr('r', 5);
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr('stroke', '#999').attr('stroke-width', 1);
}
function handleClick(event, d) {
const details = document.getElementById('node-details');
details.innerHTML = `<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">${d.label}</h3><p class="text-gray-800 dark:text-gray-300">ID: ${d.id}</p>`;
}
updateLinks();
});
</script>
</body>
</html>