<!DOCTYPE html> <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>