diff --git a/nupes-elections-front/src/App.js b/nupes-elections-front/src/App.js index fbe4eb8..f5c3c61 100644 --- a/nupes-elections-front/src/App.js +++ b/nupes-elections-front/src/App.js @@ -1,22 +1,39 @@ import {createBrowserRouter, RouterProvider} from "react-router-dom" import './App.css' -import Elections2024 from './Elections2024' +import ElectionsLegislatives2022 from "./ElectionsLegislatives2022" +import ElectionsEuropeennes2024 from './ElectionsEuropeennes2024' function App() { const router = createBrowserRouter([ { path: "/", - element: , + element: , + }, + { + path: "/elections/legislatives/2022/:typeResultats/", + element: , + }, + { + path: "/elections/legislatives/2022/:typeResultats/:zoneId/", + element: }, { path: "/elections/europeennes/2024/:typeResultats/", - element: , + element: , }, { path: "/elections/europeennes/2024/:typeResultats/:zoneId/", - element: + element: }, + // { + // path: "/elections/legislatives/2024/:typeResultats/", + // element: , + // }, + // { + // path: "/elections/legislatives/2024/:typeResultats/:zoneId/", + // element: + // }, ]) return <> diff --git a/nupes-elections-front/src/ElectionsEuropeennes2024.js b/nupes-elections-front/src/ElectionsEuropeennes2024.js index c68dca3..6e9651e 100644 --- a/nupes-elections-front/src/ElectionsEuropeennes2024.js +++ b/nupes-elections-front/src/ElectionsEuropeennes2024.js @@ -63,6 +63,27 @@ export default function ElectionsEuropeennes2024() { const siegesParListe = calculerSieges(listes, donnees, retirerSeuil ? 0 : 0.05) const [siegesParBloc, siegesParNuance] = regrouperVoix(siegesParListe, listes, blocs, nuances) + const [categoriesHistogramme, valeursHistogramme, couleursHistogramme] = useMemo(() => { + if (grouperParBloc) { + const categories = {} + const couleurs = {} + for (let bloc of blocs) { + categories[bloc.nom] = bloc.nom + couleurs[bloc.nom] = bloc.couleur + } + return [categories, voixParBloc, couleurs] + } + else { + const categories = {} + const couleurs = {} + for (let nuance of nuances) { + categories[nuance.code] = nuance.nom + couleurs[nuance.code] = nuance.couleur + } + return [categories, voixParNuance, couleurs] + } + }, [typeResultats, nuances, blocs, donnees, voixParNuance, voixParBloc, grouperParBloc]) + return <> @@ -73,8 +94,8 @@ export default function ElectionsEuropeennes2024() { + nomCategories={categoriesHistogramme} valeurParCategorie={valeursHistogramme} + totalExprimes={donnees.exprimes} couleurParCategorie={couleursHistogramme} /> diff --git a/nupes-elections-front/src/ElectionsLegislatives2022.js b/nupes-elections-front/src/ElectionsLegislatives2022.js new file mode 100644 index 0000000..5bdca16 --- /dev/null +++ b/nupes-elections-front/src/ElectionsLegislatives2022.js @@ -0,0 +1,148 @@ +import {useParams} from "react-router-dom" +import {AppBar, Container, Toolbar} from "@mui/material" +import * as Highcharts from 'highcharts' +import highchartsItem from 'highcharts/modules/item-series' +import {useEffect, useMemo, useState} from "react" +import { + SelectionAffichage, + TableauParticipation, + CarteResultats, + HistogrammeVoix, GroupementParBloc, SelectionTour +} from "./includes/composants_elections" +import {getNomZone, regrouperVoix} from "./utils" +import 'leaflet/dist/leaflet.css' +import { + TableauResultatsCandidatsLegislatives, + TableauResultatsNuancesLegislatives +} from "./includes/composants_elections_legislatives" + + +highchartsItem(Highcharts) + +export default function ElectionsLegislatives2022() { + const {typeResultats, zoneId} = useParams() + + const [grouperParBloc, setGrouperParBloc] = useState(false) + const [tour, setTour] = useState(1) + const [blocs, setBlocs] = useState([]) + const [candidats, setCandidats] = useState([]) + const [nuances, setNuances] = useState([]) + const [resultats, setResultats] = useState([]) + const [typeSousZone, setTypeSousZone] = useState("region") + + useEffect(() => { + fetch("/data/resultats/legislatives/2022/blocs.json").then(response => response.json()) + .then(data => setBlocs(data)) + fetch("/data/resultats/legislatives/2022/nuances.json").then(response => response.json()) + .then(data => setNuances(data)) + + if (typeResultats === "france") { + fetch("/data/resultats/legislatives/2022/france.json").then(response => response.json()) + .then(data => setResultats(data)) + } + else { + fetch(`/data/resultats/legislatives/2022/${typeResultats}/${zoneId}.json`) + .then(response => response.json()) + .then(data => setResultats(data)) + } + }, [typeResultats, zoneId]) + + const zoneInfo = useMemo(() => resultats?.zone ?? {}, [resultats]) + const nomZone = useMemo(() => getNomZone(typeResultats, zoneInfo), [typeResultats, zoneInfo]) + + useEffect(() => { + let circonscription = "" + + if (typeResultats === "circonscription") + circonscription = zoneId + else if (typeResultats === "bureau_vote") + circonscription = zoneInfo?.circonscription ?? "" + else { + setCandidats(nuances) + } + + if (!circonscription) + return + + fetch(`/data/resultats/legislatives/2022/candidats/${circonscription}.json`) + .then(response => response.json()) + .then(data => setCandidats(data)) + }, [typeResultats, zoneId, zoneInfo, nuances]) + + const donnees = useMemo(() => { + if (tour === 1) + return resultats?.tour1 ?? {} + else if (tour === 2) + return resultats?.tour2 ?? {} + else + return {} + }, [resultats, tour]) + + const dejaGroupeParNuance = typeResultats !== "circonscription" && typeResultats !== "bureau_vote" + const [voixParBloc, voixParNuance] = regrouperVoix(donnees.voix, candidats, blocs, nuances, + dejaGroupeParNuance) + + const candidatKey = typeSousZone === "circonscription" || typeSousZone === "bureau_vote" ? "numero" : "code" + + const [categoriesHistogramme, valeursHistogramme, couleursHistogramme] = useMemo(() => { + if (grouperParBloc) { + const categories = {} + const couleurs = {} + for (let bloc of blocs) { + categories[bloc.nom] = bloc.nom + couleurs[bloc.nom] = bloc.couleur + } + return [categories, voixParBloc, couleurs] + } + else { + if (typeResultats === "circonscription" || typeResultats === "bureau_vote") { + // On affiche les noms des candidat⋅es + const categories = {} + const couleurs = {} + for (let candidat of candidats) { + categories[candidat.numero] = `${candidat.prenom} ${candidat.nom} (${candidat.nuance})` + couleurs[candidat.numero] = nuances.filter(nuance => nuance.code === candidat.nuance)[0]?.couleur + } + return [categories, donnees.voix, couleurs] + } + else { + // On affiche les nuances + const categories = {} + const couleurs = {} + for (let nuance of nuances) { + categories[nuance.code] = nuance.nom + couleurs[nuance.code] = nuance.couleur + } + return [categories, voixParNuance, couleurs] + + } + } + }, [typeResultats, candidats, nuances, blocs, donnees, voixParNuance, voixParBloc, grouperParBloc]) + + const tableauResultats = useMemo(() => { + if (typeResultats === "circonscription" || typeResultats === "bureau_vote") + return + else + return + }, [typeResultats, candidats, blocs, nuances, donnees]) + + return <> + + + + + + + + + + {tableauResultats} + + + + +} diff --git a/nupes-elections-front/src/includes/composants_elections.js b/nupes-elections-front/src/includes/composants_elections.js index aaa3a4e..c3b689d 100644 --- a/nupes-elections-front/src/includes/composants_elections.js +++ b/nupes-elections-front/src/includes/composants_elections.js @@ -16,25 +16,19 @@ import Switch from "@mui/material/Switch"; import FormControlLabel from "@mui/material/FormControlLabel"; -export function HistogrammeVoix({titre, resultats, voixParNuance, voixParBloc, blocs, nuances, grouperParBloc}) { +export function HistogrammeVoix({titre, nomCategories, valeurParCategorie, totalExprimes, couleurParCategorie}) { const [categoriesVoix, dataVoix] = useMemo(() => { const categories = [] const data = [] - if (grouperParBloc) { - for (let bloc of blocs) { - categories.push(bloc.nom) - data.push([bloc.nom, voixParBloc[bloc.nom], bloc.couleur, bloc.nom]) - } - } - else { - for (let nuance of nuances) { - categories.push(nuance.nom) - data.push([nuance.nom, voixParNuance[nuance.code], nuance.couleur, nuance.nom]) - } + + for (let categorie of Object.keys(nomCategories)) { + categories.push(nomCategories[categorie]) + data.push([nomCategories[categorie], valeurParCategorie[categorie], couleurParCategorie[categorie], + nomCategories[categorie]]) } return [categories, data] - }, [voixParBloc, voixParNuance, blocs, nuances, grouperParBloc]) + }, [nomCategories, valeurParCategorie, couleurParCategorie]) const scoreOptions = { chart: { @@ -45,7 +39,7 @@ export function HistogrammeVoix({titre, resultats, voixParNuance, voixParBloc, b }, tooltip: { formatter: function () { - return `${this.x} : ${this.y} voix (${(100 * this.y / resultats.exprimes).toFixed(2)} %)
` + return `${this.x} : ${this.y} voix (${(100 * this.y / totalExprimes).toFixed(2)} %)
` } }, xAxis: { @@ -244,9 +238,11 @@ export function SelectionAffichage({typeResultats, typeSousZone, setTypeSousZone function ZoneGeoJSON({typeElection, anneeElection, resultatsZone, typeSousZone, - candidats, blocs, nuances, tour, grouperParBloc = false}) { + candidats, blocs, nuances, tour, grouperParBloc = false, candidatKey = "numero"}) { const sousZoneInfo = resultatsZone.zone - const donnees = resultatsZone ? (tour === 1 ? resultatsZone.tour1 : resultatsZone.tour2) : {} + const donnees = useMemo(() => { + return resultatsZone ? (tour === 1 ? resultatsZone.tour1 : resultatsZone.tour2) : {} + }, [resultatsZone, tour]) const [idZone, nomZone] = useMemo(() => { if (!sousZoneInfo) @@ -262,10 +258,23 @@ function ZoneGeoJSON({typeElection, anneeElection, resultatsZone, typeSousZone, return ["", ""] }, [typeSousZone, sousZoneInfo]) - const voixCandidats = useMemo(() => donnees?.voix ?? {}, [resultatsZone]) - const candidatsTries = trierCandidats(candidats, voixCandidats) + const [candidatsZone, setCandidatsZone] = useState(candidats) + useEffect(() => { + if (typeElection === "legislatives" && (typeSousZone === "circonscription" || typeSousZone === "bureau_vote") && sousZoneInfo.id) { + const circo_id = typeSousZone === "circonscription" ? sousZoneInfo.id : sousZoneInfo.circonscription + fetch(`/data/resultats/${typeElection}/${anneeElection}/candidats/${circo_id}.json`) + .then(response => response.json()) + .then(data => setCandidatsZone(data)) + } + }, [typeElection, anneeElection, typeSousZone, sousZoneInfo]) - const [voixParBloc, voixParNuance] = regrouperVoix(voixCandidats, candidats, blocs, nuances) + const voixCandidats = useMemo(() => donnees?.voix ?? {}, [donnees]) + const candidatsTries = trierCandidats(candidatsZone, voixCandidats, candidatKey) + + const dejaGroupesParNuance = typeElection === "legislatives" + && (typeSousZone !== "circonscription" && typeSousZone !== "bureau_vote") + const [voixParBloc, voixParNuance] = regrouperVoix(voixCandidats, candidatsZone, blocs, nuances, + dejaGroupesParNuance) let couleur = 'grey' if (grouperParBloc) { @@ -294,8 +303,8 @@ function ZoneGeoJSON({typeElection, anneeElection, resultatsZone, typeSousZone, {nomZone}
    {candidatsTries.slice(0, 5).map(candidat => -
  • - {candidat.nom} : {voixCandidats[candidat.numero]} ({(100 * voixCandidats[candidat.numero] / donnees.exprimes).toFixed(2)} %) +
  • + {candidat.nom} : {voixCandidats[candidat[candidatKey]]} ({(100 * voixCandidats[candidat[candidatKey]] / donnees.exprimes).toFixed(2)} %)
  • )}
@@ -303,7 +312,7 @@ function ZoneGeoJSON({typeElection, anneeElection, resultatsZone, typeSousZone, } function ContenuCarte({typeElection, anneeElection, zoneInfo, typeSousZone, candidats, blocs, nuances, tour, - grouperParBloc = false}) { + grouperParBloc = false, candidatKey = "numero"}) { const map = useMap() const [resultatsZones, setResultatsZones] = useState([]) @@ -366,15 +375,15 @@ function ContenuCarte({typeElection, anneeElection, zoneInfo, typeSousZone, cand return <> {resultatsZones.filter(resultatsZone => resultatsZone.zone.geometry['type']) .map(resultatsZone => - )} + blocs={blocs} nuances={nuances} tour={tour} grouperParBloc={grouperParBloc} candidatKey={candidatKey} />)} } export function CarteResultats({typeElection, anneeElection, typeResultats, zoneInfo, typeSousZone, candidats, - blocs, nuances, tour, grouperParBloc = false}) { + blocs, nuances, tour, grouperParBloc = false, candidatKey = "numero"}) { const center = typeResultats === "france" ? [46.603354, 1.888334] : [0, 0] return <> @@ -385,7 +394,7 @@ export function CarteResultats({typeElection, anneeElection, typeResultats, zone /> + blocs={blocs} nuances={nuances} tour={tour} grouperParBloc={grouperParBloc} candidatKey={candidatKey} /> } diff --git a/nupes-elections-front/src/includes/composants_elections_legislatives.js b/nupes-elections-front/src/includes/composants_elections_legislatives.js new file mode 100644 index 0000000..f12b752 --- /dev/null +++ b/nupes-elections-front/src/includes/composants_elections_legislatives.js @@ -0,0 +1,103 @@ +import {trierCandidats} from "../utils" +import TableContainer from "@mui/material/TableContainer" +import Paper from "@mui/material/Paper" +import Table from "@mui/material/Table" +import TableHead from "@mui/material/TableHead" +import TableRow from "@mui/material/TableRow" +import TableCell from "@mui/material/TableCell" +import TableBody from "@mui/material/TableBody" + +export function TableauResultatsCandidatsLegislatives({blocs, candidats, nuances, donnees}) { + const voixCandidats = donnees?.voix ?? {} + const candidatsTriees = trierCandidats(candidats, voixCandidats) + + return <> + + + + + Numéro + Candidat + Nuance + Bloc + Voix + % Inscrit⋅es + % Exprimé⋅es + + + + {candidatsTriees.map((candidat) => ( + + ))} + +
+
+ +} + +function LigneCandidat({candidat, voix, donnees, nuances, blocs}) { + const nuance = nuances.filter(nuance => nuance.code === candidat.nuance)[0] + const bloc = blocs.filter(bloc => bloc.nom === candidat.bloc)[0] + + return + {candidat.numero} + {candidat.prenom} {candidat.nom} + + {nuance.nom} ({nuance.code}) + + {bloc.nom} + {voix} + {(100 * voix / donnees.inscrits).toFixed(2)} % + {(100 * voix / donnees.exprimes).toFixed(2)} % + +} + +/** + * Composant pour le tableau des résultats des élections législatives + * @param blocs + * @param nuances + * @param donnees + * @return {JSX.Element} + * @constructor + */ +export function TableauResultatsNuancesLegislatives({blocs, nuances, donnees}) { + const voixNuances = donnees?.voix ?? {} + const nuancesTriees = trierCandidats(nuances, voixNuances, "code") + + return <> + + + + + Nuance + Bloc + Voix + % Inscrit⋅es + % Exprimé⋅es + + + + {nuancesTriees.map((nuance) => ( + + ))} + +
+
+ +} + +function LigneNuance({nuance, voix, donnees, blocs}) { + const bloc = blocs.filter(bloc => bloc.nom === nuance.bloc)[0] + + return + + {nuance.nom} ({nuance.code}) + + {bloc.nom} + {voix} + {(100 * voix / donnees.inscrits).toFixed(2)} % + {(100 * voix / donnees.exprimes).toFixed(2)} % + +} diff --git a/nupes-elections-front/src/utils.js b/nupes-elections-front/src/utils.js index d6e4a95..aa6aefd 100644 --- a/nupes-elections-front/src/utils.js +++ b/nupes-elections-front/src/utils.js @@ -1,5 +1,5 @@ export function getNomZone(typeResultats, zoneInfo) { - if (!zoneInfo) + if (!zoneInfo.type) return "" else if (typeResultats === "france") return "France" @@ -15,30 +15,36 @@ export function getNomZone(typeResultats, zoneInfo) { return zoneInfo.libelle } -export function trierCandidats(candidats, voix_par_candidat) { +export function trierCandidats(candidats, voix_par_candidat, key = "numero") { return candidats.toSorted((l1, l2) => { - return (voix_par_candidat[l2.numero] || 0) - (voix_par_candidat[l1.numero] || 0) + return (voix_par_candidat[l2[key]] || 0) - (voix_par_candidat[l1[key]] || 0) }) } -export function regrouperVoix(voixCandidats, candidats, blocs, nuances) { +export function regrouperVoix(voixCandidats, candidats, blocs, nuances, dejaGroupesParNuance = false) { if (!candidats || !voixCandidats || !blocs || !nuances || candidats.length === 0 || blocs.length === 0 || nuances.length === 0) return [{}, {}] + const key = dejaGroupesParNuance ? "code" : "numero" + const parBloc = {} - const parNuance = {} + const parNuance = dejaGroupesParNuance ? voixCandidats : {} for (let bloc of blocs) { parBloc[bloc.nom] = 0 } - for (let nuance of nuances) { - parNuance[nuance.code] = 0 + + if (!dejaGroupesParNuance) { + for (let nuance of nuances) { + parNuance[nuance.code] = 0 + } } for (let candidat of candidats) { - parBloc[candidat.bloc] += voixCandidats[candidat.numero] || 0 - parNuance[candidat.nuance] += voixCandidats[candidat.numero] || 0 + parBloc[candidat.bloc] += voixCandidats[candidat[key]] || 0 + if (!dejaGroupesParNuance) + parNuance[candidat.nuance] += voixCandidats[candidat[key]] || 0 } return [parBloc, parNuance] diff --git a/nupes/scripts/legislatives2022/export_resultats.py b/nupes/scripts/legislatives2022/export_resultats.py index 045034b..ddc1f2b 100644 --- a/nupes/scripts/legislatives2022/export_resultats.py +++ b/nupes/scripts/legislatives2022/export_resultats.py @@ -31,7 +31,12 @@ def exporter_nuances(engine: Engine, verbose: bool = False) -> None: nuances_json = [] for nuance in nuances: - nuance_json = {'code': nuance.code, 'nom': nuance.nom, 'couleur': nuance.couleur, "bloc": nuance.bloc_id} + nuance_json = { + 'code': nuance.code, + 'nom': nuance.nom, + 'couleur': nuance.couleur, + "bloc": nuance.bloc.nom, + } nuances_json.append(nuance_json) file = DATA_DIR / "resultats" / "legislatives" / "2022" / "nuances.json" @@ -54,6 +59,7 @@ def exporter_candidats(engine: Engine, verbose: bool = False) -> None: candidat_json = { 'numero': candidat.numero, 'nuance': candidat.nuance_id, + 'bloc': candidat.nuance.bloc.nom, 'nom': candidat.nom, 'prenom': candidat.prenom, 'nom_suppleance': candidat.nom_suppleance, @@ -316,8 +322,8 @@ def exporter_resultats_circonscriptions(engine: Engine, verbose: bool = False) - resultats_dict['tour1']['voix'] = resultats_t1 resultats_dict['tour2']['voix'] = resultats_t2 for voix_candidat in resultats_circonscription.voix: - resultats_t1[voix_candidat.candidat.id] = voix_candidat.voix_t1 - resultats_t2[voix_candidat.candidat.id] = voix_candidat.voix_t2 + resultats_t1[voix_candidat.candidat.numero] = voix_candidat.voix_t1 + resultats_t2[voix_candidat.candidat.numero] = voix_candidat.voix_t2 file = DATA_DIR / "resultats" / "legislatives" / "2022" / "circonscription" / f"{circonscription.id}.json" if not file.parent.is_dir(): @@ -457,8 +463,8 @@ def exporter_resultats_bureaux_vote(engine: Engine, verbose: bool = False) -> No resultats_dict['tour1']['voix'] = resultats_t1 resultats_dict['tour2']['voix'] = resultats_t2 for voix_candidat in resultats_bureau_vote.voix: - resultats_t1[voix_candidat.candidat.id] = voix_candidat.voix_t1 - resultats_t2[voix_candidat.candidat.id] = voix_candidat.voix_t2 + resultats_t1[voix_candidat.candidat.numero] = voix_candidat.voix_t1 + resultats_t2[voix_candidat.candidat.numero] = voix_candidat.voix_t2 file = DATA_DIR / "resultats" / "legislatives" / "2022" / "bureau_vote" / f"{bureau_vote.id}.json" if not file.parent.is_dir(): diff --git a/nupes/scripts/legislatives2022/import_candidats.py b/nupes/scripts/legislatives2022/import_candidats.py index 443b171..e990702 100644 --- a/nupes/scripts/legislatives2022/import_candidats.py +++ b/nupes/scripts/legislatives2022/import_candidats.py @@ -33,7 +33,7 @@ def creer_blocs(engine: Engine, verbose: bool = False) -> None: def creer_nuances(engine: Engine, verbose: bool = False) -> None: nuances = [ {"code": "DXG", "nom": "Divers extrême gauche", "couleur": "#BB0000", "bloc_id": 1}, - {"code": "RDG", "nom": "Radical de gauche", "couleur": "#FFD1DC", "bloc_id": 1}, + {"code": "RDG", "nom": "Parti radical de gauche", "couleur": "#FFD1DC", "bloc_id": 1}, {"code": "NUP", "nom": "Nouvelle union populaire écologique et sociale", "couleur": "#E4032E", "bloc_id": 1}, {"code": "DVG", "nom": "Divers gauche", "couleur": "#FFC0C0", "bloc_id": 1}, {"code": "ECO", "nom": "Écologistes", "couleur": "#77FF77", "bloc_id": 5},