diff --git a/app/components/dashboard/project-management/project-management-page.tsx b/app/components/dashboard/project-management/project-management-page.tsx index 9e1418a..44401bd 100644 --- a/app/components/dashboard/project-management/project-management-page.tsx +++ b/app/components/dashboard/project-management/project-management-page.tsx @@ -10,11 +10,7 @@ import { TableHeader, TableRow, } from "~/components/ui/table"; -import { - ChevronUp, - ChevronDown, - RefreshCw, -} from "lucide-react"; +import { ChevronUp, ChevronDown, RefreshCw } from "lucide-react"; import apiService from "~/lib/api"; import toast from "react-hot-toast"; @@ -23,7 +19,7 @@ interface ProjectData { ValueP1215S1887ValueID: number; ValueP1215S1887StageID: number; project_no: string; - importance_project : string; + importance_project: string; title: string; strategic_theme: string; value_technology_and_innovation: string; @@ -56,28 +52,103 @@ type ColumnDef = { const columns: ColumnDef[] = [ { key: "title", label: "عنوان پروژه", sortable: true, width: "200px" }, - { key: "importance_project", label: "میزان اهمیت", sortable: true, width: "150px" }, - { key: "strategic_theme", label: "مضمون راهبردی", sortable: true, width: "160px" }, - { key: "value_technology_and_innovation", label: "ارزش فناوری و نوآوری", sortable: true, width: "200px" }, - { key: "type_of_innovation", label: "انواع نوآوری", sortable: true, width: "140px" }, + { + key: "importance_project", + label: "میزان اهمیت", + sortable: true, + width: "150px", + }, + { + key: "strategic_theme", + label: "مضمون راهبردی", + sortable: true, + width: "160px", + }, + { + key: "value_technology_and_innovation", + label: "ارزش فناوری و نوآوری", + sortable: true, + width: "200px", + }, + { + key: "type_of_innovation", + label: "انواع نوآوری", + sortable: true, + width: "140px", + }, { key: "innovation", label: "میزان نوآوری", sortable: true, width: "120px" }, - { key: "person_executing", label: "مسئول اجرا", sortable: true, width: "140px" }, - { key: "excellent_observer", label: "ناطر عالی", sortable: true, width: "140px" }, + { + key: "person_executing", + label: "مسئول اجرا", + sortable: true, + width: "140px", + }, + { + key: "excellent_observer", + label: "ناطر عالی", + sortable: true, + width: "140px", + }, { key: "observer", label: "ناظر پروژه", sortable: true, width: "140px" }, { key: "moderator", label: "مجری", sortable: true, width: "140px" }, - { key: "executive_phase", label: "فاز اجرایی", sortable: true, width: "140px" }, + { + key: "executive_phase", + label: "فاز اجرایی", + sortable: true, + width: "140px", + }, { key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" }, - { key: "remaining_time", label: "زمان باقی مانده", sortable: true, width: "140px", computed: true }, - { key: "end_date", label: "تاریخ پایان (برنامه‌ریزی)", sortable: true, width: "160px" }, - { key: "renewed_duration", label: "مدت زمان تمدید", sortable: true, width: "140px" }, - { key: "done_date", label: "تاریخ پایان (واقعی)", sortable: true, width: "160px" }, - { key: "deviation_from_program", label: "متوسط انحراف برنامه‌ای", sortable: true, width: "160px" }, - { key: "approved_budget", label: "بودجه مصوب", sortable: true, width: "150px" }, - { key: "budget_spent", label: "بودجه صرف شده", sortable: true, width: "150px" }, - { key: "cost_deviation", label: "متوسط انحراف هزینه‌ای", sortable: true, width: "160px" } + { + key: "remaining_time", + label: "زمان باقی مانده", + sortable: true, + width: "140px", + computed: true, + }, + { + key: "end_date", + label: "تاریخ پایان (برنامه‌ریزی)", + sortable: true, + width: "160px", + }, + { + key: "renewed_duration", + label: "مدت زمان تمدید", + sortable: true, + width: "140px", + }, + { + key: "done_date", + label: "تاریخ پایان (واقعی)", + sortable: true, + width: "160px", + }, + { + key: "deviation_from_program", + label: "متوسط انحراف برنامه‌ای", + sortable: true, + width: "160px", + }, + { + key: "approved_budget", + label: "بودجه مصوب", + sortable: true, + width: "150px", + }, + { + key: "budget_spent", + label: "بودجه صرف شده", + sortable: true, + width: "150px", + }, + { + key: "cost_deviation", + label: "متوسط انحراف هزینه‌ای", + sortable: true, + width: "160px", + }, ]; - export function ProjectManagementPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); @@ -102,7 +173,7 @@ export function ProjectManagementPage() { try { fetchingRef.current = true; - + if (reset) { setLoading(true); setCurrentPage(1); @@ -115,7 +186,9 @@ export function ProjectManagementPage() { const fetchableColumns = columns.filter((c) => !c.computed); const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key); const sortCol = columns.find((c) => c.key === sortConfig.field); - const sortField = sortCol?.computed ? undefined : (sortCol?.apiField ?? sortCol?.key); + const sortField = sortCol?.computed + ? undefined + : (sortCol?.apiField ?? sortCol?.key); const response = await apiService.select({ ProcessName: "project", @@ -204,16 +277,16 @@ export function ProjectManagementPage() { } }, [currentPage]); - // Infinite scroll observer + // Infinite scroll observer useEffect(() => { - const scrollContainer = document.querySelector('.overflow-auto'); - + const scrollContainer = document.querySelector(".overflow-auto"); + const handleScroll = () => { if (!scrollContainer || !hasMore || loadingMore) return; - + const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; - + // Trigger load more when scrolled to 90% of the container if (scrollPercentage >= 0.9) { loadMore(); @@ -221,12 +294,12 @@ export function ProjectManagementPage() { }; if (scrollContainer) { - scrollContainer.addEventListener('scroll', handleScroll); + scrollContainer.addEventListener("scroll", handleScroll); } return () => { if (scrollContainer) { - scrollContainer.removeEventListener('scroll', handleScroll); + scrollContainer.removeEventListener("scroll", handleScroll); } }; }, [loadMore, hasMore, loadingMore]); @@ -324,10 +397,13 @@ export function ProjectManagementPage() { jy = jy - (jy >= 0 ? 474 : 473); const cycle = 1029983; const yCycle = 474 + (jy % 2820); - const jdn = jd + - (jm <= 7 ? (jm - 1) * 31 : ((jm - 7) * 30) + 186) + - div((yCycle * 682 - 110) as number, 2816) + (yCycle - 1) * 365 + - div(jy, 2820) * cycle + (1948320 - 1); + const jdn = + jd + + (jm <= 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186) + + div((yCycle * 682 - 110) as number, 2816) + + (yCycle - 1) * 365 + + div(jy, 2820) * cycle + + (1948320 - 1); return jdn; }; @@ -342,10 +418,10 @@ export function ProjectManagementPage() { const a = div((div(db, 365) + 1) * 3, 4); const da = db - a * 365; const y = g * 400 + c * 100 + b * 4 + a; - const m = div((da * 5 + 308), 153) - 2; + const m = div(da * 5 + 308, 153) - 2; const d = da - div((m + 4) * 153, 5) + 122; - const year = y - 4800 + div((m + 2), 12); - const month = (m + 2) % 12 + 1; + const year = y - 4800 + div(m + 2, 12); + const month = ((m + 2) % 12) + 1; const day = d + 1; return [year, month, day]; }; @@ -415,9 +491,9 @@ export function ProjectManagementPage() { const phaseColors: Record = { "تحقیق و توسعه": "#FFD700", // Yellow - "آزمایش": "#1E90FF", // Blue - "تولید": "#32CD32", // Green - default: "#ccc", // Fallback gray + آزمایش: "#1E90FF", // Blue + تولید: "#32CD32", // Green + default: "#ccc", // Fallback gray }; const getImportanceColor = (importance: string) => { @@ -445,8 +521,12 @@ export function ProjectManagementPage() { } const color = days > 0 ? "#3AEA83" : days < 0 ? "#F76276" : undefined; return ( - - روز {toPersianDigits(days)} + + روز {toPersianDigits(days)} ); } @@ -457,9 +537,12 @@ export function ProjectManagementPage() { return ( {String(value) || "-"} - + ); case "approved_budget": @@ -506,7 +589,11 @@ export function ProjectManagementPage() { ); default: - return {(value && String(value)) || "-"}; + return ( + + {(value && String(value)) || "-"} + + ); } }; @@ -515,47 +602,50 @@ export function ProjectManagementPage() { return (
- {/* Data Table */} + {/* Data Table */}
- - - - {columns.map((column) => ( - - {column.sortable ? ( -
+ + + {columns.map((column) => ( + + {column.sortable ? ( + - ) : ( - column.label - )} - - ))} - - - + + ) + ) : ( +
+ )} + + ) : ( + column.label + )} + + ))} + + + {loading ? ( // Skeleton loading rows (compact) Array.from({ length: 20 }).map((_, index) => ( - + {columns.map((column) => (
-
+
))} @@ -582,7 +675,10 @@ export function ProjectManagementPage() { ) : ( projects.map((project, index) => ( - + {columns.map((column) => ( (null); + const [processData, setProcessData] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchCounts = async () => { setIsLoading(true); try { - const res = await apiService.callInnovationProcess({ - ecosystem_counts_function: {}, - }); - setCounts(res.data); + const [countsRes, processRes] = await Promise.all([ + apiService.callInnovationProcess({ + ecosystem_counts_function: {}, + }), + apiService.callInnovationProcess({ + 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, + ); + + 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 = { + "0": "۰", + "1": "۱", + "2": "۲", + "3": "۳", + "4": "۴", + "5": "۵", + "6": "۶", + "7": "۷", + "8": "۸", + "9": "۹", + }; + return year.replace(/[0-9]/g, (d) => map[d] ?? d); + }; // Transform counts into chart-friendly data - const barData = - counts && !selectedCompany - ? [ - { name: "دانش بنیان", value: +counts.knowledge_based_count }, - { name: "مشاور", value: +counts.consultant_count }, - { name: "استارتاپ", value: +counts.startup_count }, - { name: "مرکز نوآوری", value: +counts.innovation_center_count }, - { name: "شتابدهنده", value: +counts.accelerator_count }, - { name: "دانشگاه", value: +counts.university_count }, - { name: "صندوق", value: +counts.fund_count }, - { name: "شرکت", value: +counts.company_count }, - ] - : []; + const barData = counts + ? [ + { + label: "دانش بنیان", + value: parseNumber(counts.knowledge_based_count), + }, + { label: "مشاور", value: parseNumber(counts.consultant_count) }, + { label: "استارتاپ", value: parseNumber(counts.startup_count) }, + { + label: "مرکز نوآوری", + value: parseNumber(counts.innovation_center_count), + }, + { label: "شتابدهنده", value: parseNumber(counts.accelerator_count) }, + { label: "دانشگاه", value: parseNumber(counts.university_count) }, + { label: "صندوق", value: parseNumber(counts.fund_count) }, + { label: "شرکت", value: parseNumber(counts.company_count) }, + ] + : []; - const lineData = [ - { month: "01", value: 3 }, - { month: "02", value: 6 }, - { month: "03", value: 4 }, - { month: "04", value: 9 }, - { month: "05", value: 7 }, - { month: "06", value: 11 }, - ]; - - return ( -
- - - {title} -
{subTitle}
+ if (isLoading) { + return ( + + {/* Header Skeleton */} + +
- - {isLoading ? ( -
در حال بارگذاری...
- ) : selectedCompany ? ( -
- این یک باکس اطلاعات نمونه است. پس از دریافت API، جزئیات شرکت نمایش داده می‌شود. -
- ) : counts ? ( -
-
-
- تعداد بازیگران اکوسیستم: - {counts.actor_count} -
-
- تعداد تفاهم نامه ها: - {counts.mou_count} -
-
+ {/* Actor Count Skeleton */} + +
+
- {/* Grid for categories */} -
-
دانش بنیان: {counts.knowledge_based_count}
-
مشاور: {counts.consultant_count}
-
استارتاپ: {counts.startup_count}
-
مرکز نوآوری: {counts.innovation_center_count}
-
شتابدهنده: {counts.accelerator_count}
-
دانشگاه: {counts.university_count}
-
صندوق: {counts.fund_count}
-
شرکت: {counts.company_count}
+ {/* Bar Chart Skeleton */} + +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ {/* Label skeleton */} +
+ + {/* Bar skeleton */} +
+
+
+ + {/* Value skeleton */} +
-
- ) : ( -
خطا در بارگذاری داده‌ها.
- )} + ))} +
- {/* Bar Chart Section */} -
- - - {selectedCompany ? "نمودار میله‌ای" : "نمودار تعداد بر اساس دسته‌بندی"} - - - - {barData.length > 0 && ( - - )} - -
+ {/* Area Chart Skeleton */} + +
+
+
+
+ {/* Chart skeleton */} +
+ {/* Y-axis skeleton */} +
+ {/* X-axis skeleton */} +
- {/* Line/Area Chart Section */} -
- - روند نمونه - - - - - - - - - + + + + + + + - - - -
+ + + + {/* Data points skeleton */} + {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+
+
+ + {/* Footer Skeleton */} + +
+
+ + ); + } + + if (isLoading) { + return ( + + {/* Header Skeleton */} + +
+
+ + {/* Actor Count Skeleton */} + +
+
+
+ + {/* Bar Chart Skeleton */} + +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ {/* Label skeleton */} +
+ + {/* Bar skeleton */} +
+
+
+ + {/* Value skeleton */} +
+
+ ))} +
+
+ + {/* Area Chart Skeleton */} + +
+
+
+
+ {/* Chart skeleton */} +
+ {/* Y-axis skeleton */} +
+ {/* X-axis skeleton */} +
+ + {/* Area chart skeleton */} + + + + + + + + + + + + {/* Data points skeleton */} + {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+
+
+ + {/* Footer Skeleton */} + +
+
+
+
+
+
+ ); + } + + if (!counts) { + return ( + + +
+ خطا در بارگذاری داده‌ها. +
+
+
+ ); + } + + return ( +
+ + + + وضعیت بازیگران اکوسیستم نوآوری و فناوری + + + + + + تعداد بازیگران اکوسیستم + + {formatNumber(counts.actor_count)} + + + + + {/* Actor Count Display */} + + تنوع بازیگران اکوسیستم + + {/* Middle - Bar Chart */} + +
+ ({ + label: item.label, + value: item.value, + valueSuffix: "", + valuePrefix: "", + maxValue: Math.max(...barData.map((d) => d.value)), + }))} + barHeight="h-5" + showAxisLabels={false} + /> +
+
+ + {/* Area Chart Section */} + +
+ + روند ایجاد بازیگران در طول سال‌ها + +
+
+ {processData.length > 0 ? ( + + + + + formatNumber(value)} + /> + + `سال ${formatPersianYear(value.toString())}` + } + formatter={(value) => [ + formatNumber(value), + "تعداد بازیگران", + ]} + /> + + + + ) : ( +
+ داده‌ای برای نمایش وجود ندارد +
+ )} +
+
+ + {/* Footer - MOU Count */} + +
+ تعداد تفاهم نامه ها + {formatNumber(counts.mou_count)} +
+
); diff --git a/app/components/ecosystem/network-graph.tsx b/app/components/ecosystem/network-graph.tsx index ea8ee7e..709012a 100644 --- a/app/components/ecosystem/network-graph.tsx +++ b/app/components/ecosystem/network-graph.tsx @@ -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(null); - const sigmaRef = useRef(null); - const graphRef = useRef(null); - const [nodes, setNodes] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [filterCategory, setFilterCategory] = useState("all"); - const [selectedNodeId, setSelectedNodeId] = useState(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(null); + const [nodes, setNodes] = useState([]); + const [links, setLinks] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isMounted, setIsMounted] = useState(false); + const [error, setError] = useState(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( - { graph_production_function: {} } - ); + const res = await apiService.callInnovationProcess({ + graph_production_function: {}, + }); if (aborted) return; - // Use robust parser for backend response - const data = parseApiResponse(JSON.parse(res.data)?.graph_production) - setNodes( - data.map((item: any) => ({ - id: String(item.stageid), - label: item.title, - category: item.category, - stageid: item.stageid, - })) + + const data = parseApiResponse(JSON.parse(res.data)?.graph_production); + console.log( + "All available fields in first item:", + Object.keys(data[0] || {}), ); + + // Create center node + const centerNode: Node = { + id: "center", + label: "مرکز اکوسیستم", + category: "center", + stageid: 0, + isCenter: true, + }; + + // Create ecosystem nodes + const ecosystemNodes: Node[] = data.map((item: any) => ({ + id: String(item.stageid), + label: item.title, + category: item.category, + stageid: item.stageid, + imageUrl: getImageUrl(item.stageid), + rawData: item, + })); + + // Create links (all nodes connected to center) + const graphLinks: Link[] = ecosystemNodes.map((node) => ({ + source: "center", + target: node.id, + })); + + setNodes([centerNode, ...ecosystemNodes]); + setLinks(graphLinks); } catch (err: any) { - if (err.name === "AbortError") { - // ignore - } else { + if (err.name !== "AbortError") { console.error("Failed to fetch graph data:", err); + setError("Failed to load graph data"); setNodes([]); + setLinks([]); } } finally { if (!aborted) setIsLoading(false); @@ -70,224 +142,436 @@ export function NetworkGraph({ onNodeClick}: NetworkGraphProps) { aborted = true; controller.abort(); }; + }, [isMounted, token]); + + // Get image URL for a node + const getImageUrl = useCallback( + (stageid: number) => { + if (!token?.accessToken) return null; + return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`; + }, + [token?.accessToken], + ); + + // Import apiService for the onClick handler + const callAPI = useCallback(async (stage_id: number) => { + return await apiService.callInnovationProcess({ + get_values_workflow_function: { + stage_id: stage_id, + }, + }); }, []); - // compute unique categories - const categories = useMemo(() => { - const set = new Set(); - nodes.forEach((n) => set.add(n.category)); - return ["all", ...Array.from(set)]; - }, [nodes]); - - // ------------- Build graph + Sigma (client-only) ---------------- + // Initialize D3 graph useEffect(() => { - // don't run on server or before container available or while loading - if (typeof window === "undefined" || !containerRef.current || isLoading) return; + if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) { + return; + } - let renderer: any = null; - let Sigma: any = null; - let isCancelled = false; + const svg = d3.select(svgRef.current); + const width = svgRef.current.clientWidth; + const height = svgRef.current.clientHeight; - (async () => { - try { - // dynamic import for sigma only - const sigmaModule = await import("sigma"); - Sigma = sigmaModule.default || sigmaModule.Sigma || sigmaModule; + // Clear previous content + svg.selectAll("*").remove(); - if (isCancelled || !containerRef.current) return; + // Create defs for patterns and filters + const defs = svg.append("defs"); - const graph = new Graph(); - graphRef.current = graph; + // Add glow filter for hover effect + const filter = defs + .append("filter") + .attr("id", "glow") + .attr("x", "-50%") + .attr("y", "-50%") + .attr("width", "200%") + .attr("height", "200%"); - // color map (you can extend) - const categoryToColor: Record = { - دانشگاه: "#3B82F6", - مشاور: "#10B981", - "دانش بنیان": "#F59E0B", - استارتاپ: "#EF4444", - شرکت: "#8B5CF6", - صندوق: "#06B6D4", - شتابدهنده: "#9333EA", - "مرکز نوآوری": "#F472B6", - center: "#000000", - }; + filter + .append("feGaussianBlur") + .attr("stdDeviation", "3") + .attr("result", "coloredBlur"); - // add central node - const CENTER_ID = "center"; - graph.addNode(CENTER_ID, { - label: "مرکز نوآوری اصلی", - x: 0, - y: 0, - size: 20, - category: "center", - color: categoryToColor.center, - }); + const feMerge = filter.append("feMerge"); + feMerge.append("feMergeNode").attr("in", "coloredBlur"); + feMerge.append("feMergeNode").attr("in", "SourceGraphic"); - // add all nodes - nodes.forEach((node, i) => { - // Place nodes in a circle, but all nodes are always present - const len = Math.max(1, nodes.length); - const radius = Math.max(5, Math.min(20, Math.ceil(len / 2))); - const angleStep = (2 * Math.PI) / len; - const angle = i * angleStep; - const jitter = (Math.random() - 0.5) * 0.4; - const x = Math.cos(angle) * (radius + jitter); - const y = Math.sin(angle) * (radius + jitter); - graph.addNode(node.id, { - label: node.label, - x, - y, - size: 8, - category: node.category, - color: categoryToColor[node.category] || "#94A3B8", - payload: node, - }); - graph.addEdge(CENTER_ID, node.id, { size: 1, color: "#CBD5E1" }); - }); + // Create zoom behavior + const zoom = d3 + .zoom() + .scaleExtent([1, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x + .on("zoom", (event) => { + container.attr("transform", event.transform); + }); - // Highlight nodes by filter - const highlightByCategory = (category: string) => { - graph.forEachNode((n: string, attrs: any) => { - if (category === "all" || attrs.category === category) { - graph.setNodeAttribute(n, "color", categoryToColor[attrs.category] || "#94A3B8"); - graph.setNodeAttribute(n, "size", attrs.category === "center" ? 20 : 12); - graph.setNodeAttribute(n, "zIndex", 1); - graph.setNodeAttribute(n, "opacity", 1); - } else { - graph.setNodeAttribute(n, "color", "#888888"); - graph.setNodeAttribute(n, "size", 7); - graph.setNodeAttribute(n, "zIndex", 0); - graph.setNodeAttribute(n, "opacity", 0.3); - } - }); - }; - highlightByCategory(filterCategory); + svg.call(zoom); - // Listen for filterCategory changes to re-apply highlight - // (This is needed if filterCategory changes after initial render) - const filterListener = () => highlightByCategory(filterCategory); - // Optionally, you could use a custom event or observer if needed - // For now, we rely on the effect re-running due to filterCategory in deps + // Create container group + const container = svg.append("g"); - // create renderer - renderer = new Sigma(graph, containerRef.current, { - renderLabels: true, - defaultNodeColor: "#94A3B8", - defaultEdgeColor: "#CBD5E1", - labelColor: { color: "#fff" }, // Set label color to white - }); - - sigmaRef.current = renderer; - - // Helper: set highlight states by mutating node attributes - const setHighlight = (nodeId: string | null) => { - graph.forEachNode((n: string, attrs: any) => { - if (nodeId && (n === nodeId || graph.hasEdge(n, nodeId) || graph.hasEdge(nodeId, n))) { - graph.setNodeAttribute(n, "size", attrs.size ? Math.min(24, attrs.size * 1.6) : 12); - graph.setNodeAttribute(n, "color", attrs.color ? attrs.color : "#fff"); - graph.setNodeAttribute(n, "highlighted", true); - } else { - graph.setNodeAttribute(n, "size", attrs.size && attrs.category === "center" ? 20 : 8); - // restore original color if we stored it; otherwise keep - graph.setNodeAttribute(n, "color", attrs.category === "center" ? categoryToColor.center : attrs.color); - graph.setNodeAttribute(n, "highlighted", false); - } - }); - // ask renderer to refresh (sigma v2 triggers update automatically when graph changes) - }; - - // events: hover highlight and click select - const onEnter = (e: any) => { - const nodeId = e.node; - setHighlight(nodeId); - }; - const onLeave = () => { - setHighlight(selectedNodeId); // keep selected highlighted, or none - }; - const onClick = (e: any) => { - const nodeId = e.node as string; - setSelectedNodeId((prev) => (prev === nodeId ? null : nodeId)); - // call external callback with payload if exists - const attrs = graph.getNodeAttributes(nodeId); - onNodeClick?.({ id: nodeId, label: attrs?.label, ...(attrs?.payload ?? {}) }); - }; - - renderer.on("enterNode", onEnter); - renderer.on("leaveNode", onLeave); - renderer.on("clickNode", onClick); - - // if there is a pre-selected node (state), reflect it - if (selectedNodeId) setHighlight(selectedNodeId); - - // cleanup on re-run - return () => { - try { - renderer.removeListener("enterNode", onEnter); - renderer.removeListener("leaveNode", onLeave); - renderer.removeListener("clickNode", onClick); - } catch {} - }; - } catch (err) { - console.error("Failed to initialize graph / sigma:", err); - } - })(); - - return () => { - isCancelled = true; - // kill previous renderer & graph - if (sigmaRef.current) { - try { - sigmaRef.current.kill?.(); - } catch {} - } - sigmaRef.current = null; - graphRef.current = null; - if (renderer) { - try { - renderer.kill?.(); - } catch {} - } - renderer = null; + // Category colors + const categoryToColor: Record = { + دانشگاه: "#3B82F6", + مشاور: "#10B981", + "دانش بنیان": "#F59E0B", + استارتاپ: "#EF4444", + شرکت: "#8B5CF6", + صندوق: "#06B6D4", + شتابدهنده: "#9333EA", + "مرکز نوآوری": "#F472B6", + center: "#34D399", }; - // rebuild whenever nodes, filterCategory or selectedNodeId changes - }, [nodes, filterCategory, isLoading, onNodeClick, selectedNodeId]); - return ( -
-
- - -
- {isLoading ? "در حال بارگذاری..." : `نمایش ${filterCategory === "all" ? nodes.length : nodes.filter(n => n.category === filterCategory).length} گره`} + // Create force simulation + const simulation = d3 + .forceSimulation(nodes) + .force( + "link", + d3 + .forceLink(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() + .on("start", (event, d) => { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }) + .on("drag", (event, d) => { + d.fx = event.x; + d.fy = event.y; + }) + .on("end", (event, d) => { + if (!event.active) simulation.alphaTarget(0); + if (!d.isCenter) { + d.fx = null; + d.fy = null; + } + }); + + nodeGroup.call(drag); + + // Add node circles/rectangles + nodeGroup.each(function (d) { + const group = d3.select(this); + + if (d.isCenter) { + // Center node as rectangle + const rect = group + .append("rect") + .attr("width", 150) + .attr("height", 60) + .attr("x", -75) + .attr("y", -30) + .attr("rx", 8) + .attr("ry", 8) + .attr("fill", categoryToColor[d.category] || "#94A3B8") + .attr("stroke", "#FFFFFF") + .attr("stroke-width", 3); + + // Add center image if available + if (d.imageUrl || d.isCenter) { + const pattern = defs + .append("pattern") + .attr("id", `image-${d.id}`) + .attr("x", 0) + .attr("y", 0) + .attr("width", 1) + .attr("height", 1); + + pattern + .append("image") + .attr("x", 0) + .attr("y", 0) + .attr("width", 150) + .attr("height", 60) + .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl) + .attr("preserveAspectRatio", "xMidYMid slice"); + + rect.attr("fill", `url(#image-${d.id})`); + } + } else { + // Regular nodes as circles + const circle = group + .append("circle") + .attr("r", 25) + .attr("fill", categoryToColor[d.category] || "8#fff") + .attr("stroke", "#FFFFFF") + .attr("stroke-width", 3); + + // Add node image if available + if (d.imageUrl) { + const pattern = defs + .append("pattern") + .attr("id", `image-${d.id}`) + .attr("x", 0) + .attr("y", 0) + .attr("width", 1) + .attr("height", 1); + + pattern + .append("image") + .attr("x", 0) + .attr("y", 0) + .attr("width", 50) + .attr("height", 50) + .attr("href", d.imageUrl) + .attr("backgroundColor", "#fff") + .attr("preserveAspectRatio", "xMidYMid slice"); + + // Create circular clip path + defs + .append("clipPath") + .attr("id", `clip-${d.id}`) + .append("circle") + .attr("r", 25); + + circle + .attr("fill", `url(#image-${d.id})`) + .attr("clip-path", `url(#clip-${d.id})`); + } + } + }); + + // Add labels below nodes + const labels = nodeGroup + .append("text") + .text((d) => d.label) + .attr("text-anchor", "middle") + .attr("dy", (d) => (d.isCenter ? 50 : 45)) + .attr("font-size", (d) => (d.isCenter ? "14px" : "12px")) + .attr("font-weight", "bold") + .attr("fill", "#F9FAFB") + .attr("stroke", "rgba(17, 24, 39, 0.95)") + .attr("stroke-width", 4) + .attr("paint-order", "stroke"); + + // Add hover effects + nodeGroup + .on("mouseenter", function (event, d) { + d3.select(this) + .select(d.isCenter ? "rect" : "circle") + .attr("filter", "url(#glow)") + .transition() + .duration(200) + .attr("stroke", "#3B82F6") + .attr("stroke-width", 4); + }) + .on("mouseleave", function (event, d) { + d3.select(this) + .select(d.isCenter ? "rect" : "circle") + .attr("filter", null) + .transition() + .duration(200) + .attr("stroke", "#FFFFFF") + .attr("stroke-width", 3); + }); + + // Add click handlers + nodeGroup.on("click", async function (event, d) { + event.stopPropagation(); + + // Don't handle center node clicks + if (d.isCenter) return; + + if (onNodeClick && d.stageid) { + try { + // Fetch detailed company data + const res = await callAPI(d.stageid); + + const responseData = JSON.parse(res.data); + const fieldValues = + JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || []; + + // Filter out image fields and find description + const filteredFields = fieldValues.filter( + (field: any) => + ![ + "image", + "img", + "des", + "dec", + "description", + "collaboration", + ].includes(field.F.toLowerCase()), + ); + + const descriptionField = fieldValues.find( + (field: any) => + field.F.toLowerCase().includes("description") || + field.F.toLowerCase().includes("about"), + ); + + const companyDetails: CompanyDetails = { + id: d.id, + label: d.label, + category: d.category, + stageid: d.stageid, + fields: filteredFields, + description: descriptionField?.V || undefined, + }; + + onNodeClick(companyDetails); + } catch (error) { + console.error("Failed to fetch company details:", error); + // Fallback to basic info + const basicDetails: CompanyDetails = { + id: d.id, + label: d.label, + category: d.category, + stageid: d.stageid, + fields: [], + }; + onNodeClick(basicDetails); + } + } + }); + + // Update positions on simulation tick + simulation.on("tick", () => { + link + .attr("x1", (d) => (d.source as Node).x!) + .attr("y1", (d) => (d.source as Node).y!) + .attr("x2", (d) => (d.target as Node).x!) + .attr("y2", (d) => (d.target as Node).y!); + + nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`); + }); + + // Cleanup function + return () => { + simulation.stop(); + }; + }, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]); + + // Show error message + if (error) { + return ( +
+
+
+ ⚠️ خطای بارگذاری +
+
{error}
+ ); + } -
- - {/* overlay selected info */} -
- {selectedNodeId ? ( - <> -
انتخاب شده: {selectedNodeId}
- - - ) : null} + // Don't render on server side + if (!isMounted) { + return ( +
+
+ در حال بارگذاری... +
+ ); + } + + if (isLoading) { + return ( +
+ {/* Skeleton Graph Container */} +
+ {/* Center Node Skeleton */} +
+
+
+ + {/* 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 ( +
+
+
+
+
+ ); + })} +
+ +
+
+ در حال بارگذاری نمودار... +
+
+
+ ); + } + + return ( +
+
); } diff --git a/app/components/ui/custom-bar-chart.tsx b/app/components/ui/custom-bar-chart.tsx index 86b8ad2..8e3ce34 100644 --- a/app/components/ui/custom-bar-chart.tsx +++ b/app/components/ui/custom-bar-chart.tsx @@ -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 = { - "0": "۰", "1": "۱", "2": "۲", "3": "۳", "4": "۴", - "5": "۵", "6": "۶", "7": "۷", "8": "۸", "9": "۹" - }; - return str.replace(/[0-9]/g, (d) => map[d] ?? d); - }; + const globalMaxValue = Math.max( + ...data.map((item) => item.maxValue || item.value), + ); // Loading skeleton if (loading) { @@ -49,18 +42,21 @@ export function CustomBarChart({ {title && (
)} - +
{Array.from({ length: 4 }).map((_, index) => (
{/* Label skeleton */}
- + {/* Bar skeleton */}
-
+
- + {/* Value skeleton */}
@@ -77,63 +73,83 @@ export function CustomBarChart({ {title} )} - +
{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 (
{/* Label */} - {item.label} - + {/* Bar Container */} -
-
+
{/* Add a subtle gradient effect for better visual appeal */}
- + {/* Value Label */} - - {item.valuePrefix || ''}{formatNumber(parseFloat(displayValue))}{item.valueSuffix || '%'} + + {item.valuePrefix || ""} + {formatNumber(parseFloat(displayValue))} + {item.valueSuffix || ""}
); })} - + {/* Axis Labels */} {showAxisLabels && globalMaxValue > 0 && (
- {formatNumber(0)}% - {formatNumber(Math.round(globalMaxValue / 4))}% - {formatNumber(Math.round(globalMaxValue / 2))}% - {formatNumber(Math.round((globalMaxValue * 3) / 4))}% - {formatNumber(Math.round(globalMaxValue))}% + {formatNumber(0)} + + {formatNumber(Math.round(globalMaxValue / 4))} + + + {formatNumber(Math.round(globalMaxValue / 2))} + + + {formatNumber(Math.round((globalMaxValue * 3) / 4))} + + + {formatNumber(Math.round(globalMaxValue))} +
@@ -141,4 +157,4 @@ export function CustomBarChart({
); -} \ No newline at end of file +} diff --git a/app/lib/utils.ts b/app/lib/utils.ts index bd0c391..b771a69 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -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); +}; diff --git a/app/routes/ecosystem.tsx b/app/routes/ecosystem.tsx index f5ce053..c78e9a9 100644 --- a/app/routes/ecosystem.tsx +++ b/app/routes/ecosystem.tsx @@ -3,67 +3,190 @@ import React from "react"; import { ProtectedRoute } from "~/components/auth/protected-route"; import { DashboardLayout } from "~/components/dashboard/layout"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; import { NetworkGraph } from "~/components/ecosystem/network-graph"; import { InfoPanel } from "~/components/ecosystem/info-panel"; -import { Checkbox } from "~/components/ui/checkbox"; +import { useAuth } from "~/contexts/auth-context"; +import moment from "moment-jalaali"; + +// Get API base URL at module level to avoid process.env access in browser +const API_BASE_URL = + import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api"; + +// Import the CompanyDetails type +import type { CompanyDetails } from "~/components/ecosystem/network-graph"; +import { formatNumber } from "~/lib/utils"; export function meta({}: Route.MetaArgs) { return [ { title: "زیست بوم فناوری" }, - { name: "description", content: "نمایش زیست بوم فناوری با گراف شبکه‌ای شرکت‌ها" }, + { + name: "description", + content: "نمایش زیست بوم فناوری با گراف شبکه‌ای شرکت‌ها", + }, ]; } -export default function EcosystemPage() { - const [selectedCompany, setSelectedCompany] = React.useState< - | { id: string; label?: string; [key: string]: unknown } - | null - >(null); - const [highlightUnpaid, setHighlightUnpaid] = React.useState(false); +moment.loadPersian({ usePersianDigits: true }); - const closeDialog = () => setSelectedCompany(null); +function handleValue(val: any): any { + if (val == null) return val; + if ( + typeof val === "string" && + /^\d{4}[-/]\d{2}[-/]\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(val) + ) { + return moment(val, "YYYY-MM-DD HH:mm:ss").format("YYYY/MM/DD"); + } + if ( + typeof val === "number" || + (typeof val === "string" && /^-?\d+$/.test(val)) + ) { + return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]); + } + return val; +} + +export default function EcosystemPage() { + const [selectedCompany, setSelectedCompany] = + React.useState(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 (
-
- {// - } +
-
- - - گراف شبکه‌ای شرکت‌ها - +
+ - +
-
{/* Node info dialog */} - !open && closeDialog()}> - + !open && closeDialog()} + > + - {selectedCompany?.label || "اطلاعات شرکت"} - - شناسه: {selectedCompany?.id} - + + معرفی شرکت + + -
-

Test

+ +
+ {/* Right Column - Description */} +
+ {/* Company Image */} +
+ {selectedCompany?.label || "اطلاعات شرکت"} + {selectedCompany?.stageid && token?.accessToken ? ( + {selectedCompany?.label { + // 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} +
+ + + +
+
+ {selectedCompany?.description ? ( +
+

+ {selectedCompany.description} +

+
+ ) : ( +
+ توضیحات در دسترس نیست +
+ )} +
+ {/* Left Column - Company Fields */} +
+

اطلاعات شرکت

+ {selectedCompany?.fields && + selectedCompany.fields.length > 0 ? ( +
+ {selectedCompany.fields.map((field, index) => ( +
+ + {field.N}: + + + {handleValue(field.V)} + {field.U && ({field.U})} + +
+ ))} +
+ ) : ( +
+ اطلاعات تکمیلی در دسترس نیست +
+ )} +
); -} \ No newline at end of file +} diff --git a/package.json b/package.json index 3694160..33415d2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b42871..6c72c9e 100644 --- a/pnpm-lock.yaml +++ b/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: {} diff --git a/public/main-circle.png b/public/main-circle.png new file mode 100644 index 0000000..27e63e6 Binary files /dev/null and b/public/main-circle.png differ