import { useState, useEffect, useRef } from "react"; import { Box, Grid, GridItem, Text, Flex, Spinner, SimpleGrid, RangeSlider, RangeSliderTrack, RangeSliderFilledTrack, RangeSliderThumb, } from "@chakra-ui/react"; import Select from "react-select"; import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Title, Tooltip, Legend, ArcElement, LineElement, PointElement, RadialLinearScale, type ChartData, type ChartOptions } from "chart.js"; import { Bar, Pie, Doughnut, Line } from "react-chartjs-2"; import type { FilterState, AvailableFilters } from "../hooks/types"; // register chart components ChartJS.register( BarElement, CategoryScale, LinearScale, Title, Tooltip, Legend, ArcElement, LineElement, PointElement, RadialLinearScale ); type LegendPosition = "top" | "bottom" | "left" | "right" | "chartArea"; interface ChartDataShape { labels: string[]; values: number[]; } interface Stats { [key: string]: ChartDataShape; } // define the six charts and their component types const chartOrder: { key:string; name: string; type: ChartType }[] = [ { key: "ppy", name: "Projects per Year", type: "line" }, { key: "psd",name: "Project-Size Distribution", type: "bar" }, { key: "frs",name: "Top 10 Funding Schemes", type: "bar" }, { key: "top10",name: "Top 10 Topics (€ M)", type: "bar" }, { key: "frb",name: "Funding Range Breakdown", type: "pie" }, { key: "ppc",name: "Projects per Country", type: "doughnut" }, ]; const FILTER_LABELS: Record = { status: "Status", organization: "Organization", country: "Country", legalBasis: "Legal Basis", topic: "EuroSciVoc", fundingScheme: "Funding Scheme", }; type ChartType = "bar" | "pie" | "doughnut" | "line"; interface DashboardProps { stats: Stats; filters: FilterState; setFilters: React.Dispatch>; availableFilters: AvailableFilters; } const Dashboard: React.FC = ({ stats: initialStats, filters, setFilters, availableFilters, }) => { const [orgInput, setOrgInput] = useState(""); const [statsData, setStatsData] = useState(initialStats); const [loadingStats, setLoadingStats] = useState(false); const fetchTimer = useRef(null); // Debounced stats & filters fetch useEffect(() => { if (fetchTimer.current) clearTimeout(fetchTimer.current); fetchTimer.current = window.setTimeout(() => { const qs = new URLSearchParams(); Object.entries(filters).forEach(([k, v]) => v && qs.set(k, v)); setLoadingStats(true); fetch(`/api/stats?${qs.toString()}`) .then(r => r.json()) .then((data: Stats) => setStatsData(data)) .catch(console.error) .finally(() => setLoadingStats(false)); }, 300); return () => { if (fetchTimer.current) clearTimeout(fetchTimer.current); }; }, [filters]); const updateFilter = (key: keyof FilterState) => (opt: { value: string } | null) => setFilters(prev => ({ ...prev, [key]: opt?.value || "" })); const updateSlider = ( k1: 'minYear' | 'minFunding' | 'minEndYear', k2: 'maxYear' | 'maxFunding' | 'maxEndYear' ) => ([min, max]: number[]) => setFilters(prev => ({ ...prev, [k1]: String(min), [k2]: String(max), })); const filterKeys: Array = [ 'status', 'organization', 'country', 'legalBasis','topic','fundingScheme', ]; if (loadingStats && !Object.keys(statsData).length) { return ; } return ( {/* Filters */} {filterKeys.map(key => { const opts = availableFilters[ key === 'status' ? 'statuses' : key === 'organization' ? 'organizations' : key === 'country' ? 'countries' : key === "legalBasis" ? "legalBases" : key === "topic" ? "topics" : 'fundingSchemes' ] || []; const isOrg = key === 'organization'; return ( {FILTER_LABELS[key]}