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