update the graph and refacator that ,also add the popup for the graph

This commit is contained in:
Saeed AB 2025-08-19 03:27:49 +03:30
parent ad02a1821e
commit a51f8b3105
9 changed files with 1799 additions and 489 deletions

View File

@ -10,11 +10,7 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import {
ChevronUp,
ChevronDown,
RefreshCw,
} from "lucide-react";
import { ChevronUp, ChevronDown, RefreshCw } from "lucide-react";
import apiService from "~/lib/api";
import toast from "react-hot-toast";
@ -23,7 +19,7 @@ interface ProjectData {
ValueP1215S1887ValueID: number;
ValueP1215S1887StageID: number;
project_no: string;
importance_project : string;
importance_project: string;
title: string;
strategic_theme: string;
value_technology_and_innovation: string;
@ -56,28 +52,103 @@ type ColumnDef = {
const columns: ColumnDef[] = [
{ key: "title", label: "عنوان پروژه", sortable: true, width: "200px" },
{ key: "importance_project", label: "میزان اهمیت", sortable: true, width: "150px" },
{ key: "strategic_theme", label: "مضمون راهبردی", sortable: true, width: "160px" },
{ key: "value_technology_and_innovation", label: "ارزش فناوری و نوآوری", sortable: true, width: "200px" },
{ key: "type_of_innovation", label: "انواع نوآوری", sortable: true, width: "140px" },
{
key: "importance_project",
label: "میزان اهمیت",
sortable: true,
width: "150px",
},
{
key: "strategic_theme",
label: "مضمون راهبردی",
sortable: true,
width: "160px",
},
{
key: "value_technology_and_innovation",
label: "ارزش فناوری و نوآوری",
sortable: true,
width: "200px",
},
{
key: "type_of_innovation",
label: "انواع نوآوری",
sortable: true,
width: "140px",
},
{ key: "innovation", label: "میزان نوآوری", sortable: true, width: "120px" },
{ key: "person_executing", label: "مسئول اجرا", sortable: true, width: "140px" },
{ key: "excellent_observer", label: "ناطر عالی", sortable: true, width: "140px" },
{
key: "person_executing",
label: "مسئول اجرا",
sortable: true,
width: "140px",
},
{
key: "excellent_observer",
label: "ناطر عالی",
sortable: true,
width: "140px",
},
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "140px" },
{ key: "moderator", label: "مجری", sortable: true, width: "140px" },
{ key: "executive_phase", label: "فاز اجرایی", sortable: true, width: "140px" },
{
key: "executive_phase",
label: "فاز اجرایی",
sortable: true,
width: "140px",
},
{ key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" },
{ key: "remaining_time", label: "زمان باقی مانده", sortable: true, width: "140px", computed: true },
{ key: "end_date", label: "تاریخ پایان (برنامه‌ریزی)", sortable: true, width: "160px" },
{ key: "renewed_duration", label: "مدت زمان تمدید", sortable: true, width: "140px" },
{ key: "done_date", label: "تاریخ پایان (واقعی)", sortable: true, width: "160px" },
{ key: "deviation_from_program", label: "متوسط انحراف برنامه‌ای", sortable: true, width: "160px" },
{ key: "approved_budget", label: "بودجه مصوب", sortable: true, width: "150px" },
{ key: "budget_spent", label: "بودجه صرف شده", sortable: true, width: "150px" },
{ key: "cost_deviation", label: "متوسط انحراف هزینه‌ای", sortable: true, width: "160px" }
{
key: "remaining_time",
label: "زمان باقی مانده",
sortable: true,
width: "140px",
computed: true,
},
{
key: "end_date",
label: "تاریخ پایان (برنامه‌ریزی)",
sortable: true,
width: "160px",
},
{
key: "renewed_duration",
label: "مدت زمان تمدید",
sortable: true,
width: "140px",
},
{
key: "done_date",
label: "تاریخ پایان (واقعی)",
sortable: true,
width: "160px",
},
{
key: "deviation_from_program",
label: "متوسط انحراف برنامه‌ای",
sortable: true,
width: "160px",
},
{
key: "approved_budget",
label: "بودجه مصوب",
sortable: true,
width: "150px",
},
{
key: "budget_spent",
label: "بودجه صرف شده",
sortable: true,
width: "150px",
},
{
key: "cost_deviation",
label: "متوسط انحراف هزینه‌ای",
sortable: true,
width: "160px",
},
];
export function ProjectManagementPage() {
const [projects, setProjects] = useState<ProjectData[]>([]);
const [loading, setLoading] = useState(false);
@ -102,7 +173,7 @@ export function ProjectManagementPage() {
try {
fetchingRef.current = true;
if (reset) {
setLoading(true);
setCurrentPage(1);
@ -115,7 +186,9 @@ export function ProjectManagementPage() {
const fetchableColumns = columns.filter((c) => !c.computed);
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
const sortCol = columns.find((c) => c.key === sortConfig.field);
const sortField = sortCol?.computed ? undefined : (sortCol?.apiField ?? sortCol?.key);
const sortField = sortCol?.computed
? undefined
: (sortCol?.apiField ?? sortCol?.key);
const response = await apiService.select({
ProcessName: "project",
@ -204,16 +277,16 @@ export function ProjectManagementPage() {
}
}, [currentPage]);
// Infinite scroll observer
// Infinite scroll observer
useEffect(() => {
const scrollContainer = document.querySelector('.overflow-auto');
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Trigger load more when scrolled to 90% of the container
if (scrollPercentage >= 0.9) {
loadMore();
@ -221,12 +294,12 @@ export function ProjectManagementPage() {
};
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
scrollContainer.addEventListener("scroll", handleScroll);
}
return () => {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll);
scrollContainer.removeEventListener("scroll", handleScroll);
}
};
}, [loadMore, hasMore, loadingMore]);
@ -324,10 +397,13 @@ export function ProjectManagementPage() {
jy = jy - (jy >= 0 ? 474 : 473);
const cycle = 1029983;
const yCycle = 474 + (jy % 2820);
const jdn = jd +
(jm <= 7 ? (jm - 1) * 31 : ((jm - 7) * 30) + 186) +
div((yCycle * 682 - 110) as number, 2816) + (yCycle - 1) * 365 +
div(jy, 2820) * cycle + (1948320 - 1);
const jdn =
jd +
(jm <= 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186) +
div((yCycle * 682 - 110) as number, 2816) +
(yCycle - 1) * 365 +
div(jy, 2820) * cycle +
(1948320 - 1);
return jdn;
};
@ -342,10 +418,10 @@ export function ProjectManagementPage() {
const a = div((div(db, 365) + 1) * 3, 4);
const da = db - a * 365;
const y = g * 400 + c * 100 + b * 4 + a;
const m = div((da * 5 + 308), 153) - 2;
const m = div(da * 5 + 308, 153) - 2;
const d = da - div((m + 4) * 153, 5) + 122;
const year = y - 4800 + div((m + 2), 12);
const month = (m + 2) % 12 + 1;
const year = y - 4800 + div(m + 2, 12);
const month = ((m + 2) % 12) + 1;
const day = d + 1;
return [year, month, day];
};
@ -415,9 +491,9 @@ export function ProjectManagementPage() {
const phaseColors: Record<string, string> = {
"تحقیق و توسعه": "#FFD700", // Yellow
"آزمایش": "#1E90FF", // Blue
"تولید": "#32CD32", // Green
default: "#ccc", // Fallback gray
آزمایش: "#1E90FF", // Blue
تولید: "#32CD32", // Green
default: "#ccc", // Fallback gray
};
const getImportanceColor = (importance: string) => {
@ -445,8 +521,12 @@ export function ProjectManagementPage() {
}
const color = days > 0 ? "#3AEA83" : days < 0 ? "#F76276" : undefined;
return (
<span dir="ltr" className="font-medium flex justify-end gap-1 items-center" style={{ color }}>
<span>روز</span> {toPersianDigits(days)}
<span
dir="ltr"
className="font-medium flex justify-end gap-1 items-center"
style={{ color }}
>
<span>روز</span> {toPersianDigits(days)}
</span>
);
}
@ -457,9 +537,12 @@ export function ProjectManagementPage() {
return (
<span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full">
<span className="text-gray-300">{String(value) || "-"}</span>
<span style=
{{backgroundColor:`${column.key === "strategic_theme" ? "#6D53FB" : column.key=== "value_technology_and_innovation" ? "#A757FF" : column.key === "type_of_innovation" ? "#E884CE" : "#C3BF8B" }`}}
className="inline-block w-2 h-2 rounded-full bg-emerald-400" />
<span
style={{
backgroundColor: `${column.key === "strategic_theme" ? "#6D53FB" : column.key === "value_technology_and_innovation" ? "#A757FF" : column.key === "type_of_innovation" ? "#E884CE" : "#C3BF8B"}`,
}}
className="inline-block w-2 h-2 rounded-full bg-emerald-400"
/>
</span>
);
case "approved_budget":
@ -506,7 +589,11 @@ export function ProjectManagementPage() {
</Badge>
);
default:
return <span className="text-gray-300">{(value && String(value)) || "-"}</span>;
return (
<span className="text-gray-300">
{(value && String(value)) || "-"}
</span>
);
}
};
@ -515,47 +602,50 @@ export function ProjectManagementPage() {
return (
<DashboardLayout title="مدیریت پروژه‌ها">
<div className="p-6 space-y-6">
{/* Data Table */}
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium bg-[#3F415A] sticky top-0 z-20"
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium bg-[#3F415A] sticky top-0 z-20"
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 20 }).map((_, index) => (
<TableRow key={`skeleton-${index}`} className="text-sm leading-tight h-8">
<TableRow
key={`skeleton-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
@ -563,7 +653,10 @@ export function ProjectManagementPage() {
>
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
<div className="h-2.5 bg-gray-600 rounded animate-pulse" style={{ width: `${Math.random() * 60 + 40}%` }} />
<div
className="h-2.5 bg-gray-600 rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }}
/>
</div>
</TableCell>
))}
@ -582,7 +675,10 @@ export function ProjectManagementPage() {
</TableRow>
) : (
projects.map((project, index) => (
<TableRow key={`${project.project_no}-${index}`} className="text-sm leading-tight h-8">
<TableRow
key={`${project.project_no}-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}

View File

@ -5,7 +5,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Area,
AreaChart,
Bar,
CartesianGrid,
ResponsiveContainer,
Tooltip,
@ -14,9 +13,26 @@ import {
} from "recharts";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils";
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 InfoPanelProps {
selectedCompany: { id: string; label?: string } | null;
selectedCompany: CompanyDetails | null;
}
interface EcosystemCounts {
@ -32,20 +48,43 @@ interface EcosystemCounts {
mou_count: string;
}
interface ProcessActorsResponse {
start_year: string;
total_count: number;
}
interface ProcessActorsData {
year: string;
value: number;
}
export function InfoPanel({ selectedCompany }: InfoPanelProps) {
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchCounts = async () => {
setIsLoading(true);
try {
const res = await apiService.callInnovationProcess<EcosystemCounts>({
ecosystem_counts_function: {},
});
setCounts(res.data);
const [countsRes, processRes] = await Promise.all([
apiService.callInnovationProcess<EcosystemCounts>({
ecosystem_counts_function: {},
}),
apiService.callInnovationProcess<ProcessActorsResponse[]>({
process_creating_actors_function: {},
}),
]);
setCounts(JSON.parse(countsRes.data));
// Process the years data and fill missing years
const processedData = processYearsData(
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
);
setProcessData(processedData);
} catch (err) {
console.error("Failed to fetch ecosystem counts:", err);
console.error("Failed to fetch data:", err);
} finally {
setIsLoading(false);
}
@ -53,114 +92,416 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
fetchCounts();
}, []);
const title = selectedCompany?.label || "نمای کلی";
const subTitle = selectedCompany ? `شناسه: ${selectedCompany.id}` : "انتخابی انجام نشده است";
// Helper function to safely parse numbers
const parseNumber = (value: string | undefined): number => {
if (!value || value === "") return 0;
const parsed = parseInt(value, 10);
return isNaN(parsed) ? 0 : parsed;
};
// Helper function to process years data and fill missing years
const processYearsData = (
data: ProcessActorsResponse[],
): ProcessActorsData[] => {
if (!data || data.length === 0) return [];
const years = data
.map((item) => parseInt(item.start_year))
.sort((a, b) => a - b);
const minYear = years[0];
const maxYear = years[years.length - 1];
const result: ProcessActorsData[] = [];
// Create a map for quick lookup
const dataMap = data.reduce(
(acc, item) => {
acc[item.start_year] = item.total_count;
return acc;
},
{} as Record<string, number>,
);
for (let year = minYear; year <= maxYear; year++) {
result.push({
year: year.toString(),
value: dataMap[year.toString()] || 0,
});
}
return result;
};
// Convert Persian years to display format without commas
const formatPersianYear = (year: string): string => {
const map: Record<string, string> = {
"0": "۰",
"1": "۱",
"2": "۲",
"3": "۳",
"4": "۴",
"5": "۵",
"6": "۶",
"7": "۷",
"8": "۸",
"9": "۹",
};
return year.replace(/[0-9]/g, (d) => map[d] ?? d);
};
// Transform counts into chart-friendly data
const barData =
counts && !selectedCompany
? [
{ name: "دانش بنیان", value: +counts.knowledge_based_count },
{ name: "مشاور", value: +counts.consultant_count },
{ name: "استارتاپ", value: +counts.startup_count },
{ name: "مرکز نوآوری", value: +counts.innovation_center_count },
{ name: "شتابدهنده", value: +counts.accelerator_count },
{ name: "دانشگاه", value: +counts.university_count },
{ name: "صندوق", value: +counts.fund_count },
{ name: "شرکت", value: +counts.company_count },
]
: [];
const barData = counts
? [
{
label: "دانش بنیان",
value: parseNumber(counts.knowledge_based_count),
},
{ label: "مشاور", value: parseNumber(counts.consultant_count) },
{ label: "استارتاپ", value: parseNumber(counts.startup_count) },
{
label: "مرکز نوآوری",
value: parseNumber(counts.innovation_center_count),
},
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
{ label: "دانشگاه", value: parseNumber(counts.university_count) },
{ label: "صندوق", value: parseNumber(counts.fund_count) },
{ label: "شرکت", value: parseNumber(counts.company_count) },
]
: [];
const lineData = [
{ month: "01", value: 3 },
{ month: "02", value: 6 },
{ month: "03", value: 4 },
{ month: "04", value: 9 },
{ month: "05", value: 7 },
{ month: "06", value: 11 },
];
return (
<div className="space-y-4 min-h-full">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]">
<CardHeader className="pb-2">
<CardTitle className="font-persian text-base">{title}</CardTitle>
<div className="text-xs text-gray-400 font-persian">{subTitle}</div>
if (isLoading) {
return (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] min-h-full flex flex-col">
{/* Header Skeleton */}
<CardHeader className="text-center pb-2">
<div className="w-48 h-6 bg-gray-600 rounded animate-pulse mx-auto mb-4"></div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-sm text-gray-300 font-persian">در حال بارگذاری...</div>
) : selectedCompany ? (
<div className="text-sm text-gray-300 font-persian">
این یک باکس اطلاعات نمونه است. پس از دریافت API، جزئیات شرکت نمایش داده میشود.
</div>
) : counts ? (
<div className="space-y-4 text-sm text-gray-300 font-persian">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="font-bold">تعداد بازیگران اکوسیستم: </span>
{counts.actor_count}
</div>
<div>
<span className="font-bold">تعداد تفاهم نامه ها: </span>
{counts.mou_count}
</div>
</div>
{/* Actor Count Skeleton */}
<CardHeader className="text-center pt-0 pb-4">
<div className="w-full h-5 bg-gray-600 rounded animate-pulse mx-auto mb-2"></div>
</CardHeader>
{/* Grid for categories */}
<div className="grid grid-cols-2 gap-4">
<div>دانش بنیان: {counts.knowledge_based_count}</div>
<div>مشاور: {counts.consultant_count}</div>
<div>استارتاپ: {counts.startup_count}</div>
<div>مرکز نوآوری: {counts.innovation_center_count}</div>
<div>شتابدهنده: {counts.accelerator_count}</div>
<div>دانشگاه: {counts.university_count}</div>
<div>صندوق: {counts.fund_count}</div>
<div>شرکت: {counts.company_count}</div>
{/* Bar Chart Skeleton */}
<CardContent className="flex-1 px-6">
<div className="w-full space-y-4">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="flex items-center gap-3"
style={{ animationDelay: `${i * 100}ms` }}
>
{/* Label skeleton */}
<div className="w-24 h-4 bg-gray-600 rounded animate-pulse"></div>
{/* Bar skeleton */}
<div className="flex-1 bg-gray-700 rounded-full h-6">
<div
className="h-6 bg-gray-500 rounded-full animate-pulse"
style={{ width: `${Math.random() * 60 + 20}%` }}
></div>
</div>
{/* Value skeleton */}
<div className="w-8 h-4 bg-gray-600 rounded animate-pulse"></div>
</div>
</div>
) : (
<div className="text-sm text-gray-300 font-persian">خطا در بارگذاری دادهها.</div>
)}
))}
</div>
</CardContent>
{/* Bar Chart Section */}
<div className="h-56">
<CardHeader className="pb-0">
<CardTitle className="font-persian text-sm">
{selectedCompany ? "نمودار میله‌ای" : "نمودار تعداد بر اساس دسته‌بندی"}
</CardTitle>
</CardHeader>
<CardContent className="h-[180px]">
{barData.length > 0 && (
<CustomBarChart data={barData} dataKey="value" labelKey="name" />
)}
</CardContent>
</div>
{/* Area Chart Skeleton */}
<CardContent className="px-6 pb-4">
<div className="mb-4">
<div className="w-40 h-5 bg-gray-600 rounded animate-pulse mb-4"></div>
</div>
<div className="h-64 bg-gray-700 rounded-lg relative overflow-hidden">
{/* Chart skeleton */}
<div className="absolute inset-4">
{/* Y-axis skeleton */}
<div className="absolute left-0 top-0 bottom-8 w-px bg-gray-600"></div>
{/* X-axis skeleton */}
<div className="absolute left-0 bottom-8 right-0 h-px bg-gray-600"></div>
{/* Line/Area Chart Section */}
<div className="h-56">
<CardHeader className="pb-0">
<CardTitle className="font-persian text-sm">روند نمونه</CardTitle>
</CardHeader>
<CardContent className="h-[180px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={lineData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Area
type="monotone"
dataKey="value"
stroke="#34d399"
fill="rgba(52, 211, 153, 0.25)"
{/* Area chart skeleton */}
<svg className="w-full h-full">
<defs>
<linearGradient
id="areaGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="#34D399" stopOpacity="0.3" />
<stop offset="100%" stopColor="#34D399" stopOpacity="0.1" />
</linearGradient>
</defs>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60"
stroke="#34D399"
strokeWidth="2"
fill="none"
className="animate-pulse"
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</div>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60 L 340 180 L 20 180 Z"
fill="url(#areaGradient)"
className="animate-pulse"
/>
</svg>
{/* Data points skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
style={{
left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`,
animationDelay: `${i * 200}ms`,
}}
></div>
))}
</div>
</div>
</CardContent>
{/* Footer Skeleton */}
<CardContent className="pt-0 pb-6">
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center"></div>
</CardContent>
</Card>
);
}
if (isLoading) {
return (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] min-h-full flex flex-col">
{/* Header Skeleton */}
<CardHeader className="text-center pb-2">
<div className="w-48 h-6 rounded animate-pulse mx-auto mb-4"></div>
</CardHeader>
{/* Actor Count Skeleton */}
<CardHeader className="text-center pt-0 pb-4">
<div className="w-36 h-5 rounded animate-pulse mx-auto mb-2"></div>
<div className="w-16 h-8 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
</CardHeader>
{/* Bar Chart Skeleton */}
<CardContent className="flex-1 px-6">
<div className="w-full space-y-4">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="flex items-center gap-3"
style={{ animationDelay: `${i * 100}ms` }}
>
{/* Label skeleton */}
<div className="w-24 h-4 bg-gray-600 rounded animate-pulse"></div>
{/* Bar skeleton */}
<div className="flex-1 bg-gray-700 rounded-full h-6">
<div
className="h-6 bg-gray-500 rounded-full animate-pulse"
style={{ width: `${Math.random() * 60 + 20}%` }}
></div>
</div>
{/* Value skeleton */}
<div className="w-8 h-4 bg-gray-600 rounded animate-pulse"></div>
</div>
))}
</div>
</CardContent>
{/* Area Chart Skeleton */}
<CardContent className="px-6 pb-4">
<div className="mb-4">
<div className="w-40 h-5 bg-gray-600 rounded animate-pulse mb-4"></div>
</div>
<div className="h-64 bg-gray-700 rounded-lg relative overflow-hidden">
{/* Chart skeleton */}
<div className="absolute inset-4">
{/* Y-axis skeleton */}
<div className="absolute left-0 top-0 bottom-8 w-px bg-gray-600"></div>
{/* X-axis skeleton */}
<div className="absolute left-0 bottom-8 right-0 h-px bg-gray-600"></div>
{/* Area chart skeleton */}
<svg className="w-full h-full">
<defs>
<linearGradient
id="areaGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="#34D399" stopOpacity="0.3" />
<stop offset="100%" stopColor="#34D399" stopOpacity="0.1" />
</linearGradient>
</defs>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60"
stroke="#34D399"
strokeWidth="2"
fill="none"
className="animate-pulse"
/>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60 L 340 180 L 20 180 Z"
fill="url(#areaGradient)"
className="animate-pulse"
/>
</svg>
{/* Data points skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
style={{
left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`,
animationDelay: `${i * 200}ms`,
}}
></div>
))}
</div>
</div>
</CardContent>
{/* Footer Skeleton */}
<CardContent className="pt-0 pb-6">
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center">
<div className="w-28 h-4 bg-gray-600 rounded animate-pulse mx-auto mb-1"></div>
<div className="w-12 h-6 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
</div>
</CardContent>
</Card>
);
}
if (!counts) {
return (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] min-h-full">
<CardContent className="flex items-center justify-center h-64">
<div className="text-sm text-gray-300 font-persian">
خطا در بارگذاری دادهها.
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]">
<CardHeader className="text-center pt-2 pb-3 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-xl text-white">
وضعیت بازیگران اکوسیستم نوآوری و فناوری
</CardTitle>
</CardHeader>
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-xl text-white flex justify-between px-4">
تعداد بازیگران اکوسیستم
<span className="font-bold text-3xl">
{formatNumber(counts.actor_count)}
</span>
</CardTitle>
</CardHeader>
{/* Actor Count Display */}
<CardHeader className="text-right text-xl py-2 pb-4 font-bold w-full">
تنوع بازیگران اکوسیستم
</CardHeader>
{/* Middle - Bar Chart */}
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
<div className="w-full">
<CustomBarChart
data={barData.map((item) => ({
label: item.label,
value: item.value,
valueSuffix: "",
valuePrefix: "",
maxValue: Math.max(...barData.map((d) => d.value)),
}))}
barHeight="h-5"
showAxisLabels={false}
/>
</div>
</CardContent>
{/* Area Chart Section */}
<CardContent className="px-2 pb-4 border-b-2 border-[#3F415A] py-4">
<div className="mb-4">
<CardTitle className="font-persian text-lg text-white mb-2">
روند ایجاد بازیگران در طول سالها
</CardTitle>
</div>
<div className="h-48">
{processData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={processData}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="rgba(255,255,255,0.1)"
/>
<XAxis
dataKey="year"
stroke="#9ca3af"
fontSize={12}
tickFormatter={formatPersianYear}
/>
<YAxis
stroke="#9ca3af"
fontSize={12}
tickFormatter={(value) => formatNumber(value)}
/>
<Tooltip
contentStyle={{
backgroundColor: "#374151",
border: "1px solid #6b7280",
borderRadius: "6px",
color: "#f3f4f6",
}}
labelFormatter={(value) =>
`سال ${formatPersianYear(value.toString())}`
}
formatter={(value) => [
formatNumber(value),
"تعداد بازیگران",
]}
/>
<Area
type="monotone"
dataKey="value"
stroke="#34d399"
fill="rgba(52, 211, 153, 0.25)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
دادهای برای نمایش وجود ندارد
</div>
)}
</div>
</CardContent>
{/* Footer - MOU Count */}
<CardContent className="py-3">
<div className="flex font-bold text-xl px-6 justify-between text-gray-300 font-persian mb-1">
تعداد تفاهم نامه ها
<span className="text-2xl">{formatNumber(counts.mou_count)}</span>
</div>
</CardContent>
</Card>
</div>
);

View File

@ -1,16 +1,49 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, { useEffect, useRef, useState, useCallback } from "react";
import * as d3 from "d3";
import apiService from "../../lib/api";
import Graph from "graphology";
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";
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: { id: string; label?: string; [key: string]: unknown }) => void;
onNodeClick?: (node: CompanyDetails) => void;
}
// Helper to robustly parse backend response
@ -23,43 +56,82 @@ function parseApiResponse(raw: any): any[] {
return Array.isArray(data) ? data : [];
}
export function NetworkGraph({ onNodeClick}: NetworkGraphProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const sigmaRef = useRef<any>(null);
const graphRef = useRef<any>(null);
const [nodes, setNodes] = useState<Node[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [filterCategory, setFilterCategory] = useState<string>("all");
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
// Check if we're in browser environment
function isBrowser(): boolean {
return typeof window !== "undefined";
}
// ------------- Fetch and robust parse ----------------
export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
const svgRef = useRef<SVGSVGElement | null>(null);
const [nodes, setNodes] = useState<Node[]>([]);
const [links, setLinks] = useState<Link[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isMounted, setIsMounted] = useState(false);
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);
return () => clearTimeout(timer);
}
}, []);
// Fetch data from API
useEffect(() => {
if (!isMounted) return;
let aborted = false;
const controller = new AbortController();
(async () => {
setIsLoading(true);
try {
const res = await apiService.callInnovationProcess<any[]>(
{ graph_production_function: {} }
);
const res = await apiService.callInnovationProcess<any[]>({
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,
}))
const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
console.log(
"All available fields in first item:",
Object.keys(data[0] || {}),
);
// Create center node
const centerNode: Node = {
id: "center",
label: "مرکز اکوسیستم",
category: "center",
stageid: 0,
isCenter: true,
};
// Create ecosystem nodes
const ecosystemNodes: Node[] = data.map((item: any) => ({
id: String(item.stageid),
label: item.title,
category: item.category,
stageid: item.stageid,
imageUrl: getImageUrl(item.stageid),
rawData: item,
}));
// Create links (all nodes connected to center)
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
source: "center",
target: node.id,
}));
setNodes([centerNode, ...ecosystemNodes]);
setLinks(graphLinks);
} catch (err: any) {
if (err.name === "AbortError") {
// ignore
} else {
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);
@ -70,224 +142,436 @@ export function NetworkGraph({ onNodeClick}: NetworkGraphProps) {
aborted = true;
controller.abort();
};
}, [isMounted, token]);
// 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.callInnovationProcess<any>({
get_values_workflow_function: {
stage_id: stage_id,
},
});
}, []);
// compute unique categories
const categories = useMemo(() => {
const set = new Set<string>();
nodes.forEach((n) => set.add(n.category));
return ["all", ...Array.from(set)];
}, [nodes]);
// ------------- Build graph + Sigma (client-only) ----------------
// Initialize D3 graph
useEffect(() => {
// don't run on server or before container available or while loading
if (typeof window === "undefined" || !containerRef.current || isLoading) return;
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) {
return;
}
let renderer: any = null;
let Sigma: any = null;
let isCancelled = false;
const svg = d3.select(svgRef.current);
const width = svgRef.current.clientWidth;
const height = svgRef.current.clientHeight;
(async () => {
try {
// dynamic import for sigma only
const sigmaModule = await import("sigma");
Sigma = sigmaModule.default || sigmaModule.Sigma || sigmaModule;
// Clear previous content
svg.selectAll("*").remove();
if (isCancelled || !containerRef.current) return;
// Create defs for patterns and filters
const defs = svg.append("defs");
const graph = new Graph();
graphRef.current = graph;
// Add glow filter for hover effect
const filter = defs
.append("filter")
.attr("id", "glow")
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");
// color map (you can extend)
const categoryToColor: Record<string, string> = {
دانشگاه: "#3B82F6",
مشاور: "#10B981",
"دانش بنیان": "#F59E0B",
استارتاپ: "#EF4444",
شرکت: "#8B5CF6",
صندوق: "#06B6D4",
شتابدهنده: "#9333EA",
"مرکز نوآوری": "#F472B6",
center: "#000000",
};
filter
.append("feGaussianBlur")
.attr("stdDeviation", "3")
.attr("result", "coloredBlur");
// add central node
const CENTER_ID = "center";
graph.addNode(CENTER_ID, {
label: "مرکز نوآوری اصلی",
x: 0,
y: 0,
size: 20,
category: "center",
color: categoryToColor.center,
});
const feMerge = filter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
// 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" });
});
// Create zoom behavior
const zoom = d3
.zoom<SVGSVGElement, unknown>()
.scaleExtent([1, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
.on("zoom", (event) => {
container.attr("transform", event.transform);
});
// 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);
svg.call(zoom);
// 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 container group
const container = svg.append("g");
// 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;
// Category colors
const categoryToColor: Record<string, string> = {
دانشگاه: "#3B82F6",
مشاور: "#10B981",
"دانش بنیان": "#F59E0B",
استارتاپ: "#EF4444",
شرکت: "#8B5CF6",
صندوق: "#06B6D4",
شتابدهنده: "#9333EA",
"مرکز نوآوری": "#F472B6",
center: "#34D399",
};
// rebuild whenever nodes, filterCategory or selectedNodeId changes
}, [nodes, filterCategory, isLoading, onNodeClick, selectedNodeId]);
return (
<div className="relative w-full h-full flex flex-col">
<div className="p-2 flex items-center gap-2">
<label className="text-sm">فیلتر:</label>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="px-2 py-1 border rounded bg-white text-gray-900 dark:bg-gray-800 dark:text-white dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-primary"
>
{categories.map((c) => (
<option key={c} value={c}>
{c === "all" ? "همه" : c}
</option>
))}
</select>
<div className="ml-4 text-sm text-gray-600">
{isLoading ? "در حال بارگذاری..." : `نمایش ${filterCategory === "all" ? nodes.length : nodes.filter(n => n.category === filterCategory).length} گره`}
// Create force simulation
const simulation = d3
.forceSimulation<Node>(nodes)
.force(
"link",
d3
.forceLink<Node, Link>(links)
.id((d) => d.id)
.distance(150)
.strength(0.1),
)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collision",
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
);
// Fix center node position
const centerNode = nodes.find((n) => n.isCenter);
if (centerNode) {
centerNode.fx = width / 2;
centerNode.fy = height / 2;
}
// Create links
const link = container
.selectAll(".link")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr("stroke", "#E2E8F0")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.6);
// Create node groups
const nodeGroup = container
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.style("cursor", "pointer");
// Add drag behavior
const drag = d3
.drag<SVGGElement, Node>()
.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);
// 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("rx", 8)
.attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8")
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 3);
// Add center image if available
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", 150)
.attr("height", 60)
.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("stroke", "#FFFFFF")
.attr("stroke-width", 3);
// Add node image if available
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("backgroundColor", "#fff")
.attr("preserveAspectRatio", "xMidYMid slice");
// Create circular clip path
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})`);
}
}
});
// Add labels below nodes
const labels = nodeGroup
.append("text")
.text((d) => d.label)
.attr("text-anchor", "middle")
.attr("dy", (d) => (d.isCenter ? 50 : 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");
// Add hover effects
nodeGroup
.on("mouseenter", function (event, d) {
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);
});
// 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",
"des",
"dec",
"description",
"collaboration",
].includes(field.F.toLowerCase()),
);
const descriptionField = fieldValues.find(
(field: any) =>
field.F.toLowerCase().includes("description") ||
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);
// Fallback to basic info
const basicDetails: CompanyDetails = {
id: d.id,
label: d.label,
category: d.category,
stageid: d.stageid,
fields: [],
};
onNodeClick(basicDetails);
}
}
});
// 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!);
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">
<div className="text-center p-8 bg-black bg-opacity-50 rounded-lg border border-gray-700">
<div className="text-red-400 text-lg font-persian mb-4">
خطای بارگذاری
</div>
<div className="text-gray-300 font-persian text-sm">{error}</div>
</div>
</div>
);
}
<div ref={containerRef} className="flex-1 relative" style={{ minHeight: 360 }} />
{/* overlay selected info */}
<div className="p-2">
{selectedNodeId ? (
<>
<div className="text-sm">انتخاب شده: {selectedNodeId}</div>
<button
onClick={() => setSelectedNodeId(null)}
className="mt-1 px-2 py-1 text-sm border rounded"
>
پاک کردن انتخاب
</button>
</>
) : null}
// Don't render on server side
if (!isMounted) {
return (
<div className="w-full h-full flex items-center justify-center bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]">
<div className="text-white font-persian text-sm">
در حال بارگذاری...
</div>
</div>
);
}
if (isLoading) {
return (
<div className="w-full h-full relative bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]">
{/* 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;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return (
<div
key={i}
className="absolute w-8 h-8 rounded-full bg-gray-600 animate-pulse"
style={{
left: `calc(50% + ${x}px - 16px)`,
top: `calc(50% + ${y}px - 16px)`,
animationDelay: `${i * 200}ms`,
}}
>
<div className="w-full h-full rounded-full bg-gradient-to-r from-gray-500 to-gray-600 animate-pulse"></div>
<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`,
}}
></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-[linear-gradient(to_bottom_left,#464861,10%,#111628)] overflow-hidden">
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
</div>
);
}

View File

@ -1,4 +1,5 @@
import * as React from "react";
import { formatNumber } from "~/lib/utils";
export interface BarChartData {
label: string;
@ -27,20 +28,12 @@ export function CustomBarChart({
barHeight = "h-6",
showAxisLabels = true,
className = "",
loading = false
loading = false,
}: CustomBarChartProps) {
// Calculate the maximum value across all data points for consistent scaling
const globalMaxValue = Math.max(...data.map(item => item.maxValue || item.value));
// Function to format numbers with Persian digits
const formatNumber = (value: number): string => {
const str = String(value);
const map: Record<string, string> = {
"0": "۰", "1": "۱", "2": "۲", "3": "۳", "4": "۴",
"5": "۵", "6": "۶", "7": "۷", "8": "۸", "9": "۹"
};
return str.replace(/[0-9]/g, (d) => map[d] ?? d);
};
const globalMaxValue = Math.max(
...data.map((item) => item.maxValue || item.value),
);
// Loading skeleton
if (loading) {
@ -49,18 +42,21 @@ export function CustomBarChart({
{title && (
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
)}
<div className="space-y-4">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="flex items-center gap-3">
{/* Label skeleton */}
<div className="h-4 bg-gray-600 rounded animate-pulse min-w-[160px]"></div>
{/* Bar skeleton */}
<div className="flex-1 bg-gray-700 rounded-full h-6">
<div className="h-6 bg-gray-600 rounded-full animate-pulse" style={{ width: `${Math.random() * 60 + 20}%` }}></div>
<div
className="h-6 bg-gray-600 rounded-full animate-pulse"
style={{ width: `${Math.random() * 60 + 20}%` }}
></div>
</div>
{/* Value skeleton */}
<div className="h-4 bg-gray-600 rounded animate-pulse min-w-[60px]"></div>
</div>
@ -77,63 +73,83 @@ export function CustomBarChart({
{title}
</h3>
)}
<div className="space-y-4">
{data.map((item, index) => {
const percentage = globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
const percentage =
globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
const displayValue = item.value.toFixed(1);
return (
<div key={index} className="flex items-center gap-3">
{/* Label */}
<span
<span
className={`font-persian text-sm min-w-[160px] text-right ${
item.labelColor || 'text-white'
item.labelColor || "text-white"
}`}
>
{item.label}
</span>
{/* Bar Container */}
<div className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}>
<div
<div
className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}
>
<div
className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${
item.color || 'bg-emerald-400'
item.color || "bg-emerald-400"
}`}
style={{
width: `${Math.min(percentage, 100)}%`
style={{
width: `${Math.min(percentage, 100)}%`,
}}
>
{/* Add a subtle gradient effect for better visual appeal */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-white/10 rounded-full"></div>
</div>
</div>
{/* Value Label */}
<span className={`font-bold text-sm min-w-[60px] text-left ${
item.color?.includes('emerald') ? 'text-emerald-400' :
item.color?.includes('blue') ? 'text-blue-400' :
item.color?.includes('purple') ? 'text-purple-400' :
item.color?.includes('red') ? 'text-red-400' :
item.color?.includes('yellow') ? 'text-yellow-400' :
'text-emerald-400'
}`}>
{item.valuePrefix || ''}{formatNumber(parseFloat(displayValue))}{item.valueSuffix || '%'}
<span
className={`font-bold text-sm min-w-[60px] text-left ${
item.color?.includes("emerald")
? "text-emerald-400"
: item.color?.includes("blue")
? "text-blue-400"
: item.color?.includes("purple")
? "text-purple-400"
: item.color?.includes("red")
? "text-red-400"
: item.color?.includes("yellow")
? "text-yellow-400"
: "text-emerald-400"
}`}
>
{item.valuePrefix || ""}
{formatNumber(parseFloat(displayValue))}
{item.valueSuffix || ""}
</span>
</div>
);
})}
{/* Axis Labels */}
{showAxisLabels && globalMaxValue > 0 && (
<div className="flex items-center gap-3 mt-6">
<span className="min-w-[160px]"></span>
<div className="flex-1 flex justify-between pt-2 border-t border-gray-700">
<span className="text-gray-400 text-xs">{formatNumber(0)}%</span>
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue / 4))}%</span>
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue / 2))}%</span>
<span className="text-gray-400 text-xs">{formatNumber(Math.round((globalMaxValue * 3) / 4))}%</span>
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue))}%</span>
<span className="text-gray-400 text-xs">{formatNumber(0)}</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue / 4))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue / 2))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round((globalMaxValue * 3) / 4))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue))}
</span>
</div>
<span className="min-w-[60px]"></span>
</div>
@ -141,4 +157,4 @@ export function CustomBarChart({
</div>
</div>
);
}
}

View File

@ -1,6 +1,13 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
export const formatNumber = (value: string | number) => {
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue);
};

View File

@ -3,67 +3,190 @@ import React from "react";
import { ProtectedRoute } from "~/components/auth/protected-route";
import { DashboardLayout } from "~/components/dashboard/layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { NetworkGraph } from "~/components/ecosystem/network-graph";
import { InfoPanel } from "~/components/ecosystem/info-panel";
import { Checkbox } from "~/components/ui/checkbox";
import { useAuth } from "~/contexts/auth-context";
import moment from "moment-jalaali";
// 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";
// Import the CompanyDetails type
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
import { formatNumber } from "~/lib/utils";
export function meta({}: Route.MetaArgs) {
return [
{ title: "زیست بوم فناوری" },
{ name: "description", content: "نمایش زیست بوم فناوری با گراف شبکه‌ای شرکت‌ها" },
{
name: "description",
content: "نمایش زیست بوم فناوری با گراف شبکه‌ای شرکت‌ها",
},
];
}
export default function EcosystemPage() {
const [selectedCompany, setSelectedCompany] = React.useState<
| { id: string; label?: string; [key: string]: unknown }
| null
>(null);
const [highlightUnpaid, setHighlightUnpaid] = React.useState(false);
moment.loadPersian({ usePersianDigits: true });
const closeDialog = () => setSelectedCompany(null);
function handleValue(val: any): any {
if (val == null) return val;
if (
typeof val === "string" &&
/^\d{4}[-/]\d{2}[-/]\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(val)
) {
return moment(val, "YYYY-MM-DD HH:mm:ss").format("YYYY/MM/DD");
}
if (
typeof val === "number" ||
(typeof val === "string" && /^-?\d+$/.test(val))
) {
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
}
return val;
}
export default function EcosystemPage() {
const [selectedCompany, setSelectedCompany] =
React.useState<CompanyDetails | null>(null);
const { token } = useAuth();
const closeDialog = () => {
setSelectedCompany(null);
};
// Construct image URL
const getImageUrl = (stageid: number) => {
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token?.accessToken}`;
};
return (
<ProtectedRoute requireAuth={true}>
<DashboardLayout title="زیست بوم فناوری">
<div className="p-4 lg:p-6">
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
<div className="lg:col-span-4">
{//<InfoPanel selectedCompany={selectedCompany} />
}
<InfoPanel selectedCompany={selectedCompany} />
</div>
<div className="lg:col-span-8">
<Card className="h-[70vh] lg:h-[calc(100vh-220px)]">
<CardHeader className="pb-2 flex flex-row items-center justify-between gap-4">
<CardTitle className="font-persian text-base">گراف شبکهای شرکتها</CardTitle>
</CardHeader>
<div className="lg:col-span-8 h-full">
<Card className="h-full overflow-hidden">
<CardContent className="p-0 h-full">
<NetworkGraph onNodeClick={setSelectedCompany} highlightUnpaid={highlightUnpaid} />
<NetworkGraph onNodeClick={setSelectedCompany} />
</CardContent>
</Card>
</div>
</div>
</div>
{/* Node info dialog */}
<Dialog open={!!selectedCompany} onOpenChange={(open) => !open && closeDialog()}>
<DialogContent className="font-persian">
<Dialog
open={!!selectedCompany}
onOpenChange={(open) => !open && closeDialog()}
>
<DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
<DialogHeader>
<DialogTitle>{selectedCompany?.label || "اطلاعات شرکت"}</DialogTitle>
<DialogDescription>
شناسه: {selectedCompany?.id}
</DialogDescription>
<DialogTitle className="text-right border-b-2 border-gray-600 py-1 mr-4 text-xl">
معرفی شرکت
</DialogTitle>
<DialogDescription className="text-center text-green-400"></DialogDescription>
</DialogHeader>
<div className="space-y-2 text-sm">
<p>Test</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Right Column - Description */}
<div className="space-y-4 p-6 border-l-2 border-gray-600">
{/* Company Image */}
<div className="flex justify-between px-10 items-center text-3xl font-bold mb-4">
{selectedCompany?.label || "اطلاعات شرکت"}
{selectedCompany?.stageid && token?.accessToken ? (
<img
src={getImageUrl(selectedCompany.stageid)}
alt={selectedCompany?.label || ""}
className="w-14 h-14 object-cover rounded-2xl"
onError={(e) => {
// Hide image and show fallback on error
e.currentTarget.style.display = "none";
if (e.currentTarget.nextSibling) {
(
e.currentTarget.nextSibling as HTMLElement
).style.display = "flex";
}
}}
/>
) : null}
<div
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-green-400 flex items-center justify-center"
style={{
display:
selectedCompany?.stageid && token?.accessToken
? "none"
: "flex",
}}
>
<svg
className="w-10 h-10 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
</div>
</div>
{selectedCompany?.description ? (
<div className="p-4 rounded-lg">
<p className="font-persian leading-relaxed">
{selectedCompany.description}
</p>
</div>
) : (
<div className="text-gray-500 font-persian text-sm">
توضیحات در دسترس نیست
</div>
)}
</div>
{/* Left Column - Company Fields */}
<div className="space-y-2">
<h3 className="font-persian text-lg font-bold">اطلاعات شرکت</h3>
{selectedCompany?.fields &&
selectedCompany.fields.length > 0 ? (
<div className="space-y-3">
{selectedCompany.fields.map((field, index) => (
<div
key={index}
className="flex justify-between items-center rounded-lg"
>
<span className="font-persian font-light">
{field.N}:
</span>
<span className="font-persian font-light text-right">
{handleValue(field.V)}
{field.U && <span className="mr-1">({field.U})</span>}
</span>
</div>
))}
</div>
) : (
<div className="text-gray-500 font-persian text-sm">
اطلاعات تکمیلی در دسترس نیست
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
</DashboardLayout>
</ProtectedRoute>
);
}
}

View File

@ -17,12 +17,16 @@
"@radix-ui/react-slot": "^1.0.2",
"@react-router/node": "^7.7.0",
"@react-router/serve": "^7.7.1",
"@sigma/node-image": "^3.0.0",
"@types/d3": "^7.4.3",
"chart.js": "^4.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"graphology": "^0.26.0",
"isbot": "^5.1.27",
"lucide-react": "^0.525.0",
"moment-jalaali": "^0.10.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hot-toast": "^2.5.2",

View File

@ -32,6 +32,12 @@ importers:
'@react-router/serve':
specifier: ^7.7.1
version: 7.8.0(react-router@7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.8.3)
'@sigma/node-image':
specifier: ^3.0.0
version: 3.0.0(sigma@3.0.2(graphology-types@0.24.8))
'@types/d3':
specifier: ^7.4.3
version: 7.4.3
chart.js:
specifier: ^4.5.0
version: 4.5.0
@ -41,6 +47,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
d3:
specifier: ^7.9.0
version: 7.9.0
graphology:
specifier: ^0.26.0
version: 0.26.0(graphology-types@0.24.8)
@ -50,6 +59,9 @@ importers:
lucide-react:
specifier: ^0.525.0
version: 0.525.0(react@19.1.0)
moment-jalaali:
specifier: ^0.10.4
version: 0.10.4
react:
specifier: ^19.1.0
version: 19.1.0
@ -962,6 +974,11 @@ packages:
cpu: [x64]
os: [win32]
'@sigma/node-image@3.0.0':
resolution: {integrity: sha512-i4WLNPugDY4jgQEZtNSiSVj4HHXOraciXLtlgdygeUxMVEhH8PJ/+Q1vQ9f/SlKFnZQ+7vH3HnsSDW6FD9aP+g==}
peerDependencies:
sigma: '>=3.0.0-beta.10'
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@ -1061,33 +1078,102 @@ packages:
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
'@types/d3-axis@3.0.6':
resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
'@types/d3-brush@3.0.6':
resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
'@types/d3-chord@3.0.6':
resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-contour@3.0.6':
resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
'@types/d3-delaunay@6.0.4':
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
'@types/d3-dispatch@3.0.7':
resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-dsv@3.0.7':
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
'@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
'@types/d3-fetch@3.0.7':
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
'@types/d3-force@3.0.10':
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
'@types/d3-format@3.0.4':
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
'@types/d3-geo@3.1.0':
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
'@types/d3-hierarchy@3.1.7':
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
'@types/d3-path@3.1.1':
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
'@types/d3-polygon@3.0.2':
resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
'@types/d3-quadtree@3.0.6':
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
'@types/d3-random@3.0.3':
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
'@types/d3-scale-chromatic@3.1.0':
resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
'@types/d3-scale@4.0.9':
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
'@types/d3-shape@3.1.7':
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
'@types/d3-time-format@4.0.3':
resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
'@types/d3-time@3.0.4':
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
'@types/d3@7.4.3':
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/node@20.19.9':
resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==}
@ -1202,6 +1288,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
compressible@2.0.18:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'}
@ -1243,18 +1333,67 @@ packages:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
d3-axis@3.0.0:
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
engines: {node: '>=12'}
d3-brush@3.0.0:
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
engines: {node: '>=12'}
d3-chord@3.0.1:
resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
engines: {node: '>=12'}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-contour@4.0.2:
resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
engines: {node: '>=12'}
d3-delaunay@6.0.4:
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-dsv@3.0.1:
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
engines: {node: '>=12'}
hasBin: true
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-fetch@3.0.1:
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
engines: {node: '>=12'}
d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
d3-geo@3.1.1:
resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
engines: {node: '>=12'}
d3-hierarchy@3.1.2:
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
@ -1263,10 +1402,30 @@ packages:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
d3-polygon@3.0.1:
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
engines: {node: '>=12'}
d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
d3-random@3.0.1:
resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
engines: {node: '>=12'}
d3-scale-chromatic@3.1.0:
resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
engines: {node: '>=12'}
d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
@ -1283,6 +1442,20 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
d3@7.9.0:
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
engines: {node: '>=12'}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@ -1311,6 +1484,9 @@ packages:
babel-plugin-macros:
optional: true
delaunator@5.0.1:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@ -1513,6 +1689,10 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
@ -1545,6 +1725,9 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jalaali-js@1.2.8:
resolution: {integrity: sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==}
jiti@2.5.0:
resolution: {integrity: sha512-NWDAhdnATItTnRhip9VTd8oXDjVcbhetRN6YzckApnXGxpGUooKMAaf0KVvlZG0+KlJMGkeLElVn4M1ReuxKUQ==}
hasBin: true
@ -1700,6 +1883,15 @@ packages:
engines: {node: '>=10'}
hasBin: true
moment-jalaali@0.10.4:
resolution: {integrity: sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==}
moment-timezone@0.5.48:
resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
morgan@1.10.1:
resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==}
engines: {node: '>= 0.8.0'}
@ -1936,11 +2128,17 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
rollup@4.45.1:
resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@ -3084,6 +3282,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.45.1':
optional: true
'@sigma/node-image@3.0.0(sigma@3.0.2(graphology-types@0.24.8))':
dependencies:
sigma: 3.0.2(graphology-types@0.24.8)
'@standard-schema/spec@1.0.0': {}
'@standard-schema/utils@0.3.0': {}
@ -3161,30 +3363,125 @@ snapshots:
'@types/d3-array@3.2.1': {}
'@types/d3-axis@3.0.6':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-brush@3.0.6':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-chord@3.0.6': {}
'@types/d3-color@3.1.3': {}
'@types/d3-contour@3.0.6':
dependencies:
'@types/d3-array': 3.2.1
'@types/geojson': 7946.0.16
'@types/d3-delaunay@6.0.4': {}
'@types/d3-dispatch@3.0.7': {}
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-dsv@3.0.7': {}
'@types/d3-ease@3.0.2': {}
'@types/d3-fetch@3.0.7':
dependencies:
'@types/d3-dsv': 3.0.7
'@types/d3-force@3.0.10': {}
'@types/d3-format@3.0.4': {}
'@types/d3-geo@3.1.0':
dependencies:
'@types/geojson': 7946.0.16
'@types/d3-hierarchy@3.1.7': {}
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
'@types/d3-path@3.1.1': {}
'@types/d3-polygon@3.0.2': {}
'@types/d3-quadtree@3.0.6': {}
'@types/d3-random@3.0.3': {}
'@types/d3-scale-chromatic@3.1.0': {}
'@types/d3-scale@4.0.9':
dependencies:
'@types/d3-time': 3.0.4
'@types/d3-selection@3.0.11': {}
'@types/d3-shape@3.1.7':
dependencies:
'@types/d3-path': 3.1.1
'@types/d3-time-format@4.0.3': {}
'@types/d3-time@3.0.4': {}
'@types/d3-timer@3.0.2': {}
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-zoom@3.0.8':
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3@7.4.3':
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-axis': 3.0.6
'@types/d3-brush': 3.0.6
'@types/d3-chord': 3.0.6
'@types/d3-color': 3.1.3
'@types/d3-contour': 3.0.6
'@types/d3-delaunay': 6.0.4
'@types/d3-dispatch': 3.0.7
'@types/d3-drag': 3.0.7
'@types/d3-dsv': 3.0.7
'@types/d3-ease': 3.0.2
'@types/d3-fetch': 3.0.7
'@types/d3-force': 3.0.10
'@types/d3-format': 3.0.4
'@types/d3-geo': 3.1.0
'@types/d3-hierarchy': 3.1.7
'@types/d3-interpolate': 3.0.4
'@types/d3-path': 3.1.1
'@types/d3-polygon': 3.0.2
'@types/d3-quadtree': 3.0.6
'@types/d3-random': 3.0.3
'@types/d3-scale': 4.0.9
'@types/d3-scale-chromatic': 3.1.0
'@types/d3-selection': 3.0.11
'@types/d3-shape': 3.1.7
'@types/d3-time': 3.0.4
'@types/d3-time-format': 4.0.3
'@types/d3-timer': 3.0.2
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
'@types/estree@1.0.8': {}
'@types/geojson@7946.0.16': {}
'@types/node@20.19.9':
dependencies:
undici-types: 6.21.0
@ -3305,6 +3602,8 @@ snapshots:
color-name@1.1.4: {}
commander@7.2.0: {}
compressible@2.0.18:
dependencies:
mime-db: 1.54.0
@ -3347,18 +3646,80 @@ snapshots:
dependencies:
internmap: 2.0.3
d3-axis@3.0.0: {}
d3-brush@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-chord@3.0.1:
dependencies:
d3-path: 3.1.0
d3-color@3.1.0: {}
d3-contour@4.0.2:
dependencies:
d3-array: 3.2.4
d3-delaunay@6.0.4:
dependencies:
delaunator: 5.0.1
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-dsv@3.0.1:
dependencies:
commander: 7.2.0
iconv-lite: 0.6.3
rw: 1.3.3
d3-ease@3.0.1: {}
d3-fetch@3.0.1:
dependencies:
d3-dsv: 3.0.1
d3-force@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-format@3.1.0: {}
d3-geo@3.1.1:
dependencies:
d3-array: 3.2.4
d3-hierarchy@3.1.2: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-path@3.1.0: {}
d3-polygon@3.0.1: {}
d3-quadtree@3.0.1: {}
d3-random@3.0.1: {}
d3-scale-chromatic@3.1.0:
dependencies:
d3-color: 3.1.0
d3-interpolate: 3.0.1
d3-scale@4.0.2:
dependencies:
d3-array: 3.2.4
@ -3367,6 +3728,8 @@ snapshots:
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-selection@3.0.0: {}
d3-shape@3.2.0:
dependencies:
d3-path: 3.1.0
@ -3381,6 +3744,56 @@ snapshots:
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
d3@7.9.0:
dependencies:
d3-array: 3.2.4
d3-axis: 3.0.0
d3-brush: 3.0.0
d3-chord: 3.0.1
d3-color: 3.1.0
d3-contour: 4.0.2
d3-delaunay: 6.0.4
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-dsv: 3.0.1
d3-ease: 3.0.1
d3-fetch: 3.0.1
d3-force: 3.0.0
d3-format: 3.1.0
d3-geo: 3.1.1
d3-hierarchy: 3.1.2
d3-interpolate: 3.0.1
d3-path: 3.1.0
d3-polygon: 3.0.1
d3-quadtree: 3.0.1
d3-random: 3.0.1
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
d3-selection: 3.0.0
d3-shape: 3.2.0
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-timer: 3.0.1
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
debug@2.6.9:
dependencies:
ms: 2.0.0
@ -3393,6 +3806,10 @@ snapshots:
dedent@1.6.0: {}
delaunator@5.0.1:
dependencies:
robust-predicates: 3.0.2
depd@2.0.0: {}
destroy@1.2.0: {}
@ -3623,6 +4040,10 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
immer@10.1.1: {}
inherits@2.0.4: {}
@ -3647,6 +4068,8 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jalaali-js@1.2.8: {}
jiti@2.5.0: {}
js-tokens@4.0.0: {}
@ -3750,6 +4173,18 @@ snapshots:
mkdirp@3.0.1: {}
moment-jalaali@0.10.4:
dependencies:
jalaali-js: 1.2.8
moment: 2.30.1
moment-timezone: 0.5.48
moment-timezone@0.5.48:
dependencies:
moment: 2.30.1
moment@2.30.1: {}
morgan@1.10.1:
dependencies:
basic-auth: 2.0.1
@ -3961,6 +4396,8 @@ snapshots:
retry@0.12.0: {}
robust-predicates@3.0.2: {}
rollup@4.45.1:
dependencies:
'@types/estree': 1.0.8
@ -3987,6 +4424,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.45.1
fsevents: 2.3.3
rw@1.3.3: {}
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}

BIN
public/main-circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB