import * as d3 from 'https://esm.sh/d3@7'; let simulation; export function mount(containerId, data, dotNetHelper) { const container = document.getElementById(containerId); if (!container) return; const width = container.clientWidth || 400; const height = container.clientHeight || 400; // Create SVG const svg = d3.select(container).append("svg") .attr("viewBox", [0, 0, width, height]) .attr("width", "100%") .attr("height", "100%"); // Radial gradient for Nebula effect const defs = svg.append("defs"); const radialGradient = defs.append("radialGradient") .attr("id", "nebulaGlow") .attr("cx", "50%") .attr("cy", "50%") .attr("r", "50%"); radialGradient.append("stop").attr("offset", "0%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 1); radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0); // Root Group for Zoom const rootGroup = svg.append("g").attr("class", "zoom-containment"); // Attach Zoom Behavior const zoom = d3.zoom() .scaleExtent([0.5, 4]) .on("zoom", (e) => rootGroup.attr("transform", e.transform)); svg.call(zoom); // Subtle Link Distance & Charge simulation = d3.forceSimulation(data.nodes) .force("link", d3.forceLink(data.links).id(d => d.id).distance(60)) .force("charge", d3.forceManyBody().strength(-150)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(25)); // Links const link = rootGroup.append("g") .selectAll("line") .data(data.links) .join("line") .attr("stroke", "#444") .attr("stroke-opacity", 0.6) .attr("stroke-width", 1.5); // Nodes const node = rootGroup.append("g") .selectAll("g") .data(data.nodes) .join("g") .style("cursor", "pointer") .on("click", (e, d) => { // Remove active state from all, add to clicked node.select("circle.node-core").classed("nexus-node-active", false); d3.select(e.currentTarget).select("circle.node-core").classed("nexus-node-active", true); dotNetHelper.invokeMethodAsync('OnNodeClicked', d.id); }) .call(drag(simulation)); // Outer glow for nodes node.append("circle") .attr("r", 14) .attr("fill", "url(#nebulaGlow)") .attr("opacity", 0.4); // Core circle node.append("circle") .attr("class", "node-core") .attr("r", 6) .attr("fill", "#888") .attr("stroke", "#222") .attr("stroke-width", 2); // Labels node.append("text") .text(d => d.label) .attr("x", 12) .attr("y", 4) .attr("fill", "#ccc") .attr("font-family", "var(--nexus-font-sans)") .attr("font-size", "0.75rem"); 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 drag(simulation) { function dragstarted(event) { if (!event.active) simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; } function dragged(event) { event.subject.fx = event.x; event.subject.fy = event.y; } function dragended(event) { if (!event.active) simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; } return d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended); } export function unmount(containerId) { if (simulation) { simulation.stop(); } const container = document.getElementById(containerId); if (container) { container.innerHTML = ''; // clear svg } } export function scrollToNode(id) { const el = document.getElementById(id); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } export function pause() { if (simulation) { simulation.stop(); } } export function resume() { if (simulation) { // give it a gentle kick to settle if moved simulation.alphaTarget(0.1).restart(); } }