Skip to content

Commit 9e2d0fc

Browse files
committed
feat(fundings): Add "institutions" tab
1 parent 3bc4e45 commit 9e2d0fc

3 files changed

Lines changed: 377 additions & 3 deletions

File tree

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
import { Title } from "@dataesr/dsfr-plus";
2+
import { useQuery } from "@tanstack/react-query";
3+
import type HighchartsInstance from "highcharts/es-modules/masters/highcharts.src.js";
4+
import { useState } from "react";
5+
import { useSearchParams } from "react-router-dom";
6+
7+
import DefaultSkeleton from "../../../../components/charts-skeletons/default.tsx";
8+
import { useChartColor } from "../../../../hooks/useChartColor.tsx";
9+
import { getI18nLabel } from "../../../../utils.tsx";
10+
import ChartWrapperFundings from "../../components/chart-wrapper-fundings/index.tsx";
11+
import SegmentedControl from "../../components/segmented-control/index.tsx";
12+
import i18n from "../../i18n.json";
13+
import { formatCompactNumber, funders, getCssColor, getEsQuery, getYearRangeLabel, pattern } from "../../utils.ts";
14+
15+
const { VITE_APP_ES_INDEX_PARTICIPATIONS, VITE_APP_SERVER_URL } = import.meta.env;
16+
17+
export default function Institutions({ name }: { name: string | undefined }) {
18+
const [selectedControl, setSelectedControl] = useState("projects")
19+
const [searchParams] = useSearchParams()
20+
const region = searchParams.get("region")
21+
const structure = searchParams.get("structure")
22+
const yearMax = searchParams.get("yearMax")
23+
const yearMin = searchParams.get("yearMin")
24+
const color = useChartColor()
25+
26+
const body:any = {
27+
...getEsQuery({ regions: [region], structures: [structure], yearMax, yearMin }),
28+
aggregations: {
29+
by_institution_project: {
30+
terms: {
31+
field: "participant_id_name_default.keyword",
32+
order: { "by_unique_projects": "desc" },
33+
},
34+
aggregations: {
35+
by_unique_projects: {
36+
cardinality: {
37+
field: "project_id.keyword",
38+
},
39+
},
40+
by_project_type: {
41+
terms: {
42+
field: "project_type.keyword",
43+
},
44+
aggregations: {
45+
is_coordinator: {
46+
terms: {
47+
field: "participation_is_coordinator",
48+
},
49+
aggregations: {
50+
unique_projects: {
51+
cardinality: {
52+
field: "project_id.keyword",
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
},
61+
by_institution_budget: {
62+
terms: {
63+
field: "participant_id_name_default.keyword",
64+
order: { "sum_budget": "desc" },
65+
},
66+
aggregations: {
67+
sum_budget: {
68+
sum: {
69+
field: "project_budgetFinanced",
70+
},
71+
},
72+
by_project_type: {
73+
terms: {
74+
field: "project_type.keyword",
75+
},
76+
aggregations: {
77+
is_coordinator: {
78+
terms: {
79+
field: "participation_is_coordinator",
80+
},
81+
aggregations: {
82+
should_ignore_budget: {
83+
terms: {
84+
field: structure ? "participant_ignore_total_budget" : "region_ignore_total_budget",
85+
missing: false,
86+
},
87+
aggregations: {
88+
sum_budget: {
89+
sum: {
90+
field: "project_budgetFinanced",
91+
},
92+
},
93+
},
94+
},
95+
},
96+
},
97+
},
98+
},
99+
},
100+
},
101+
by_institution_participation: {
102+
terms: {
103+
field: "participant_id_name_default.keyword",
104+
order: { "sum_budget_funding": "desc" },
105+
},
106+
aggregations: {
107+
sum_budget_funding: {
108+
sum: {
109+
field: "participation_funding",
110+
},
111+
},
112+
by_project_type: {
113+
terms: {
114+
field: "project_type.keyword",
115+
},
116+
aggregations: {
117+
is_coordinator: {
118+
terms: {
119+
field: "participation_is_coordinator",
120+
},
121+
aggregations: {
122+
should_ignore_funding: {
123+
terms: {
124+
field: structure ? "participant_ignore_funding" : "region_ignore_funding",
125+
missing: false,
126+
},
127+
aggregations: {
128+
sum_budget_funding: {
129+
sum: {
130+
field: "participation_funding",
131+
},
132+
},
133+
},
134+
},
135+
},
136+
},
137+
},
138+
},
139+
},
140+
},
141+
},
142+
};
143+
144+
const { data, isLoading } = useQuery({
145+
queryKey: ["fundings-institutions", region, structure, yearMax, yearMin],
146+
queryFn: () =>
147+
fetch(`${VITE_APP_SERVER_URL}/elasticsearch?index=${VITE_APP_ES_INDEX_PARTICIPATIONS}`, {
148+
body: JSON.stringify(body),
149+
headers: {
150+
"Access-Control-Allow-Origin": "*",
151+
"Content-Type": "application/json",
152+
},
153+
method: "POST",
154+
}).then((response) => response.json()),
155+
});
156+
157+
const seriesBudget: any = [];
158+
const seriesFunding: any = [];
159+
const seriesProject: any = [];
160+
const seriesBudgetRegion: any = [];
161+
const seriesFundingRegion: any = [];
162+
const seriesProjectRegion: any = [];
163+
const institutionsBudget = data?.aggregations?.by_institution_budget?.buckets ?? [];
164+
const institutionsFunding = data?.aggregations?.by_institution_participation?.buckets ?? [];
165+
const institutionsProject = data?.aggregations?.by_institution_project?.buckets ?? [];
166+
funders.forEach((funder) => {
167+
seriesBudget.push({
168+
color: { pattern: { ...pattern, backgroundColor: getCssColor({ name: funder, prefix: "funder" }) } },
169+
data: institutionsBudget.map((bucket) => bucket.by_project_type.buckets
170+
?.find((bucket) => bucket.key === funder)?.is_coordinator?.buckets
171+
?.find((bucket) => bucket.key === 1)?.should_ignore_budget?.buckets
172+
?.find((bucket) => bucket.key === 0)?.sum_budget?.value ?? 0),
173+
name: [funder, getI18nLabel(i18n, 'coordinator')].join(' - '),
174+
});
175+
seriesBudget.push({
176+
color: getCssColor({ name: funder, prefix: "funder" }),
177+
data: institutionsBudget.map((bucket) => bucket.by_project_type.buckets
178+
?.find((bucket) => bucket.key === funder)?.is_coordinator?.buckets
179+
?.find((bucket) => bucket.key === 0)?.should_ignore_budget?.buckets
180+
?.find((bucket) => bucket.key === 0)?.sum_budget?.value ?? 0),
181+
name: [funder, getI18nLabel(i18n, 'not-coordinator')].join(' - '),
182+
});
183+
seriesBudgetRegion.push({
184+
color: getCssColor({ name: funder, prefix: "funder" }),
185+
data: institutionsBudget.map((bucket) => bucket
186+
?.by_project_type.buckets?.find((bucket) => bucket.key === funder)
187+
?.is_coordinator?.buckets?.reduce((acc, curr) => acc + (curr?.should_ignore_budget?.buckets?.find((bucket) => bucket.key == 0)?.sum_budget?.value ?? 0), 0)
188+
?? 0),
189+
name: funder,
190+
});
191+
seriesFunding.push({
192+
color: { pattern: { ...pattern, backgroundColor: getCssColor({ name: funder, prefix: "funder" }) } },
193+
data: institutionsFunding.map((bucket) => bucket.by_project_type.buckets
194+
?.find((bucket) => bucket.key === funder)?.is_coordinator?.buckets
195+
?.find((bucket) => bucket.key === 1)?.should_ignore_funding?.buckets
196+
?.find((bucket) => bucket.key === 0)?.sum_budget_funding?.value ?? 0),
197+
name: [funder, getI18nLabel(i18n, 'coordinator')].join(' - '),
198+
});
199+
seriesFunding.push({
200+
color: getCssColor({ name: funder, prefix: "funder" }),
201+
data: institutionsFunding.map((bucket) => bucket.by_project_type.buckets
202+
?.find((bucket) => bucket.key === funder)?.is_coordinator?.buckets
203+
?.find((bucket) => bucket.key === 0)?.should_ignore_funding?.buckets
204+
?.find((bucket) => bucket.key === 0)?.sum_budget_funding?.value ?? 0),
205+
name: [funder, getI18nLabel(i18n, 'not-coordinator')].join(' - '),
206+
});
207+
seriesFundingRegion.push({
208+
color: getCssColor({ name: funder, prefix: "funder" }),
209+
data: institutionsFunding.map((bucket) => bucket
210+
?.by_project_type.buckets?.find((bucket) => bucket.key === funder)
211+
?.is_coordinator?.buckets?.reduce((acc, curr) => acc + (curr?.should_ignore_funding?.buckets?.find((bucket) => bucket.key == 0)?.sum_budget_funding?.value ?? 0), 0)
212+
?? 0),
213+
name: funder,
214+
});
215+
seriesProject.push({
216+
color: { pattern: { ...pattern, backgroundColor: getCssColor({ name: funder, prefix: "funder" }) } },
217+
data: institutionsProject.map((bucket) => bucket.by_project_type.buckets
218+
?.find((bucket) => bucket.key === funder)?.is_coordinator?.buckets
219+
?.find((bucket) => bucket.key === 1)?.unique_projects?.value ?? 0),
220+
name: [funder, getI18nLabel(i18n, 'coordinator')].join(' - '),
221+
});
222+
seriesProject.push({
223+
color: getCssColor({ name: funder, prefix: "funder" }),
224+
data: institutionsProject.map((bucket) => bucket.by_project_type.buckets
225+
?.find((bucket) => bucket.key === funder)?.is_coordinator?.buckets
226+
?.find((bucket) => bucket.key === 0)?.unique_projects?.value ?? 0),
227+
name: [funder, getI18nLabel(i18n, 'not-coordinator')].join(' - '),
228+
});
229+
seriesProjectRegion.push({
230+
color: getCssColor({ name: funder, prefix: "funder" }),
231+
data: institutionsProject.map((bucket) => bucket
232+
?.by_project_type.buckets?.find((bucket) => bucket.key === funder)
233+
?.is_coordinator?.buckets?.reduce((acc, curr) => acc + (curr?.unique_projects?.value ?? 0), 0)
234+
?? 0),
235+
name: funder,
236+
});
237+
});
238+
const categoriesBudget = institutionsBudget.map((bucket) => bucket.key.split('###')[1]);
239+
const categoriesFunding = institutionsFunding.map((bucket) => bucket.key.split('###')[1]);
240+
const categoriesProject = institutionsProject.map((bucket) => bucket.key.split('###')[1]);
241+
242+
const title = `Principaux établissements de ${structure ? "l'établissement" : "la région"} ${name} impliqués dans les projets par AAP ${getYearRangeLabel({ yearMax, yearMin })}`;
243+
// If view by number of projects
244+
let axis = getI18nLabel(i18n, 'number_of_projects_funded');
245+
let categories = categoriesProject;
246+
let dataLabel = function (this: any) {
247+
return `${this.y} projet${this.y > 1 ? 's' : ''}`;
248+
};
249+
let series = structure ? seriesProject.reverse() : seriesProjectRegion.reverse();
250+
let stackLabel = function (this: any) {
251+
return `${this.total} projet${this.total > 1 ? 's' : ''}`;
252+
};
253+
let tooltip = function (this: any) {
254+
return `<b>${this.y}</b> projets <b>${this.series.name}</b> auxquels participe <b>${categoriesProject[this.x]}</b> ${getYearRangeLabel({ isBold: true, yearMax, yearMin })}`;
255+
};
256+
switch (selectedControl) {
257+
// If view by global amount
258+
case 'amount_global':
259+
axis = getI18nLabel(i18n, 'funding_total');
260+
categories = categoriesBudget;
261+
dataLabel = function (this: any) {
262+
return `${formatCompactNumber(this.y)} €`;
263+
};
264+
series = structure ? seriesBudget.reverse() : seriesBudgetRegion.reverse();
265+
stackLabel = function (this: any) {
266+
return `${formatCompactNumber(this.total)} €`;
267+
};
268+
tooltip = function (this: any) {
269+
return `<b>${formatCompactNumber(this.y)} €</b> ont été financés au global dans le cadre de projets <b>${this.series.name}</b> pour des projets débutés ${getYearRangeLabel({ isBold: true, yearMax, yearMin })} auxquels prend part <b>${categoriesBudget[this.x]}</b>`;
270+
};
271+
break;
272+
// If view by amount by structure
273+
case 'amount_by_structure':
274+
axis = getI18nLabel(i18n, structure ? 'funding_by_structure' : 'funding_by_region');
275+
categories = categoriesFunding;
276+
dataLabel = function (this: any) {
277+
return `${formatCompactNumber(this.y)} €`;
278+
};
279+
series = structure ? seriesFunding.reverse() : seriesFundingRegion.reverse();
280+
stackLabel = function (this: any) {
281+
return `${formatCompactNumber(this.total)} €`;
282+
};
283+
tooltip = function (this: any) {
284+
return `<b>${formatCompactNumber(this.y)} €</b> ont été perçus par <b>${categoriesBudget[this.x]}</b> dans le cadre de projets <b>${this.series.name}</b> pour des projets débutés ${getYearRangeLabel({ isBold: true, yearMax, yearMin })}`;
285+
};
286+
break;
287+
};
288+
289+
const config = {
290+
comment: {
291+
"fr": <>Ce graphe présente la répartition des projets financés par appels à projets (AAP) dans lesquels l'établissement est impliqué, ventilée par laboratoire et par financeur.
292+
Les sources de données ne donnent pas toujours accès au niveau laboratoire. Pour les projets européens, c'est un travail mené avec 5 organismes pour ajouter ce niveau, avec un délai d'actualisation de un an. Pour le PIA, les données au niveau laboratoire ne sont pas disponibles.
293+
Le type de participation est distingué, en pointillé quand l'établissement est coordinateur, en couleur simple s'il est partenaire non-coordinateur. Le financement global représente le volume total de financements des projets auxquels participe l'établissement. Le financement perçu approxime la part réelle allouée à chaque établissement partenaire d’un projet (en assimilant consommation et subvention pour le PIA).
294+
</>
295+
},
296+
id: "institutions",
297+
integrationURL: `/integration?chart_id=institutions&${searchParams.toString()}`,
298+
title,
299+
};
300+
301+
const options: HighchartsInstance.Options = {
302+
chart: { height: "1000px" },
303+
legend: { enabled: true, reversed: true },
304+
plotOptions: {
305+
series: {
306+
dataLabels: {
307+
enabled: true,
308+
formatter: dataLabel,
309+
},
310+
stacking: "normal",
311+
},
312+
},
313+
series,
314+
title: { text: "" },
315+
tooltip: { formatter: tooltip },
316+
xAxis: { categories, title: { text: "" } },
317+
yAxis: {
318+
stackLabels: {
319+
enabled: true,
320+
style: {
321+
fontWeight: "bold",
322+
},
323+
formatter: stackLabel,
324+
},
325+
title: { text: axis },
326+
},
327+
};
328+
329+
return (
330+
<div className={`chart-container chart-container--${color}`} id="institutions">
331+
<Title as="h2" look="h6">
332+
{title}
333+
</Title>
334+
<SegmentedControl selectedControl={selectedControl} setSelectedControl={setSelectedControl} />
335+
{isLoading ? <DefaultSkeleton height="1000px" /> : <ChartWrapperFundings config={config} hideTitle options={options} />}
336+
</div>
337+
);
338+
}

0 commit comments

Comments
 (0)