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> |