import { useEffect, useRef } from "react"; import * as d3 from "d3"; import { CATEGORICAL_8 } from "@/lib/colors"; export default function AttributionGraph({ graph, width = 520, height = 320 }) { const ref = useRef(null); useEffect(() => { if (!graph || !ref.current) return; const svg = d3.select(ref.current); svg.selectAll("*").remove(); const nodes = graph.nodes.map((n) => ({ ...n })); const edges = (graph.edges || []).map((e) => ({ source: e.source, target: e.target, weight: e.weight, })); const sim = d3 .forceSimulation(nodes) .force( "link", d3 .forceLink(edges) .id((d) => d.id) .distance((d) => 90 - Math.abs(d.weight) * 60) .strength((d) => Math.abs(d.weight) * 0.6), ) .force("charge", d3.forceManyBody().strength(-180)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(18)); const link = svg .append("g") .attr("stroke-opacity", 0.4) .selectAll("line") .data(edges) .enter() .append("line") .attr("stroke", (d) => (d.weight > 0 ? "#6EE7B7" : "#F87171")) .attr("stroke-width", (d) => Math.max(0.5, Math.abs(d.weight) * 2)); const maxActivation = Math.max( 1e-6, ...nodes.map((n) => Math.abs(n.activation || 0)), ); const node = svg .append("g") .selectAll("g") .data(nodes) .enter() .append("g"); node.append("circle") .attr("r", (d) => 5 + 14 * (Math.abs(d.activation || 0) / maxActivation)) .attr("fill", (_, i) => CATEGORICAL_8[i % CATEGORICAL_8.length]) .attr("stroke", "var(--ns-bg-canvas)") .attr("stroke-width", 1.5); node.append("text") .text((d) => `#${d.id}`) .attr("dy", 4) .attr("dx", 14) .attr("font-family", "JetBrains Mono, monospace") .attr("font-size", 10) .attr("fill", "var(--ns-fg-secondary)"); node.call( d3 .drag() .on("start", (event, d) => { if (!event.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) .on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; }) .on("end", (event, d) => { if (!event.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }), ); sim.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})`); }); return () => sim.stop(); }, [graph, width, height]); return (
); }