Spaces:
Running
Running
| // Graph Data - Reconstructed Structure | |
| const graphData = { | |
| nodes: [ | |
| { id: "About", group: 1 }, | |
| { id: "Notes", group: 2 }, | |
| { id: "Projects", group: 2 }, | |
| { id: "Audio", group: 2 }, | |
| { id: "Visual", group: 3 } | |
| ], | |
| links: [ | |
| { source: "About", target: "Notes" }, | |
| { source: "About", target: "Projects" }, | |
| { source: "About", target: "Audio" }, | |
| { source: "About", target: "Visual" } | |
| ], | |
| }; | |
| // Setup Canvas | |
| const container = document.getElementById('graph-container'); | |
| let width = container.clientWidth; | |
| let height = container.clientHeight; | |
| // Clear any existing SVG to prevent duplicates on reload | |
| d3.select("#graph-container").selectAll("*").remove(); | |
| const svg = d3.select("#graph-container") | |
| .append("svg") | |
| .attr("width", "100%") | |
| .attr("height", "100%") | |
| .attr("viewBox", [0, 0, width, height]) | |
| .call(d3.zoom().on("zoom", (event) => { | |
| g.attr("transform", event.transform); | |
| })); | |
| const g = svg.append("g"); | |
| // Simulation Setup | |
| const simulation = d3.forceSimulation(graphData.nodes) | |
| .force("link", d3.forceLink(graphData.links).id(d => d.id).distance(150)) | |
| .force("charge", d3.forceManyBody().strength(-400)) | |
| .force("center", d3.forceCenter(width / 2, height / 2)) | |
| .force("collide", d3.forceCollide().radius(30)); | |
| // Render Links | |
| const link = g.append("g") | |
| .attr("class", "links") | |
| .selectAll("line") | |
| .data(graphData.links) | |
| .join("line") | |
| .attr("stroke-width", 2) | |
| // CSS variables are handled by style.css targeting 'line' | |
| // but we can add specific classes if needed | |
| .attr("class", "graph-link"); | |
| // Render Nodes | |
| const node = g.append("g") | |
| .attr("class", "nodes") | |
| .selectAll("g") | |
| .data(graphData.nodes) | |
| .join("g") | |
| .call(d3.drag() | |
| .on("start", dragstarted) | |
| .on("drag", dragged) | |
| .on("end", dragended)); | |
| // Node Circles | |
| node.append("circle") | |
| .attr("r", 8) | |
| .attr("class", "graph-node"); | |
| // Node Labels | |
| node.append("text") | |
| .attr("x", 12) | |
| .attr("y", "0.31em") | |
| .text(d => d.id) | |
| .attr("class", "graph-text") | |
| .clone(true).lower() | |
| .attr("fill", "none") | |
| .attr("stroke", "var(--bg-primary)") | |
| .attr("stroke-width", 3); | |
| // Simulation 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})`); | |
| }); | |
| // Drag Interaction Functions | |
| 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; | |
| } | |
| // Handle Window Resize | |
| window.addEventListener('resize', () => { | |
| width = container.clientWidth; | |
| height = container.clientHeight; | |
| simulation.force("center", d3.forceCenter(width / 2, height / 2)); | |
| simulation.alpha(0.3).restart(); | |
| }); | |
| // Populate Sidebar (Optional integration) | |
| const nodeList = document.getElementById('node-list'); | |
| if (nodeList) { | |
| nodeList.innerHTML = ''; // Clear existing | |
| graphData.nodes.forEach(node => { | |
| const item = document.createElement('div'); | |
| item.style.padding = '0.5rem'; | |
| item.style.cursor = 'pointer'; | |
| item.style.borderBottom = '1px solid var(--border-color)'; | |
| item.textContent = node.id; | |
| item.addEventListener('click', () => { | |
| // Optional: Zoom to node | |
| console.log('Focus on:', node.id); | |
| }); | |
| nodeList.appendChild(item); | |
| }); | |
| } |