جزییات گراف

This commit is contained in:
mahmoodsht 2025-10-02 20:41:00 +03:30
parent 9d0fd5968b
commit b4b023ec32

View File

@ -3,7 +3,6 @@ import * as d3 from "d3";
import apiService from "../../lib/api";
import { useAuth } from "../../contexts/auth-context";
// Get API base URL at module level to avoid process.env access in browser
const API_BASE_URL =
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
@ -46,7 +45,6 @@ export interface NetworkGraphProps {
onNodeClick?: (node: CompanyDetails) => void;
}
// Helper to robustly parse backend response
function parseApiResponse(raw: any): any[] {
let data = raw;
try {
@ -56,7 +54,6 @@ function parseApiResponse(raw: any): any[] {
return Array.isArray(data) ? data : [];
}
// Check if we're in browser environment
function isBrowser(): boolean {
return typeof window !== "undefined";
}
@ -70,7 +67,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
const [error, setError] = useState<string | null>(null);
const { token } = useAuth();
// Ensure component only renders on client side
useEffect(() => {
if (isBrowser()) {
const timer = setTimeout(() => setIsMounted(true), 100);
@ -78,7 +74,22 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
}
}, []);
// Fetch data from API
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<any>({
get_values_workflow_function: {
stage_id: stage_id,
},
});
}, []);
useEffect(() => {
if (!isMounted) return;
@ -99,18 +110,28 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
Object.keys(data[0] || {}),
);
// Create center node
// نود مرکزی
const centerNode: Node = {
id: "center",
label: "پتروشیمی بندر امام", //مرکز زیست بوم
label: "پتروشیمی بندر امام",
category: "center",
stageid: 0,
isCenter: true,
};
// Create ecosystem nodes
const ecosystemNodes: Node[] = data.map((item: any) => ({
id: String(item.stageid),
// دسته‌بندی‌ها
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,
@ -118,13 +139,16 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
rawData: item,
}));
// Create links (all nodes connected to center)
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
source: "center",
target: node.id,
}));
// لینک‌ها: مرکز → دسته‌بندی‌ها → نودهای نهایی
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, ...ecosystemNodes]);
setNodes([centerNode, ...categoryNodes, ...finalNodes]);
setLinks(graphLinks);
} catch (err: any) {
if (err.name !== "AbortError") {
@ -142,43 +166,18 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
aborted = true;
controller.abort();
};
}, [isMounted, token]);
}, [isMounted, token, getImageUrl]);
// Get image URL for a node
const getImageUrl = useCallback(
(stageid: number) => {
if (!token?.accessToken) return null;
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
},
[token?.accessToken],
);
// Import apiService for the onClick handler
const callAPI = useCallback(async (stage_id: number) => {
return await apiService.call<any>({
get_values_workflow_function: {
stage_id: stage_id,
},
});
}, []);
// Initialize D3 graph
useEffect(() => {
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) {
return;
}
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;
// Clear previous content
svg.selectAll("*").remove();
// Create defs for patterns and filters
const defs = svg.append("defs");
// Add glow filter for hover effect
const filter = defs
.append("filter")
.attr("id", "glow")
@ -196,20 +195,15 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
// Create zoom behavior
const container = svg.append("g");
const zoom = d3
.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
.on("zoom", (event) => {
container.attr("transform", event.transform);
});
.scaleExtent([0.3, 2.5])
.on("zoom", (event) => container.attr("transform", event.transform));
svg.call(zoom);
// Create container group
const container = svg.append("g");
// Category colors
const categoryToColor: Record<string, string> = {
دانشگاه: "#3B82F6",
مشاور: "#10B981",
@ -222,7 +216,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
center: "#34D399",
};
// Create force simulation
const simulation = d3
.forceSimulation<Node>(nodes)
.force(
@ -231,16 +224,15 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.forceLink<Node, Link>(links)
.id((d) => d.id)
.distance(150)
.strength(0.1),
.strength(0.2),
)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collision",
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
);
.force("radial", d3.forceRadial(d => d.isCenter ? 0 : 300, width/2, height/2))
.force("collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35)));
const initialScale = 0.85;
// Initial zoom to show entire graph
const initialScale = 0.6;
const initialTranslate = [
width / 2 - (width / 2) * initialScale,
height / 2 - (height / 2) * initialScale,
@ -252,25 +244,60 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.scale(initialScale),
);
// Fix center node position
const centerNode = nodes.find((n) => n.isCenter);
// Fix center node
const centerNode = nodes.find(n => n.isCenter);
const categoryNodes = nodes.filter(n => !n.isCenter && n.stageid === -1);
if (centerNode) {
centerNode.fx = width / 2;
centerNode.fy = height / 2;
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);
});
}
// Create links
// نودهای نهایی **هیچ 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("line")
.append("path")
.attr("class", "link")
.attr("stroke", "#E2E8F0")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.6);
.attr("stroke-opacity", 0.6)
.attr("fill", "none");
// Create node groups
const nodeGroup = container
.selectAll(".node")
.data(nodes)
@ -279,7 +306,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("class", "node")
.style("cursor", "pointer");
// Add drag behavior
const drag = d3
.drag<SVGGElement, Node>()
.on("start", (event, d) => {
@ -301,18 +327,16 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
nodeGroup.call(drag);
// Add node circles/rectangles
nodeGroup.each(function (d) {
const group = d3.select(this);
if (d.isCenter) {
// Center node as rectangle
const rect = group
.append("rect")
.attr("width", 150)
.attr("height", 60)
.attr("x", -75)
.attr("y", -30)
.attr("width", 200)
.attr("height", 80)
.attr("x", -100) // نصف عرض جدید منفی
.attr("y", -40) // نصف ارتفاع جدید منفی
.attr("rx", 8)
.attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8")
@ -320,7 +344,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("stroke-width", 3)
.style("pointer-events", "none");
// Add center image if available
if (d.imageUrl || d.isCenter) {
const pattern = defs
.append("pattern")
@ -334,23 +357,21 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.append("image")
.attr("x", 0)
.attr("y", 0)
.attr("width", 150)
.attr("height", 60)
.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})`);
}
} else {
// Regular nodes as circles
const circle = group
.append("circle")
.attr("r", 25)
.attr("fill", categoryToColor[d.category] || "8#fff")
.attr("fill", categoryToColor[d.category] || "#fff")
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 3);
// Add node image if available
if (d.imageUrl) {
const pattern = defs
.append("pattern")
@ -367,10 +388,8 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("width", 50)
.attr("height", 50)
.attr("href", d.imageUrl)
.attr("backgroundColor", "#fff")
.attr("preserveAspectRatio", "xMidYMid slice");
// Create circular clip path
defs
.append("clipPath")
.attr("id", `clip-${d.id}`)
@ -384,7 +403,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
}
});
// Add labels below nodes
const labels = nodeGroup
.append("text")
.text((d) => d.label)
@ -397,7 +415,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("stroke-width", 4)
.attr("paint-order", "stroke");
// Add hover effects
nodeGroup
.on("mouseenter", function (event, d) {
if (d.isCenter) return;
@ -419,22 +436,17 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("stroke-width", 3);
});
// Add click handlers
nodeGroup.on("click", async function (event, d) {
event.stopPropagation();
// Don't handle center node clicks
if (d.isCenter) return;
if (onNodeClick && d.stageid) {
try {
// Fetch detailed company data
const res = await callAPI(d.stageid);
const responseData = JSON.parse(res.data);
const fieldValues =
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
// Filter out image fields and find description
const filteredFields = fieldValues.filter(
(field: any) =>
!["image", "img", "full_name", "about_collaboration"].includes(
@ -461,7 +473,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
onNodeClick(companyDetails);
} catch (error) {
console.error("Failed to fetch company details:", error);
// Fallback to basic info
const basicDetails: CompanyDetails = {
id: d.id,
label: d.label,
@ -474,24 +485,26 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
}
});
// Update positions on simulation tick
simulation.on("tick", () => {
link
.attr("x1", (d) => (d.source as Node).x!)
.attr("y1", (d) => (d.source as Node).y!)
.attr("x2", (d) => (d.target as Node).x!)
.attr("y2", (d) => (d.target as Node).y!);
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})`);
});
// Cleanup function
return () => {
simulation.stop();
};
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
// Show error message
if (error) {
return (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
@ -505,7 +518,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
);
}
// Don't render on server side
if (!isMounted) {
return (
<div className="w-full h-full flex items-center justify-center bg-transparent">
@ -519,14 +531,11 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
if (isLoading) {
return (
<div className="w-full h-full relative bg-transparent">
{/* Skeleton Graph Container */}
<div className="w-full h-full flex items-center justify-center relative">
{/* Center Node Skeleton */}
<div className="w-12 h-12 rounded-lg bg-gray-600 animate-pulse relative z-10">
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-gray-500 to-gray-600 animate-pulse"></div>
</div>
{/* Outer Ring Nodes Skeleton */}
{Array.from({ length: 8 }).map((_, i) => {
const angle = (i * 2 * Math.PI) / 8;
const radius = 120;
@ -547,42 +556,28 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
<div
className="absolute w-16 h-3 bg-gray-600 rounded animate-pulse"
style={{
left: "50%",
top: "40px",
transform: "translateX(-50%)",
animationDelay: `${i * 200 + 100}ms`,
}}
></div>
<div
className="absolute w-0.5 bg-gray-600 animate-pulse opacity-30"
style={{
left: "50%",
top: "50%",
height: `${radius - 16}px`,
transformOrigin: "top",
transform: `translateX(-50%) rotate(${angle + Math.PI}rad)`,
animationDelay: `${i * 100}ms`,
transform: `rotate(${(i * 360) / 8}deg) translateX(32px)`,
transformOrigin: "left center",
}}
></div>
</div>
);
})}
</div>
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2">
<div className="text-white font-persian text-sm animate-pulse">
در حال بارگذاری نمودار...
</div>
</div>
</div>
);
}
return (
<div className="w-full h-full relative bg-transparent overflow-hidden">
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
<div className="w-full h-full">
<svg
ref={svgRef}
className="w-full h-full bg-transparent"
style={{ cursor: "grab" }}
/>
</div>
);
}
export default NetworkGraph;