import * as d3 from "d3"; import { useCallback, useEffect, useRef, useState } from "react"; import { useStoredDate } from "~/hooks/useStoredDate"; import { EventBus } from "~/lib/utils"; import type { CalendarDate } from "~/types/util.type"; import { useAuth } from "../../contexts/auth-context"; import apiService from "../../lib/api"; const API_BASE_URL = //بندر امام // import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api"; //آپادانا import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api"; //نوری // import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api"; export interface Node { id: string; label: string; category: string; stageid: number; imageUrl?: string; x?: number; y?: number; fx?: number | null; fy?: number | null; isCenter?: boolean; rawData?: any; } export interface Link { source: string; target: string; } export interface CompanyDetails { id: string; label: string; category: string; stageid: number; fields: { F: string; N: string; V: string; T: number; U: string; S: boolean; }[]; description?: string; } export interface NetworkGraphProps { onNodeClick?: (node: CompanyDetails) => void; onLoadingChange?: (loading: boolean) => void; } 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 : []; } function isBrowser(): boolean { return typeof window !== "undefined"; } export function NetworkGraph({ onNodeClick, onLoadingChange, }: NetworkGraphProps) { const svgRef = useRef(null); const [nodes, setNodes] = useState([]); const [links, setLinks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isMounted, setIsMounted] = useState(false); const [error, setError] = useState(null); const { token } = useAuth(); // const [date, setDate] = useState(); const [date, setDate] = useStoredDate(); useEffect(() => { const handler = (date: CalendarDate) => { if (date) setDate(date); }; EventBus.on("dateSelected", handler); return () => { EventBus.off("dateSelected", handler); }; }, []); useEffect(() => { if (isBrowser()) { const timer = setTimeout(() => setIsMounted(true), 100); return () => clearTimeout(timer); } }, []); const getImageUrl = useCallback( (stageid: number) => { if (!token?.accessToken) return null; return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`; }, [token?.accessToken] ); const callAPI = useCallback( async (stage_id: number) => { return await apiService.call({ get_values_workflow_function: { stage_id: stage_id, // start_date: date?.start || null, // end_date: date?.end || null, }, }); }, [date] ); useEffect(() => { if (!isMounted) return; let aborted = false; const controller = new AbortController(); (async () => { setIsLoading(true); try { const res = await apiService.call({ graph_production_function: { start_date: date.start || null, end_date: date.end || null, }, }); if (aborted) return; const data = parseApiResponse(JSON.parse(res.data)?.graph_production); console.log( "All available fields in first item:", Object.keys(data[0] || {}) ); // نود مرکزی const centerNode: Node = { id: "center", // label: "پتروشیمی بندر امام", // label: "پتروشیمی نوری", label: "پتروشیمی آپادانا", category: "center", stageid: 0, isCenter: true, }; // دسته‌بندی‌ها const categories = Array.from( new Set(data.map((item: any) => item.category)) ); const categoryNodes: Node[] = categories.map((cat, index) => ({ id: `cat-${index}`, label: cat, category: cat, stageid: -1, })); // نودهای نهایی const finalNodes: Node[] = data.map((item: any) => ({ id: `node-${item.stageid}`, label: item.title, category: item.category, stageid: item.stageid, imageUrl: getImageUrl(item.stageid), rawData: item, })); // لینک‌ها: مرکز → دسته‌بندی‌ها → نودهای نهایی const graphLinks: Link[] = [ ...categoryNodes.map((cat) => ({ source: "center", target: cat.id })), ...finalNodes.map((node) => { const catIndex = categories.indexOf(node.category); return { source: `cat-${catIndex}`, target: node.id }; }), ]; setNodes([centerNode, ...categoryNodes, ...finalNodes]); setLinks(graphLinks); } catch (err: any) { if (err.name !== "AbortError") { console.error("Failed to fetch graph data:", err); setError("Failed to load graph data"); setNodes([]); setLinks([]); } } finally { if (!aborted) setIsLoading(false); } })(); return () => { aborted = true; controller.abort(); }; }, [isMounted, token, getImageUrl, date]); useEffect(() => { if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) return; const svg = d3.select(svgRef.current); const width = svgRef.current.clientWidth; const height = svgRef.current.clientHeight; svg.selectAll("*").remove(); const defs = svg.append("defs"); const filter = defs .append("filter") .attr("id", "glow") .attr("x", "-50%") .attr("y", "-50%") .attr("width", "200%") .attr("height", "200%"); filter .append("feGaussianBlur") .attr("stdDeviation", "3") .attr("result", "coloredBlur"); const feMerge = filter.append("feMerge"); feMerge.append("feMergeNode").attr("in", "coloredBlur"); feMerge.append("feMergeNode").attr("in", "SourceGraphic"); const container = svg.append("g"); const zoom = d3 .zoom() .scaleExtent([0.3, 2.5]) .on("zoom", (event) => container.attr("transform", event.transform)); svg.call(zoom); const categoryToColor: Record = { دانشگاه: "#3B82F6", مشاور: "#10B981", "دانش بنیان": "#F59E0B", استارتاپ: "#EF4444", "تامین کننده": "#8B5CF6", صندوق: "#06B6D4", شتابدهنده: "#9333EA", "مرکز نوآوری": "#F472B6", center: "#34D399", }; const simulation = d3 .forceSimulation(nodes) .force( "link", d3 .forceLink(links) .id((d) => d.id) .distance(150) .strength(0.2) ) .force("charge", d3.forceManyBody().strength(-300)) .force("center", d3.forceCenter(width / 2, height / 2)) .force( "radial", d3.forceRadial((d) => (d.isCenter ? 0 : 300), width / 2, height / 2) ) .force( "collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35)) ); // Initial zoom to show entire graph const initialScale = 0.6; const initialTranslate = [ width / 2 - (width / 2) * initialScale, height / 2 - (height / 2) * initialScale, ]; svg.call( zoom.transform, d3.zoomIdentity .translate(initialTranslate[0], initialTranslate[1]) .scale(initialScale) ); // Fix center node const centerNode = nodes.find((n) => n.isCenter); const categoryNodes = nodes.filter((n) => !n.isCenter && n.stageid === -1); if (centerNode) { const centerX = width / 2; const centerY = height / 2; centerNode.fx = centerX; centerNode.fy = centerY; const baseRadius = 450; // شعاع پایه const variation = 100; // تغییر طول یکی در میان const angleStep = (2 * Math.PI) / categoryNodes.length; categoryNodes.forEach((catNode, i) => { const angle = i * angleStep; const radius = baseRadius + (i % 2 === 0 ? -variation : variation); catNode.fx = centerX + radius * Math.cos(angle); catNode.fy = centerY + radius * Math.sin(angle); }); } // نودهای نهایی **هیچ fx/fy نداشته باشند** // فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد // const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1); // categoryNodes.forEach((catNode) => { // const childNodes = finalNodes.filter(n => n.category === catNode.category); // const childCount = childNodes.length; // const radius = 100; // فاصله از دسته // const angleStep = (2 * Math.PI) / childCount; // childNodes.forEach((node, i) => { // const angle = i * angleStep; // node.fx = catNode.fx! + radius * Math.cos(angle); // node.fy = catNode.fy! + radius * Math.sin(angle); // }); // }); // Curved links const link = container .selectAll(".link") .data(links) .enter() .append("path") .attr("class", "link") .attr("stroke", "#E2E8F0") .attr("stroke-width", 2) .attr("stroke-opacity", 0.6) .attr("fill", "none"); const nodeGroup = container .selectAll(".node") .data(nodes) .enter() .append("g") .attr("class", "node") .style("cursor", (d) => (d.stageid === -1 ? "default" : "pointer")); const drag = d3 .drag() .on("start", (event, d) => { if (!event.active) simulation.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) simulation.alphaTarget(0); if (!d.isCenter) { d.fx = null; d.fy = null; } }); nodeGroup.call(drag); nodeGroup.each(function (d) { const group = d3.select(this); // if (d.isCenter) { // const rect = group // .append("rect") // .attr("width", 200) // .attr("height", 80) // .attr("x", -100) // نصف عرض جدید منفی // .attr("y", -40) // نصف ارتفاع جدید منفی // .attr("rx", 8) // .attr("ry", 8) // .attr("fill", categoryToColor[d.category] || "#94A3B8") // .attr("stroke", "#FFFFFF") // .attr("stroke-width", 3) // .style("pointer-events", "none"); // if (d.imageUrl || d.isCenter) { // const pattern = defs // .append("pattern") // .attr("id", `image-${d.id}`) // .attr("x", 0) // .attr("y", 0) // .attr("width", 1) // .attr("height", 1); // pattern // .append("image") // .attr("x", 0) // .attr("y", 0) // .attr("width", 200) // ← هم‌اندازه با مستطیل // .attr("height", 80) // .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl) // .attr("preserveAspectRatio", "xMidYMid slice"); // rect.attr("fill", `url(#image-${d.id})`); // } // } // راه حل ساده‌تر - ابعاد ثابت با حفظ نسبت if (d.isCenter) { //آپادانا const fixedWidth = 198; const fixedHeight = 200; // یا می‌توانید براساس نسبت تصویر محاسبه کنید //بندر امام // const fixedWidth = 100; // const fixedHeight = 80; // یا می‌توانید براساس نسبت تصویر محاسبه کنید //نوری // const fixedWidth = 100; // const fixedHeight = 80; // یا می‌توانید براساس نسبت تصویر محاسبه کنید const rect = group .append("rect") .attr("width", fixedWidth) .attr("height", fixedHeight) .attr("x", -fixedWidth / 2) .attr("y", -fixedHeight / 2) .attr("rx", 8) .attr("ry", 8) .attr("fill", categoryToColor[d.category] || "#94A3B8") .attr("stroke", "#FFFFFF") .attr("stroke-width", 3) .style("pointer-events", "none"); const pattern = defs .append("pattern") .attr("id", `image-${d.id}`) .attr("x", 0) .attr("y", 0) .attr("width", 1) .attr("height", 1); pattern .append("image") .attr("x", 0) .attr("y", 0) .attr("width", fixedWidth) .attr("height", fixedHeight) .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl) .attr("preserveAspectRatio", "xMidYMid meet"); // حفظ نسبت تصویر rect.attr("fill", `url(#image-${d.id})`); } else { const circle = group .append("circle") .attr("r", 25) .attr("fill", categoryToColor[d.category] || "#fff") .attr("stroke", "#FFFFFF") .attr("stroke-width", 3); if (d.imageUrl) { const pattern = defs .append("pattern") .attr("id", `image-${d.id}`) .attr("x", 0) .attr("y", 0) .attr("width", 1) .attr("height", 1); pattern .append("image") .attr("x", 0) .attr("y", 0) .attr("width", 50) .attr("height", 50) .attr("href", d.imageUrl) .attr("preserveAspectRatio", "xMidYMid slice"); defs .append("clipPath") .attr("id", `clip-${d.id}`) .append("circle") .attr("r", 25); circle .attr("fill", `url(#image-${d.id})`) .attr("clip-path", `url(#clip-${d.id})`); } } }); const labels = nodeGroup .append("text") .text((d) => d.label) .attr("text-anchor", "middle") .attr("dy", (d) => { if (d.isCenter) { //آپادانا const centerNodeHeight = 200; // ارتفاع نود مرکزی //بندر امام // const centerNodeHeight = 80; // ارتفاع نود مرکزی //نوری // const centerNodeHeight = 80; // ارتفاع نود مرکزی return centerNodeHeight / 2 + 20; // نصف ارتفاع + فاصله 20px } return 45; // برای نودهای دیگر }) .attr("font-size", (d) => (d.isCenter ? "14px" : "12px")) .attr("font-weight", "bold") .attr("fill", "#F9FAFB") .attr("stroke", "rgba(17, 24, 39, 0.95)") .attr("stroke-width", 4) .attr("paint-order", "stroke"); nodeGroup .on("mouseenter", function (event, d) { if (d.isCenter) return; d3.select(this) .select(d.isCenter ? "rect" : "circle") .attr("filter", "url(#glow)") .transition() .duration(200) .attr("stroke", "#3B82F6") .attr("stroke-width", 4); }) .on("mouseleave", function (event, d) { d3.select(this) .select(d.isCenter ? "rect" : "circle") .attr("filter", null) .transition() .duration(200) .attr("stroke", "#FFFFFF") .attr("stroke-width", 3); }); nodeGroup.on("click", async function (event, d) { event.stopPropagation(); // جلوگیری از کلیک روی مرکز و دسته‌بندی‌ها if (d.isCenter || d.stageid === -1) return; if (onNodeClick && d.stageid) { // Open dialog immediately with basic info const basicDetails: CompanyDetails = { id: d.id, label: d.label, category: d.category, stageid: d.stageid, fields: [], }; onNodeClick(basicDetails); // Start loading onLoadingChange?.(true); try { if (date.start && date.end) { const res = await callAPI(d.stageid); const responseData = JSON.parse(res.data); const fieldValues = JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || []; const filteredFields = fieldValues.filter( (field: any) => !["image", "img", "full_name", "about_collaboration"].includes( field.F.toLowerCase() ) ); const descriptionField = fieldValues.find( (field: any) => field.F.toLowerCase().includes("description") || field.F.toLowerCase().includes("about_collaboration") || field.F.toLowerCase().includes("about") ); const companyDetails: CompanyDetails = { id: d.id, label: d.label, category: d.category, stageid: d.stageid, fields: filteredFields, description: descriptionField?.V || undefined, }; onNodeClick(companyDetails); } } catch (error) { console.error("Failed to fetch company details:", error); // Keep the basic details already shown } finally { // Stop loading onLoadingChange?.(false); } } }); simulation.on("tick", () => { link.attr("d", (d: any) => { const sx = (d.source as Node).x!; const sy = (d.source as Node).y!; const tx = (d.target as Node).x!; const ty = (d.target as Node).y!; const dx = tx - sx; const dy = ty - sy; const dr = Math.sqrt(dx * dx + dy * dy) * 1.2; // منحنی return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`; }); nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`); }); return () => { simulation.stop(); }; }, [nodes, links, isLoading, isMounted, onNodeClick, callAPI, date]); if (error) { return (
⚠️ خطای بارگذاری
{error}
); } if (!isMounted) { return (
در حال بارگذاری...
); } if (isLoading) { return (
{Array.from({ length: 8 }).map((_, i) => { const angle = (i * 2 * Math.PI) / 8; const radius = 120; const x = Math.cos(angle) * radius; const y = Math.sin(angle) * radius; return (
); })}
); } return (
); } export default NetworkGraph;