جزییات گراف
This commit is contained in:
parent
9d0fd5968b
commit
b4b023ec32
|
|
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user