Rom89823974978 commited on
Commit
73ce5b8
·
1 Parent(s): 40e9eac

Adjusted backend and frontend for new plots

Browse files
backend/main.py CHANGED
@@ -52,13 +52,13 @@ class Settings(SettingsBase):
52
  Configuration settings loaded from environment or .env file.
53
  """
54
  # Data sources
55
- parquet_path: str = "gs://mda_eu_project/data/consolidated_clean_pred.parquet"
56
- whoosh_dir: str = "gs://mda_eu_project/whoosh_index"
57
- vectorstore_path: str = "gs://mda_eu_project/vectorstore_index"
58
 
59
  # Model names
60
  embedding_model: str = "sentence-transformers/LaBSE"
61
- llm_model: str = "google/flan-t5-base"
62
  cross_encoder_model: str = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
63
 
64
  # RAG parameters
@@ -305,6 +305,7 @@ def get_projects(
305
  country: str = "",
306
  fundingScheme: str = "",
307
  proj_id: str = "",
 
308
  sortOrder: str = "desc",
309
  sortField: str = "startDate",
310
  ):
@@ -317,6 +318,7 @@ def get_projects(
317
  - search: substring search in project title
318
  - status, legalBasis, organization, country, fundingScheme: filters
319
  - proj_id: exact project ID filter
 
320
  - sortOrder: 'asc' or 'desc'
321
  - sortField: field name to sort by (fallback to startDate)
322
 
@@ -344,6 +346,8 @@ def get_projects(
344
  sel = sel.filter(pl.col("_fundingScheme_lc").str.contains(fundingScheme.lower()))
345
  if proj_id:
346
  sel = sel.filter(pl.col("id") == int(proj_id))
 
 
347
 
348
  # Base columns to return
349
  base_cols = [
@@ -410,6 +414,8 @@ def get_filters(request: Request):
410
  df = df.filter(pl.col("list_name").list.contains(org))
411
  if c := params.get("country"):
412
  df = df.filter(pl.col("list_country").list.contains(c))
 
 
413
  if search := params.get("search"):
414
  df = df.filter(pl.col("_title_lc").str.contains(search.lower()))
415
 
@@ -420,57 +426,175 @@ def get_filters(request: Request):
420
  return {
421
  "statuses": normalize(df["status"].to_list()),
422
  "legalBases": normalize(df["legalBasis"].to_list()),
423
- "organizations": normalize(df["list_name"].explode().to_list())[:500],
424
  "countries": normalize(df["list_country"].explode().to_list()),
425
  "fundingSchemes": normalize(df["fundingScheme"].explode().to_list()),
 
426
  }
427
 
428
  @app.get("/api/stats")
429
  def get_stats(request: Request):
430
  """
431
- Compute annual statistics on projects with optional filters for status, legal basis, etc.
432
-
433
- Returns a dict of chart data for projects per year.
434
  """
435
- lf = app.state.df.lazy()
 
436
  params = request.query_params
437
 
438
- # Apply lazy filters
439
  if s := params.get("status"):
440
  lf = lf.filter(pl.col("_status_lc") == s.lower())
 
441
  if lb := params.get("legalBasis"):
442
  lf = lf.filter(pl.col("_legalBasis_lc") == lb.lower())
 
443
  if org := params.get("organization"):
444
  lf = lf.filter(pl.col("list_name").list.contains(org))
 
445
  if c := params.get("country"):
446
  lf = lf.filter(pl.col("list_country").list.contains(c))
 
447
  if mn := params.get("minFunding"):
448
  lf = lf.filter(pl.col("ecMaxContribution") >= int(mn))
 
449
  if mx := params.get("maxFunding"):
450
  lf = lf.filter(pl.col("ecMaxContribution") <= int(mx))
 
451
  if y1 := params.get("minYear"):
452
  lf = lf.filter(pl.col("startDate").dt.year() >= int(y1))
 
453
  if y2 := params.get("maxYear"):
454
  lf = lf.filter(pl.col("startDate").dt.year() <= int(y2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
- # Group by year and count
457
- grouped = (
458
- lf.select(pl.col("startDate").dt.year().alias("year"))
459
- .group_by("year")
460
- .agg(pl.count().alias("count"))
461
- .sort("year")
462
- .collect()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  )
464
- years, counts = grouped["year"].to_list(), grouped["count"].to_list()
 
465
 
466
- # Return data ready for frontend charts
467
  return {
468
- "Projects per Year": {"labels": years, "values": counts},
469
- "Projects per Year 2": {"labels": years, "values": counts},
470
- "Projects per Year 3": {"labels": years, "values": counts},
471
- "Projects per Year 4": {"labels": years, "values": counts},
472
- "Projects per Year 5": {"labels": years, "values": counts},
473
- "Projects per Year 6": {"labels": years, "values": counts},
474
  }
475
 
476
  @app.get("/api/project/{project_id}/organizations")
 
52
  Configuration settings loaded from environment or .env file.
53
  """
54
  # Data sources
55
+ parquet_path: str = "gs://mda_kul_project/data/consolidated_clean_pred.parquet"
56
+ whoosh_dir: str = "gs://mda_kul_project/whoosh_index"
57
+ vectorstore_path: str = "gs://mda_kul_project/vectorstore_index"
58
 
59
  # Model names
60
  embedding_model: str = "sentence-transformers/LaBSE"
61
+ llm_model: str = "google/mt5-base"
62
  cross_encoder_model: str = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
63
 
64
  # RAG parameters
 
305
  country: str = "",
306
  fundingScheme: str = "",
307
  proj_id: str = "",
308
+ topic: str = "",
309
  sortOrder: str = "desc",
310
  sortField: str = "startDate",
311
  ):
 
318
  - search: substring search in project title
319
  - status, legalBasis, organization, country, fundingScheme: filters
320
  - proj_id: exact project ID filter
321
+ - topic: filter by EuroSciVoc topic
322
  - sortOrder: 'asc' or 'desc'
323
  - sortField: field name to sort by (fallback to startDate)
324
 
 
346
  sel = sel.filter(pl.col("_fundingScheme_lc").str.contains(fundingScheme.lower()))
347
  if proj_id:
348
  sel = sel.filter(pl.col("id") == int(proj_id))
349
+ if topic:
350
+ sel = sel.filter(pl.col("list_euroSciVocTitle").list.contains(topic))
351
 
352
  # Base columns to return
353
  base_cols = [
 
414
  df = df.filter(pl.col("list_name").list.contains(org))
415
  if c := params.get("country"):
416
  df = df.filter(pl.col("list_country").list.contains(c))
417
+ if t := params.get("topics"):
418
+ df = df.filter(pl.col("list_euroSciVocTitle").list.contains(t))
419
  if search := params.get("search"):
420
  df = df.filter(pl.col("_title_lc").str.contains(search.lower()))
421
 
 
426
  return {
427
  "statuses": normalize(df["status"].to_list()),
428
  "legalBases": normalize(df["legalBasis"].to_list()),
429
+ "organizations": normalize(df["list_name"].explode().to_list()),
430
  "countries": normalize(df["list_country"].explode().to_list()),
431
  "fundingSchemes": normalize(df["fundingScheme"].explode().to_list()),
432
+ "topics": normalize(df["list_euroSciVocTitle"].explode().to_list()),
433
  }
434
 
435
  @app.get("/api/stats")
436
  def get_stats(request: Request):
437
  """
438
+ Compute various statistics on projects with optional filters for status, legal basis, funding, etc.
439
+ Returns a dict of chart data.
 
440
  """
441
+ df = app.state.df
442
+ lf = df.lazy()
443
  params = request.query_params
444
 
445
+ # Apply filters
446
  if s := params.get("status"):
447
  lf = lf.filter(pl.col("_status_lc") == s.lower())
448
+ df = df.filter(pl.col("_status_lc") == s.lower())
449
  if lb := params.get("legalBasis"):
450
  lf = lf.filter(pl.col("_legalBasis_lc") == lb.lower())
451
+ df = df.filter(pl.col("_legalBasis_lc") == lb.lower())
452
  if org := params.get("organization"):
453
  lf = lf.filter(pl.col("list_name").list.contains(org))
454
+ df = df.filter(pl.col("list_name").list.contains(org))
455
  if c := params.get("country"):
456
  lf = lf.filter(pl.col("list_country").list.contains(c))
457
+ df = df.filter(pl.col("list_country").list.contains(c))
458
  if mn := params.get("minFunding"):
459
  lf = lf.filter(pl.col("ecMaxContribution") >= int(mn))
460
+ df = df.filter(pl.col("ecMaxContribution") >= int(mn))
461
  if mx := params.get("maxFunding"):
462
  lf = lf.filter(pl.col("ecMaxContribution") <= int(mx))
463
+ df = df.filter(pl.col("ecMaxContribution") <= int(mx))
464
  if y1 := params.get("minYear"):
465
  lf = lf.filter(pl.col("startDate").dt.year() >= int(y1))
466
+ df = df.filter(pl.col("startDate").dt.year() >= int(y1))
467
  if y2 := params.get("maxYear"):
468
  lf = lf.filter(pl.col("startDate").dt.year() <= int(y2))
469
+ df = df.filter(pl.col("startDate").dt.year() <= int(y2))
470
+
471
+ # 1) Projects per Year (Line)
472
+ yearly = (
473
+ lf
474
+ .select(pl.col("startDate").dt.year().alias("year"))
475
+ .group_by("year")
476
+ .agg(pl.count().alias("count"))
477
+ .sort("year")
478
+ .collect()
479
+ )
480
+ years = yearly["year"].to_list()
481
+ year_counts = yearly["count"].to_list()
482
+
483
+ # 2) Project-Size Distribution by totalCost buckets (Bar)
484
+ size_buckets = (
485
+ df
486
+ .with_columns(
487
+ pl.when(pl.col("totalCost") < 100_000).then("<100 K")
488
+ .when(pl.col("totalCost") < 500_000).then("100 K–500 K")
489
+ .when(pl.col("totalCost") < 1_000_000).then("500 K–1 M")
490
+ .when(pl.col("totalCost") < 5_000_000).then("1 M–5 M")
491
+ .when(pl.col("totalCost") < 10_000_000).then("5 M–10 M")
492
+ .otherwise("≥10 M")
493
+ .alias("size_range")
494
+ )
495
+ .group_by("size_range")
496
+ .agg(pl.count().alias("count"))
497
+ # ensure our custom order
498
+ .with_columns(
499
+ pl.col("size_range").apply(
500
+ lambda x: ["<100 K","100 K–500 K","500 K–1 M","1 M–5 M","5 M–10 M","≥10 M"].index(x)
501
+ ).alias("order")
502
+ )
503
+ .sort("order")
504
+ .collect()
505
+ )
506
+ size_labels = size_buckets["size_range"].to_list()
507
+ size_counts = size_buckets["count"].to_list()
508
+
509
+ # 3) EU Co-funding Ratio by Scheme (Bar)
510
+ # a) First, filter out bad records and compute ratio:
511
+ clean = (
512
+ df
513
+ # drop if either value is null or totalCost is zero
514
+ .filter(
515
+ pl.col("ecMaxContribution").is_not_null() &
516
+ pl.col("totalCost").is_not_null() &
517
+ (pl.col("totalCost") != 0)
518
+ )
519
+ # compute ecMaxContributionRatio as Float64
520
+ .with_columns(
521
+ (
522
+ pl.col("ecMaxContribution").cast(pl.Float64)
523
+ / pl.col("totalCost").cast(pl.Float64)
524
+ ).alias("ecMaxContributionRatio")
525
+ )
526
+ )
527
+
528
+ # b) Explode by scheme and aggregate the mean of ratio
529
+ ratio = (
530
+ clean
531
+ .explode("fundingScheme")
532
+ .group_by("fundingScheme")
533
+ .agg(
534
+ pl.col("ecMaxContributionRatio").mean().alias("avg_ratio")
535
+ )
536
+ .sort("avg_ratio", descending=True)
537
+ .head(10)
538
+ .collect()
539
+ )
540
 
541
+ scheme_labels = ratio["fundingScheme"].to_list()
542
+ scheme_values = (ratio["avg_ratio"] * 100).round(1).to_list() # now in percentage
543
+
544
+
545
+ # 4) Top 10 Macro Topics by EC Contribution (Bar)
546
+ top_topics = (
547
+ df
548
+ .explode("list_euroSciVocTitle")
549
+ .group_by("list_euroSciVocTitle")
550
+ .agg(pl.col("ecMaxContribution").sum().alias("total_ec"))
551
+ .sort("total_ec", descending=True)
552
+ .head(10)
553
+ .collect()
554
+ )
555
+ topic_labels = top_topics["list_euroSciVocTitle"].to_list()
556
+ topic_values = (top_topics["total_ec"] / 1e6).round(1).to_list()
557
+
558
+ # 5) Projects by Funding Range (Pie)
559
+ fund_range = (
560
+ df
561
+ .with_columns(
562
+ pl.when(pl.col("ecMaxContribution") < 100_000).then("<100 K")
563
+ .when(pl.col("ecMaxContribution") < 500_000).then("100 K–500 K")
564
+ .when(pl.col("ecMaxContribution") < 1_000_000).then("500 K–1 M")
565
+ .when(pl.col("ecMaxContribution") < 5_000_000).then("1 M–5 M")
566
+ .when(pl.col("ecMaxContribution") < 10_000_000).then("5 M–10 M")
567
+ .otherwise("≥10 M")
568
+ .alias("funding_range")
569
+ )
570
+ .group_by("funding_range")
571
+ .agg(pl.count().alias("count"))
572
+ .sort("funding_range")
573
+ .collect()
574
+ )
575
+ fr_labels = fund_range["funding_range"].to_list()
576
+ fr_counts = fund_range["count"].to_list()
577
+
578
+ # 6) Projects per Country (Doughnut)
579
+ country = (
580
+ df
581
+ .explode("list_country")
582
+ .group_by("list_country")
583
+ .agg(pl.count().alias("count"))
584
+ .sort("count", descending=True)
585
+ .head(10)
586
+ .collect()
587
  )
588
+ country_labels = country["list_country"].to_list()
589
+ country_counts = country["count"].to_list()
590
 
 
591
  return {
592
+ "Projects per Year": {"labels": years, "values": year_counts},
593
+ "Project-Size Distribution": {"labels": size_labels, "values": size_counts},
594
+ "Co-funding Ratio by Scheme": {"labels": scheme_labels, "values": scheme_values},
595
+ "Top 10 Topics (€ M)": {"labels": topic_labels, "values": topic_values},
596
+ "Funding Range Breakdown": {"labels": fr_labels, "values": fr_counts},
597
+ "Projects per Country": {"labels": country_labels, "values": country_counts},
598
  }
599
 
600
  @app.get("/api/project/{project_id}/organizations")
backend/requirements.txt CHANGED
@@ -15,9 +15,9 @@ google-cloud-storage==2.11.0
15
 
16
  # RAG & embeddings
17
  pytorch-lightning==2.5.1
18
- langchain
19
- langchain-huggingface
20
- sentence-transformers
21
  langchain-community==0.3.24
22
 
23
  # Vector store
@@ -27,31 +27,31 @@ faiss-cpu==1.11.0
27
  whoosh==2.7.4
28
 
29
  # Transformers
30
- torch>=2.6.0
31
  blobfile>=1.5.0
32
  tiktoken>=0.4.0
33
  sentencepiece==0.2.0
34
- transformers
35
- torchvision
36
- sympy>=1.13.1
37
- peft
38
 
39
  aiofiles==24.1.0
40
  optimum==1.25.3
41
  bitsandbytes==0.45.5
42
- hf_xet
43
- HuggingFace
44
- huggingface_hub
45
- huggingface_hub[hf_xet]
46
 
47
  # ————————————————————————————————————————————————
48
-
49
- joblib
50
- shap
51
- matplotlib
52
- scipy
53
- scikit-learn
54
- imbalanced-learn
55
- xgboost
56
- evidently
57
- optuna
 
15
 
16
  # RAG & embeddings
17
  pytorch-lightning==2.5.1
18
+ langchain==0.3.25
19
+ langchain-huggingface==0.2.0
20
+ sentence-transformers==4.1.0
21
  langchain-community==0.3.24
22
 
23
  # Vector store
 
27
  whoosh==2.7.4
28
 
29
  # Transformers
30
+ torch==2.7.0
31
  blobfile>=1.5.0
32
  tiktoken>=0.4.0
33
  sentencepiece==0.2.0
34
+ transformers==4.62.3
35
+ torchvision==0.22.0
36
+ sympy==1.14.0
37
+ peft==0.15.2
38
 
39
  aiofiles==24.1.0
40
  optimum==1.25.3
41
  bitsandbytes==0.45.5
42
+ hf_xet==1.1.2
43
+ HuggingFace==0.0.1
44
+ huggingface_hub==0.32.1
45
+ huggingface_hub[hf_xet]==0.32.1
46
 
47
  # ————————————————————————————————————————————————
48
+ # Predictive modeling
49
+ joblib==1.5.1
50
+ shap==0.47.2
51
+ matplotlib==3.10.3
52
+ scipy==1.15.3
53
+ scikit-learn==1.6.1
54
+ imbalanced-learn==0.13.0
55
+ xgboost==3.0.2
56
+ evidently==0.7.6
57
+ optuna==4.3.0
frontend/src/components/Dashboard.tsx CHANGED
@@ -25,8 +25,10 @@ import {
25
  LineElement,
26
  PointElement,
27
  RadialLinearScale,
 
 
28
  } from "chart.js";
29
- import { Bar, Pie, Doughnut, Line, Radar, PolarArea } from "react-chartjs-2";
30
  import type { FilterState, AvailableFilters } from "../hooks/types";
31
 
32
  // register chart components
@@ -43,16 +45,30 @@ ChartJS.register(
43
  RadialLinearScale
44
  );
45
 
46
- interface ChartData { labels: string[]; values: number[]; }
47
- interface Stats { [key: string]: ChartData; }
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  const FILTER_LABELS: Record<keyof FilterState, string> = {
50
  status: "Status",
51
  organization: "Organization",
52
  country: "Country",
53
  legalBasis: "Legal Basis",
 
54
  };
55
 
 
 
56
  interface DashboardProps {
57
  stats: Stats;
58
  filters: FilterState;
@@ -60,9 +76,6 @@ interface DashboardProps {
60
  availableFilters: AvailableFilters;
61
  }
62
 
63
- const chartTypes = ["bar","pie","doughnut","line","radar","polarArea"] as const;
64
- type ChartType = typeof chartTypes[number];
65
-
66
  const Dashboard: React.FC<DashboardProps> = ({
67
  stats: initialStats,
68
  filters,
@@ -104,7 +117,7 @@ const Dashboard: React.FC<DashboardProps> = ({
104
  setFilters(prev => ({ ...prev, [k1]: String(min), [k2]: String(max) }));
105
 
106
  const filterKeys: Array<keyof FilterState> = [
107
- 'status', 'organization', 'country', 'legalBasis'
108
  ];
109
 
110
  if (loadingStats && !Object.keys(statsData).length) {
@@ -121,7 +134,8 @@ const Dashboard: React.FC<DashboardProps> = ({
121
  key === 'status' ? 'statuses'
122
  : key === 'organization' ? 'organizations'
123
  : key === 'country' ? 'countries'
124
- : 'legalBases'
 
125
  ] || [];
126
  const isOrg = key === 'organization';
127
  return (
@@ -201,23 +215,135 @@ const Dashboard: React.FC<DashboardProps> = ({
201
  </Flex>
202
  )}
203
  <SimpleGrid columns={{ base:1, md:2, lg:3 }} spacing={6}>
204
- {Object.entries(statsData).map(([label, data], idx) => {
205
- const type = chartTypes[idx % chartTypes.length] as ChartType;
206
- const chartProps = {
207
- data: { labels: data.labels, datasets: [{ label, data: data.values, backgroundColor: "#003399", borderColor: "#FFCC00", borderWidth: 1 }] },
208
- options: { responsive: true, plugins: { legend: { position: "top" as const }, title: { display: true, text: label } } }
209
- };
210
-
211
- return (
212
- <Box key={label} bg="white" borderRadius="md" p={4}>
213
- {type === "bar" && <Bar {...chartProps} />}
214
- {type === "pie" && <Pie {...chartProps} />}
215
- {type === "doughnut" && <Doughnut {...chartProps} />}
216
- {type === "line" && <Line {...chartProps} />}
217
- {type === "radar" && <Radar {...chartProps} />}
218
- {type === "polarArea" && <PolarArea {...chartProps} />}
219
- </Box>
220
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  })}
222
  </SimpleGrid>
223
  </Box>
 
25
  LineElement,
26
  PointElement,
27
  RadialLinearScale,
28
+ type ChartData,
29
+ type ChartOptions
30
  } from "chart.js";
31
+ import { Bar, Pie, Doughnut, Line } from "react-chartjs-2";
32
  import type { FilterState, AvailableFilters } from "../hooks/types";
33
 
34
  // register chart components
 
45
  RadialLinearScale
46
  );
47
 
48
+ type LegendPosition = "top" | "bottom" | "left" | "right" | "chartArea";
49
+ interface ChartDataShape { labels: string[]; values: number[]; }
50
+ interface Stats { [key: string]: ChartDataShape; }
51
+
52
+ // define the six charts and their component types
53
+ const chartOrder: { key: string; type: ChartType }[] = [
54
+ { key: "Projects per Year", type: "line" },
55
+ { key: "Project-Size Distribution", type: "bar" },
56
+ { key: "Co-funding Ratio by Scheme", type: "bar" },
57
+ { key: "Top 10 Topics (€ M)", type: "bar" },
58
+ { key: "Funding Range Breakdown", type: "pie" },
59
+ { key: "Projects per Country", type: "doughnut" },
60
+ ];
61
 
62
  const FILTER_LABELS: Record<keyof FilterState, string> = {
63
  status: "Status",
64
  organization: "Organization",
65
  country: "Country",
66
  legalBasis: "Legal Basis",
67
+ topics: "Topics",
68
  };
69
 
70
+ type ChartType = "bar" | "pie" | "doughnut" | "line";
71
+
72
  interface DashboardProps {
73
  stats: Stats;
74
  filters: FilterState;
 
76
  availableFilters: AvailableFilters;
77
  }
78
 
 
 
 
79
  const Dashboard: React.FC<DashboardProps> = ({
80
  stats: initialStats,
81
  filters,
 
117
  setFilters(prev => ({ ...prev, [k1]: String(min), [k2]: String(max) }));
118
 
119
  const filterKeys: Array<keyof FilterState> = [
120
+ 'status', 'organization', 'country', 'legalBasis','topics'
121
  ];
122
 
123
  if (loadingStats && !Object.keys(statsData).length) {
 
134
  key === 'status' ? 'statuses'
135
  : key === 'organization' ? 'organizations'
136
  : key === 'country' ? 'countries'
137
+ : key === "legalBasis" ? "legalBases"
138
+ : 'topics'
139
  ] || [];
140
  const isOrg = key === 'organization';
141
  return (
 
215
  </Flex>
216
  )}
217
  <SimpleGrid columns={{ base:1, md:2, lg:3 }} spacing={6}>
218
+ {chartOrder.map(({ key, type }) => {
219
+ const raw = statsData[key]!;
220
+
221
+ // ---- properly typed Chart.js data & options ----
222
+ if (type === "bar") {
223
+ const data: ChartData<"bar", number[], string> = {
224
+ labels: raw.labels,
225
+ datasets: [
226
+ {
227
+ label: key,
228
+ data: raw.values,
229
+ backgroundColor: "#003399",
230
+ borderColor: "#FFCC00",
231
+ borderWidth: 1,
232
+ },
233
+ ],
234
+ };
235
+ const options: ChartOptions<"bar"> = {
236
+ responsive: true,
237
+ plugins: {
238
+ legend: {
239
+ position: "top" as LegendPosition,
240
+ },
241
+ title: {
242
+ display: true,
243
+ text: key,
244
+ },
245
+ },
246
+ };
247
+ return (
248
+ <Box key={key} bg="white" borderRadius="md" p={4}>
249
+ <Bar data={data} options={options} />
250
+ </Box>
251
+ );
252
+ }
253
+ if (type === "line") {
254
+ const data: ChartData<"line", number[], string> = {
255
+ labels: raw.labels,
256
+ datasets: [
257
+ {
258
+ label: key,
259
+ data: raw.values,
260
+ backgroundColor: "#003399",
261
+ borderColor: "#FFCC00",
262
+ borderWidth: 1,
263
+ },
264
+ ],
265
+ };
266
+ const options: ChartOptions<"line"> = {
267
+ responsive: true,
268
+ plugins: {
269
+ legend: {
270
+ position: "top" as LegendPosition,
271
+ },
272
+ title: {
273
+ display: true,
274
+ text: key,
275
+ },
276
+ },
277
+ };
278
+ return (
279
+ <Box key={key} bg="white" borderRadius="md" p={4}>
280
+ <Line data={data} options={options} />
281
+ </Box>
282
+ );
283
+ }
284
+ if (type === "pie") {
285
+ const data: ChartData<"pie", number[], string> = {
286
+ labels: raw.labels,
287
+ datasets: [
288
+ {
289
+ label: key,
290
+ data: raw.values,
291
+ backgroundColor: "#003399",
292
+ borderColor: "#FFCC00",
293
+ borderWidth: 1,
294
+ },
295
+ ],
296
+ };
297
+ const options: ChartOptions<"pie"> = {
298
+ responsive: true,
299
+ plugins: {
300
+ legend: {
301
+ position: "top" as LegendPosition,
302
+ },
303
+ title: {
304
+ display: true,
305
+ text: key,
306
+ },
307
+ },
308
+ };
309
+ return (
310
+ <Box key={key} bg="white" borderRadius="md" p={4}>
311
+ <Pie data={data} options={options} />
312
+ </Box>
313
+ );
314
+ }
315
+ if (type === "doughnut") {
316
+ const data: ChartData<"doughnut", number[], string> = {
317
+ labels: raw.labels,
318
+ datasets: [
319
+ {
320
+ label: key,
321
+ data: raw.values,
322
+ backgroundColor: "#003399",
323
+ borderColor: "#FFCC00",
324
+ borderWidth: 1,
325
+ },
326
+ ],
327
+ };
328
+ const options: ChartOptions<"doughnut"> = {
329
+ responsive: true,
330
+ plugins: {
331
+ legend: {
332
+ position: "top" as LegendPosition,
333
+ },
334
+ title: {
335
+ display: true,
336
+ text: key,
337
+ },
338
+ },
339
+ };
340
+ return (
341
+ <Box key={key} bg="white" borderRadius="md" p={4}>
342
+ <Doughnut data={data} options={options} />
343
+ </Box>
344
+ );
345
+ }
346
+ return null;
347
  })}
348
  </SimpleGrid>
349
  </Box>
frontend/src/components/ProjectExplorer.tsx CHANGED
@@ -27,6 +27,7 @@ interface FilterOptions {
27
  countries: string[];
28
  fundingSchemes: string[];
29
  ids: string[];
 
30
  }
31
  const MIN_SEARCH_LEN = 3;
32
 
@@ -51,6 +52,8 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
51
  setFundingSchemeFilter,
52
  idFilter,
53
  setIdFilter,
 
 
54
  setSortField,
55
  sortField,
56
  setSortOrder,
@@ -72,6 +75,7 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
72
  countries: [],
73
  fundingSchemes: [],
74
  ids: [],
 
75
  });
76
  const [loadingFilters, setLoadingFilters] = useState(false);
77
 
@@ -86,6 +90,7 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
86
  if (search.length >= MIN_SEARCH_LEN) params.set("search", search);
87
  if (idFilter.length >= MIN_SEARCH_LEN) params.set("proj_id", idFilter);
88
  if (fundingSchemeFilter) params.set("fundingScheme", fundingSchemeFilter);
 
89
  params.set("sortField", sortField);
90
  params.set("sortOrder", sortOrder);
91
 
@@ -102,6 +107,7 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
102
  search,
103
  idFilter,
104
  fundingSchemeFilter,
 
105
  sortField,
106
  sortOrder,
107
  ]);
@@ -189,6 +195,15 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
189
  >
190
  {filterOpts.fundingSchemes.map((c) => <option key={c} value={c}>{c}</option>)}
191
  </ChakraSelect>
 
 
 
 
 
 
 
 
 
192
  </Flex>
193
 
194
  <Box
 
27
  countries: string[];
28
  fundingSchemes: string[];
29
  ids: string[];
30
+ topics: string[];
31
  }
32
  const MIN_SEARCH_LEN = 3;
33
 
 
52
  setFundingSchemeFilter,
53
  idFilter,
54
  setIdFilter,
55
+ topicsFilter,
56
+ setTopicsFilter,
57
  setSortField,
58
  sortField,
59
  setSortOrder,
 
75
  countries: [],
76
  fundingSchemes: [],
77
  ids: [],
78
+ topics: [],
79
  });
80
  const [loadingFilters, setLoadingFilters] = useState(false);
81
 
 
90
  if (search.length >= MIN_SEARCH_LEN) params.set("search", search);
91
  if (idFilter.length >= MIN_SEARCH_LEN) params.set("proj_id", idFilter);
92
  if (fundingSchemeFilter) params.set("fundingScheme", fundingSchemeFilter);
93
+ if (topicsFilter) params.set("topics", topicsFilter);
94
  params.set("sortField", sortField);
95
  params.set("sortOrder", sortOrder);
96
 
 
107
  search,
108
  idFilter,
109
  fundingSchemeFilter,
110
+ topicsFilter,
111
  sortField,
112
  sortOrder,
113
  ]);
 
195
  >
196
  {filterOpts.fundingSchemes.map((c) => <option key={c} value={c}>{c}</option>)}
197
  </ChakraSelect>
198
+ <ChakraSelect
199
+ placeholder={loadingFilters ? "Loading..." : "Funding Scheme"}
200
+ value={topicsFilter}
201
+ onChange={(e) => { setTopicsFilter(e.target.value); setPage(0); }}
202
+ isDisabled={loadingFilters}
203
+ width="180px"
204
+ >
205
+ {filterOpts.topics.map((c) => <option key={c} value={c}>{c}</option>)}
206
+ </ChakraSelect>
207
  </Flex>
208
 
209
  <Box
frontend/src/hooks/types.ts CHANGED
@@ -22,11 +22,6 @@ export interface Project {
22
 
23
  export interface ProjectDetailsProps {
24
  project: Project;
25
- // question: string;
26
- // setQuestion: React.Dispatch<React.SetStateAction<string>>;
27
- // askChatbot: () => void;
28
- // chatHistory: ChatMessage[];
29
- // messagesEndRef: React.RefObject<HTMLDivElement>;
30
  }
31
 
32
 
@@ -57,6 +52,7 @@ export interface FilterState {
57
  organization: string;
58
  country: string;
59
  legalBasis: string;
 
60
  minYear: string;
61
  maxYear: string;
62
  minFunding: string;
@@ -77,6 +73,7 @@ export interface AvailableFilters {
77
  legalBases: string[];
78
  fundingSchemes: string[];
79
  ids: string[];
 
80
  }
81
 
82
  export interface ProjectExplorerProps {
@@ -95,6 +92,8 @@ export interface ProjectExplorerProps {
95
  setFundingSchemeFilter: (value: string) => void;
96
  idFilter: string;
97
  setIdFilter: (value: string) => void;
 
 
98
  setSortField: (field: string) => void;
99
  sortField: string;
100
  setSortOrder : (order: "asc" | "desc") => void;
 
22
 
23
  export interface ProjectDetailsProps {
24
  project: Project;
 
 
 
 
 
25
  }
26
 
27
 
 
52
  organization: string;
53
  country: string;
54
  legalBasis: string;
55
+ topics: string;
56
  minYear: string;
57
  maxYear: string;
58
  minFunding: string;
 
73
  legalBases: string[];
74
  fundingSchemes: string[];
75
  ids: string[];
76
+ topics: string[];
77
  }
78
 
79
  export interface ProjectExplorerProps {
 
92
  setFundingSchemeFilter: (value: string) => void;
93
  idFilter: string;
94
  setIdFilter: (value: string) => void;
95
+ topicsFilter: string;
96
+ setTopicsFilter: (value: string) => void;
97
  setSortField: (field: string) => void;
98
  sortField: string;
99
  setSortOrder : (order: "asc" | "desc") => void;
frontend/src/hooks/useAppState.ts CHANGED
@@ -24,6 +24,7 @@ export const useAppState = () => {
24
  const [countryFilter, setCountryFilter] = useState('');
25
  const [fundingSchemeFilter, setFundingSchemeFilter ] = useState('');
26
  const [idFilter, setIdFilter] = useState('');
 
27
  const [sortField, setSortField] = useState('');
28
  const [sortOrder, setSortOrder] = useState<SortOrder>("asc");
29
  const [filters, setFilters] = useState<FilterState>({
@@ -31,6 +32,7 @@ export const useAppState = () => {
31
  organization: "",
32
  country: "",
33
  legalBasis: "",
 
34
  minYear: "2000",
35
  maxYear: "2025",
36
  minFunding: "0",
@@ -46,13 +48,14 @@ export const useAppState = () => {
46
  countries: [],
47
  legalBases: [],
48
  fundingSchemes:[],
49
- ids:[]
 
50
  });
51
 
52
  const messagesEndRef = useRef<HTMLDivElement | null>(null);
53
 
54
  const fetchProjects = () => {
55
- fetch(`/api/projects?page=${page}&search=${encodeURIComponent(search)}&status=${statusFilter}&legalBasis=${legalFilter}&organization=${orgFilter}&country=${countryFilter}&fundingScheme=${fundingSchemeFilter}&proj_id=${idFilter}&sortField=${sortField}&sortOrder=${sortOrder}`)
56
  .then(res => res.json())
57
  .then((data: Project[]) => setProjects(data))
58
  .catch(console.error);
@@ -77,7 +80,8 @@ export const useAppState = () => {
77
  countries: data.countries,
78
  legalBases: data.legalBases,
79
  fundingSchemes: data.fundingSchemes,
80
- ids: []
 
81
  });
82
  });
83
  };
 
24
  const [countryFilter, setCountryFilter] = useState('');
25
  const [fundingSchemeFilter, setFundingSchemeFilter ] = useState('');
26
  const [idFilter, setIdFilter] = useState('');
27
+ const [topicsFilter, setTopicsFilter] = useState('');
28
  const [sortField, setSortField] = useState('');
29
  const [sortOrder, setSortOrder] = useState<SortOrder>("asc");
30
  const [filters, setFilters] = useState<FilterState>({
 
32
  organization: "",
33
  country: "",
34
  legalBasis: "",
35
+ topics: "",
36
  minYear: "2000",
37
  maxYear: "2025",
38
  minFunding: "0",
 
48
  countries: [],
49
  legalBases: [],
50
  fundingSchemes:[],
51
+ ids:[],
52
+ topics: []
53
  });
54
 
55
  const messagesEndRef = useRef<HTMLDivElement | null>(null);
56
 
57
  const fetchProjects = () => {
58
+ fetch(`/api/projects?page=${page}&search=${encodeURIComponent(search)}&status=${statusFilter}&legalBasis=${legalFilter}&organization=${orgFilter}&country=${countryFilter}&fundingScheme=${fundingSchemeFilter}&proj_id=${idFilter}&topic=${topicsFilter}&sortField=${sortField}&sortOrder=${sortOrder}`)
59
  .then(res => res.json())
60
  .then((data: Project[]) => setProjects(data))
61
  .catch(console.error);
 
80
  countries: data.countries,
81
  legalBases: data.legalBases,
82
  fundingSchemes: data.fundingSchemes,
83
+ ids: [],
84
+ topics: data.topics
85
  });
86
  });
87
  };