import React, { useEffect, useMemo, useRef, useState } from "react"; import apiService from "../../lib/api"; import Graph from "graphology"; export interface Node { id: string; label: string; category: string; stageid: number; } export interface NetworkGraphProps { onNodeClick?: (node: { id: string; label?: string; [key: string]: unknown }) => void; } // Helper to robustly parse backend response function parseApiResponse(raw: any): any[] { let data = raw; try { if (typeof data === "string") data = JSON.parse(data); if (typeof data === "string") data = JSON.parse(data); } catch {} return Array.isArray(data) ? data : []; } export function NetworkGraph({ onNodeClick}: NetworkGraphProps) { const containerRef = useRef(null); const sigmaRef = useRef(null); const graphRef = useRef(null); const [nodes, setNodes] = useState([]); const [isLoading, setIsLoading] = useState(true); const [filterCategory, setFilterCategory] = useState("all"); const [selectedNodeId, setSelectedNodeId] = useState(null); // ------------- Fetch and robust parse ---------------- useEffect(() => { let aborted = false; const controller = new AbortController(); (async () => { setIsLoading(true); try { const res = await apiService.callInnovationProcess( { graph_production_function: {} } ); if (aborted) return; // Use robust parser for backend response const data = parseApiResponse(JSON.parse(res.data)?.graph_production) setNodes( data.map((item: any) => ({ id: String(item.stageid), label: item.title, category: item.category, stageid: item.stageid, })) ); console.log('Fetched nodes:', data); } catch (err: any) { if (err.name === "AbortError") { // ignore } else { console.error("Failed to fetch graph data:", err); setNodes([]); } } finally { if (!aborted) setIsLoading(false); } })(); return () => { aborted = true; controller.abort(); }; }, []); // compute unique categories const categories = useMemo(() => { const set = new Set(); nodes.forEach((n) => set.add(n.category)); return ["all", ...Array.from(set)]; }, [nodes]); // ------------- Build graph + Sigma (client-only) ---------------- useEffect(() => { // don't run on server or before container available or while loading if (typeof window === "undefined" || !containerRef.current || isLoading) return; let renderer: any = null; let Sigma: any = null; let isCancelled = false; (async () => { try { // dynamic import for sigma only const sigmaModule = await import("sigma"); Sigma = sigmaModule.default || sigmaModule.Sigma || sigmaModule; if (isCancelled || !containerRef.current) return; const graph = new Graph(); graphRef.current = graph; // color map (you can extend) const categoryToColor: Record = { دانشگاه: "#3B82F6", مشاور: "#10B981", "دانش بنیان": "#F59E0B", استارتاپ: "#EF4444", شرکت: "#8B5CF6", صندوق: "#06B6D4", شتابدهنده: "#9333EA", "مرکز نوآوری": "#F472B6", center: "#000000", }; // add central node const CENTER_ID = "center"; graph.addNode(CENTER_ID, { label: "مرکز نوآوری اصلی", x: 0, y: 0, size: 20, category: "center", color: categoryToColor.center, }); // add all nodes nodes.forEach((node, i) => { // Place nodes in a circle, but all nodes are always present const len = Math.max(1, nodes.length); const radius = Math.max(5, Math.min(20, Math.ceil(len / 2))); const angleStep = (2 * Math.PI) / len; const angle = i * angleStep; const jitter = (Math.random() - 0.5) * 0.4; const x = Math.cos(angle) * (radius + jitter); const y = Math.sin(angle) * (radius + jitter); graph.addNode(node.id, { label: node.label, x, y, size: 8, category: node.category, color: categoryToColor[node.category] || "#94A3B8", payload: node, }); graph.addEdge(CENTER_ID, node.id, { size: 1, color: "#CBD5E1" }); }); // Highlight nodes by filter const highlightByCategory = (category: string) => { graph.forEachNode((n: string, attrs: any) => { if (category === "all" || attrs.category === category) { graph.setNodeAttribute(n, "color", categoryToColor[attrs.category] || "#94A3B8"); graph.setNodeAttribute(n, "size", attrs.category === "center" ? 20 : 12); graph.setNodeAttribute(n, "zIndex", 1); graph.setNodeAttribute(n, "opacity", 1); } else { graph.setNodeAttribute(n, "color", "#888888"); graph.setNodeAttribute(n, "size", 7); graph.setNodeAttribute(n, "zIndex", 0); graph.setNodeAttribute(n, "opacity", 0.3); } }); }; highlightByCategory(filterCategory); // Listen for filterCategory changes to re-apply highlight // (This is needed if filterCategory changes after initial render) const filterListener = () => highlightByCategory(filterCategory); // Optionally, you could use a custom event or observer if needed // For now, we rely on the effect re-running due to filterCategory in deps // create renderer renderer = new Sigma(graph, containerRef.current, { renderLabels: true, defaultNodeColor: "#94A3B8", defaultEdgeColor: "#CBD5E1", labelColor: { color: "#fff" }, // Set label color to white }); sigmaRef.current = renderer; // Helper: set highlight states by mutating node attributes const setHighlight = (nodeId: string | null) => { graph.forEachNode((n: string, attrs: any) => { if (nodeId && (n === nodeId || graph.hasEdge(n, nodeId) || graph.hasEdge(nodeId, n))) { graph.setNodeAttribute(n, "size", attrs.size ? Math.min(24, attrs.size * 1.6) : 12); graph.setNodeAttribute(n, "color", attrs.color ? attrs.color : "#fff"); graph.setNodeAttribute(n, "highlighted", true); } else { graph.setNodeAttribute(n, "size", attrs.size && attrs.category === "center" ? 20 : 8); // restore original color if we stored it; otherwise keep graph.setNodeAttribute(n, "color", attrs.category === "center" ? categoryToColor.center : attrs.color); graph.setNodeAttribute(n, "highlighted", false); } }); // ask renderer to refresh (sigma v2 triggers update automatically when graph changes) }; // events: hover highlight and click select const onEnter = (e: any) => { const nodeId = e.node; setHighlight(nodeId); }; const onLeave = () => { setHighlight(selectedNodeId); // keep selected highlighted, or none }; const onClick = (e: any) => { const nodeId = e.node as string; setSelectedNodeId((prev) => (prev === nodeId ? null : nodeId)); // call external callback with payload if exists const attrs = graph.getNodeAttributes(nodeId); onNodeClick?.({ id: nodeId, label: attrs?.label, ...(attrs?.payload ?? {}) }); }; renderer.on("enterNode", onEnter); renderer.on("leaveNode", onLeave); renderer.on("clickNode", onClick); // if there is a pre-selected node (state), reflect it if (selectedNodeId) setHighlight(selectedNodeId); // cleanup on re-run return () => { try { renderer.removeListener("enterNode", onEnter); renderer.removeListener("leaveNode", onLeave); renderer.removeListener("clickNode", onClick); } catch {} }; } catch (err) { console.error("Failed to initialize graph / sigma:", err); } })(); return () => { isCancelled = true; // kill previous renderer & graph if (sigmaRef.current) { try { sigmaRef.current.kill?.(); } catch {} } sigmaRef.current = null; graphRef.current = null; if (renderer) { try { renderer.kill?.(); } catch {} } renderer = null; }; // rebuild whenever nodes, filterCategory or selectedNodeId changes }, [nodes, filterCategory, isLoading, onNodeClick, selectedNodeId]); return (
{isLoading ? "در حال بارگذاری..." : `نمایش ${filterCategory === "all" ? nodes.length : nodes.filter(n => n.category === filterCategory).length} گره`}
{/* overlay selected info */}
{selectedNodeId ? ( <>
انتخاب شده: {selectedNodeId}
) : null}
); } export default NetworkGraph;