update the graph and refacator that ,also add the popup for the graph
This commit is contained in:
parent
ad02a1821e
commit
a51f8b3105
|
|
@ -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";
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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",
|
||||
|
|
@ -206,7 +279,7 @@ export function ProjectManagementPage() {
|
|||
|
||||
// Infinite scroll observer
|
||||
useEffect(() => {
|
||||
const scrollContainer = document.querySelector('.overflow-auto');
|
||||
const scrollContainer = document.querySelector(".overflow-auto");
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainer || !hasMore || loadingMore) return;
|
||||
|
|
@ -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,8 +491,8 @@ export function ProjectManagementPage() {
|
|||
|
||||
const phaseColors: Record<string, string> = {
|
||||
"تحقیق و توسعه": "#FFD700", // Yellow
|
||||
"آزمایش": "#1E90FF", // Blue
|
||||
"تولید": "#32CD32", // Green
|
||||
آزمایش: "#1E90FF", // Blue
|
||||
تولید: "#32CD32", // Green
|
||||
default: "#ccc", // Fallback gray
|
||||
};
|
||||
|
||||
|
|
@ -445,7 +521,11 @@ 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
|
||||
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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -555,7 +642,10 @@ export function ProjectManagementPage() {
|
|||
{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}
|
||||
|
|
|
|||
|
|
@ -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>({
|
||||
const [countsRes, processRes] = await Promise.all([
|
||||
apiService.callInnovationProcess<EcosystemCounts>({
|
||||
ecosystem_counts_function: {},
|
||||
});
|
||||
setCounts(res.data);
|
||||
}),
|
||||
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
|
||||
const barData = counts
|
||||
? [
|
||||
{ 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 },
|
||||
{
|
||||
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 },
|
||||
];
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
</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 min-h-full">
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-300 font-persian">خطا در بارگذاری دادهها.</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Bar Chart Section */}
|
||||
<div className="h-56">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="font-persian text-sm">
|
||||
{selectedCompany ? "نمودار میلهای" : "نمودار تعداد بر اساس دستهبندی"}
|
||||
<CardHeader className="text-center pt-2 pb-3 border-b-2 border-[#3F415A]">
|
||||
<CardTitle className="font-persian text-xl text-white">
|
||||
وضعیت بازیگران اکوسیستم نوآوری و فناوری
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[180px]">
|
||||
{barData.length > 0 && (
|
||||
<CustomBarChart data={barData} dataKey="value" labelKey="name" />
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
|
||||
{/* Line/Area Chart Section */}
|
||||
<div className="h-56">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="font-persian text-sm">روند نمونه</CardTitle>
|
||||
<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>
|
||||
<CardContent className="h-[180px]">
|
||||
|
||||
{/* 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={lineData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<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>
|
||||
</CardContent>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
||||
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,36 +142,74 @@ 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)
|
||||
filter
|
||||
.append("feGaussianBlur")
|
||||
.attr("stdDeviation", "3")
|
||||
.attr("result", "coloredBlur");
|
||||
|
||||
const feMerge = filter.append("feMerge");
|
||||
feMerge.append("feMergeNode").attr("in", "coloredBlur");
|
||||
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
// Create container group
|
||||
const container = svg.append("g");
|
||||
|
||||
// Category colors
|
||||
const categoryToColor: Record<string, string> = {
|
||||
دانشگاه: "#3B82F6",
|
||||
مشاور: "#10B981",
|
||||
|
|
@ -109,187 +219,361 @@ export function NetworkGraph({ onNodeClick}: NetworkGraphProps) {
|
|||
صندوق: "#06B6D4",
|
||||
شتابدهنده: "#9333EA",
|
||||
"مرکز نوآوری": "#F472B6",
|
||||
center: "#000000",
|
||||
center: "#34D399",
|
||||
};
|
||||
|
||||
// add central node
|
||||
const CENTER_ID = "center";
|
||||
graph.addNode(CENTER_ID, {
|
||||
label: "مرکز نوآوری اصلی",
|
||||
x: 0,
|
||||
y: 0,
|
||||
size: 20,
|
||||
category: "center",
|
||||
color: categoryToColor.center,
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
// 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" });
|
||||
});
|
||||
nodeGroup.call(drag);
|
||||
|
||||
// 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);
|
||||
// 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 {
|
||||
graph.setNodeAttribute(n, "color", "#888888");
|
||||
graph.setNodeAttribute(n, "size", 7);
|
||||
graph.setNodeAttribute(n, "zIndex", 0);
|
||||
graph.setNodeAttribute(n, "opacity", 0.3);
|
||||
// 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})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
highlightByCategory(filterCategory);
|
||||
|
||||
// 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
|
||||
// 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");
|
||||
|
||||
// create renderer
|
||||
renderer = new Sigma(graph, containerRef.current, {
|
||||
renderLabels: true,
|
||||
defaultNodeColor: "#94A3B8",
|
||||
defaultEdgeColor: "#CBD5E1",
|
||||
labelColor: { color: "#fff" }, // Set label color to white
|
||||
// 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);
|
||||
});
|
||||
|
||||
sigmaRef.current = renderer;
|
||||
// Add click handlers
|
||||
nodeGroup.on("click", async function (event, d) {
|
||||
event.stopPropagation();
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 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 ?? {}) });
|
||||
};
|
||||
// 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!);
|
||||
|
||||
renderer.on("enterNode", onEnter);
|
||||
renderer.on("leaveNode", onLeave);
|
||||
renderer.on("clickNode", onClick);
|
||||
nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// if there is a pre-selected node (state), reflect it
|
||||
if (selectedNodeId) setHighlight(selectedNodeId);
|
||||
|
||||
// cleanup on re-run
|
||||
// Cleanup function
|
||||
return () => {
|
||||
try {
|
||||
renderer.removeListener("enterNode", onEnter);
|
||||
renderer.removeListener("leaveNode", onLeave);
|
||||
renderer.removeListener("clickNode", onClick);
|
||||
} catch {}
|
||||
simulation.stop();
|
||||
};
|
||||
} 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;
|
||||
};
|
||||
// rebuild whenever nodes, filterCategory or selectedNodeId changes
|
||||
}, [nodes, filterCategory, isLoading, onNodeClick, selectedNodeId]);
|
||||
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
|
||||
|
||||
// Show error message
|
||||
if (error) {
|
||||
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} گره`}
|
||||
<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>
|
||||
|
||||
<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}
|
||||
<div className="text-gray-300 font-persian text-sm">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
export default NetworkGraph;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -58,7 +51,10 @@ export function CustomBarChart({
|
|||
|
||||
{/* 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 */}
|
||||
|
|
@ -80,7 +76,8 @@ export function CustomBarChart({
|
|||
|
||||
<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 (
|
||||
|
|
@ -88,20 +85,22 @@ export function CustomBarChart({
|
|||
{/* Label */}
|
||||
<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
|
||||
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)}%`
|
||||
width: `${Math.min(percentage, 100)}%`,
|
||||
}}
|
||||
>
|
||||
{/* Add a subtle gradient effect for better visual appeal */}
|
||||
|
|
@ -110,15 +109,24 @@ export function CustomBarChart({
|
|||
</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>
|
||||
);
|
||||
|
|
@ -129,11 +137,19 @@ export function CustomBarChart({
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,63 +3,186 @@ 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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
439
pnpm-lock.yaml
439
pnpm-lock.yaml
|
|
@ -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
BIN
public/main-circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Loading…
Reference in New Issue
Block a user