Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Enhanced Directed Graph Visualization</title> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<style> | |
body, | |
html { | |
height: 100%; | |
width: 100%; | |
margin: 0; | |
padding: 0; | |
font-family: Arial, sans-serif; | |
} | |
#mynetwork { | |
width: 100%; | |
height: 100%; | |
} | |
.node circle { | |
fill: #69b3a2; | |
stroke: #333; | |
stroke-width: 1.5px; | |
} | |
.node text { | |
font: 12px sans-serif; | |
pointer-events: none; | |
fill: #555; | |
} | |
.link { | |
fill: none; | |
stroke: #999; | |
stroke-opacity: 0.6; | |
stroke-width: 1.5px; | |
marker-end: url(#arrowhead); | |
} | |
.link.directed { | |
stroke: #ff5722; | |
} | |
.link.highlighted { | |
stroke-width: 3px; | |
stroke: #ff5722; | |
} | |
.tooltip { | |
position: absolute; | |
text-align: center; | |
width: auto; | |
height: auto; | |
padding: 5px; | |
font: 12px sans-serif; | |
background: lightsteelblue; | |
border: 0px; | |
border-radius: 8px; | |
pointer-events: none; | |
opacity: 0; | |
} | |
#controls { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
z-index: 1; | |
} | |
#legend { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
background-color: rgba(255, 255, 255, 0.7); | |
padding: 10px; | |
border-radius: 3px; | |
font-size: 12px; | |
} | |
#legend .legend-item { | |
display: flex; | |
align-items: center; | |
} | |
#legend .legend-item span { | |
margin-left: 5px; | |
} | |
#legend .legend-color { | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
} | |
#search { | |
position: absolute; | |
top: 50px; | |
left: 10px; | |
z-index: 1; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="controls"> | |
<button onclick="fitNetwork()">Fit View</button> | |
<button onclick="expandAll()">Expand All</button> | |
<button onclick="collapseAll()">Collapse All</button> | |
<input type="range" id="charge" min="-1000" max="0" value="-300" step="10"> | |
<label for="charge">Charge Strength</label> | |
</div> | |
<div id="search"> | |
<input type="text" id="searchInput" placeholder="Search nodes..."> | |
<button onclick="searchNode()">Search</button> | |
</div> | |
<div id="legend"> | |
<div class="legend-item"> | |
<div class="legend-color" style="background-color: #69b3a2;"></div> | |
<span>Node</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color" style="background-color: #ff5722;"></div> | |
<span>Directed Link</span> | |
</div> | |
</div> | |
<div id="mynetwork"></div> | |
<div class="tooltip"></div> | |
<script> | |
// Define dimensions | |
const width = window.innerWidth; | |
const height = window.innerHeight; | |
// Default graph data | |
const defaultGraphData = { | |
"nodes": [ | |
{"id": "1", "label": "Root"}, | |
{"id": "2", "label": "Child 1"}, | |
{"id": "3", "label": "Child 2"} | |
], | |
"edges": [ | |
{"from": "1", "to": "2"}, | |
{"from": "1", "to": "3"} | |
] | |
}; | |
// Function to initialize the graph | |
function initializeGraph(graphData) { | |
// Create the SVG container | |
const svg = d3.select("#mynetwork") | |
.append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.call(d3.zoom().on("zoom", function (event) { | |
svg.attr("transform", event.transform); | |
})) | |
.append("g"); | |
// Create a tooltip | |
const tooltip = d3.select("body").append("div") | |
.attr("class", "tooltip") | |
.style("opacity", 0); | |
// Define the arrowhead marker | |
svg.append("defs").append("marker") | |
.attr("id", "arrowhead") | |
.attr("viewBox", "0 -5 10 10") | |
.attr("refX", 20) | |
.attr("refY", 0) | |
.attr("markerWidth", 8) | |
.attr("markerHeight", 8) | |
.attr("orient", "auto") | |
.append("path") | |
.attr("d", "M 0,-5 L 10,0 L 0,5") | |
.attr("fill", "#999"); | |
// Rename edges from "from" and "to" to "source" and "target" | |
graphData.edges.forEach(edge => { | |
edge.source = edge.from; | |
edge.target = edge.to; | |
delete edge.from; | |
delete edge.to; | |
}); | |
// Collect all node IDs | |
const nodeIds = new Set(graphData.nodes.map(node => node.id)); | |
// Create a mapping for default nodes | |
const defaultNode = { | |
id: "default", | |
label: "Unknown Node", | |
x: width / 2, | |
y: height / 2 | |
}; | |
// Ensure all edges have valid nodes | |
graphData.edges.forEach(edge => { | |
if (!nodeIds.has(edge.source)) { | |
graphData.nodes.push({ ...defaultNode, id: edge.source }); | |
nodeIds.add(edge.source); | |
} | |
if (!nodeIds.has(edge.target)) { | |
graphData.nodes.push({ ...defaultNode, id: edge.target }); | |
nodeIds.add(edge.target); | |
} | |
}); | |
// Create the simulation | |
const simulation = d3.forceSimulation(graphData.nodes) | |
.force("link", d3.forceLink(graphData.edges).id(d => d.id).distance(100)) | |
.force("charge", d3.forceManyBody().strength(-300)) | |
.force("center", d3.forceCenter(width / 2, height / 2)) | |
.force("collision", d3.forceCollide().radius(50)); | |
// Create links | |
const link = svg.append("g") | |
.attr("class", "links") | |
.selectAll("line") | |
.data(graphData.edges) | |
.enter().append("line") | |
.attr("class", "link directed"); | |
// Create nodes | |
const node = svg.append("g") | |
.attr("class", "nodes") | |
.selectAll("g") | |
.data(graphData.nodes) | |
.enter().append("g") | |
.attr("class", "node") | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
node.append("circle") | |
.attr("r", 15) | |
.attr("fill", "#69b3a2"); | |
node.append("text") | |
.attr("x", 18) | |
.attr("y", 5) | |
.text(d => d.label) | |
.attr("font-size", "12px") | |
.attr("fill", "#555"); | |
// Add tooltips and highlighting | |
node.on("mouseover", function (event, d) { | |
highlightConnections(d); | |
showTooltip(event, d); | |
}).on("mouseout", function () { | |
unhighlightConnections(); | |
hideTooltip(); | |
}); | |
// Update positions on tick | |
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 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 highlightConnections(d) { | |
node.style("opacity", n => n === d || graphData.edges.some(l => (l.source === d && l.target === n) || (l.target === d && l.source === n)) ? 1 : 0.1); | |
link.style("opacity", l => l.source === d || l.target === d ? 1 : 0.1) | |
.classed("highlighted", l => l.source === d || l.target === d); | |
} | |
function unhighlightConnections() { | |
node.style("opacity", 1); | |
link.style("opacity", 1).classed("highlighted", false); | |
} | |
function showTooltip(event, d) { | |
tooltip.transition().duration(200).style("opacity", .9); | |
tooltip.html(` | |
<strong>${d.label}</strong><br/> | |
ID: ${d.id}<br/> | |
Connections: ${graphData.edges.filter(l => l.source === d || l.target === d).length} | |
`) | |
.style("left", (event.pageX + 10) + "px") | |
.style("top", (event.pageY - 28) + "px"); | |
} | |
function hideTooltip() { | |
tooltip.transition().duration(500).style("opacity", 0); | |
} | |
window.fitNetwork = () => { | |
const bounds = svg.node().getBBox(); | |
const parent = svg.node().parentElement; | |
const fullWidth = parent.clientWidth; | |
const fullHeight = parent.clientHeight; | |
const width = bounds.width; | |
const height = bounds.height; | |
const midX = bounds.x + width / 2; | |
const midY = bounds.y + height / 2; | |
if (width === 0 || height === 0) return; // nothing to fit | |
const scale = 0.8 / Math.max(width / fullWidth, height / fullHeight); | |
const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY]; | |
svg.transition() | |
.duration(750) | |
.call( | |
d3.zoom().transform, | |
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale) | |
); | |
}; | |
window.expandAll = () => { | |
node.style("display", "block"); | |
link.style("display", "block"); | |
simulation.alpha(1).restart(); | |
}; | |
window.collapseAll = () => { | |
node.style("display", d => d.id === 'root' ? "block" : "none"); | |
link.style("display", "none"); | |
simulation.alpha(1).restart(); | |
}; | |
d3.select("#charge").on("input", function () { | |
simulation.force("charge").strength(+this.value); | |
simulation.alpha(1).restart(); | |
}); | |
window.searchNode = () => { | |
const searchTerm = document.getElementById("searchInput").value.toLowerCase(); | |
node.style("opacity", d => d.label.toLowerCase().includes(searchTerm) ? 1 : 0.1); | |
link.style("opacity", 0.1); | |
}; | |
} | |
// Function to load graph data and initialize | |
function loadAndInitializeGraph(graphDataOrUrl) { | |
if (typeof graphDataOrUrl === 'string') { | |
// If it's a URL, fetch the data | |
d3.json(graphDataOrUrl).then(data => { | |
initializeGraph(data); | |
}).catch(error => { | |
console.error("Error loading the JSON file:", error); | |
initializeGraph(defaultGraphData); | |
}); | |
} else if (typeof graphDataOrUrl === 'object') { | |
// If it's an object, use it directly | |
initializeGraph(graphDataOrUrl); | |
} else { | |
// If neither, use the default data | |
initializeGraph(defaultGraphData); | |
} | |
} | |
// Usage: | |
// loadAndInitializeGraph(someJsonObject); // To use a JSON object | |
// loadAndInitializeGraph(); // To use default data | |
// Initialize with default data | |
loadAndInitializeGraph("../memory/graph_data.json"); // To load from a file | |
</script> | |
</body> | |
</html> |