diff --git a/nupes-elections-front/src/Elections2024.js b/nupes-elections-front/src/Elections2024.js index fc6e351..a46f4c9 100644 --- a/nupes-elections-front/src/Elections2024.js +++ b/nupes-elections-front/src/Elections2024.js @@ -1,572 +1,80 @@ import {useParams} from "react-router-dom" -import {MenuItem, Select} from "@mui/material" -import FormGroup from '@mui/material/FormGroup' -import FormControlLabel from '@mui/material/FormControlLabel' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell from '@mui/material/TableCell' -import TableContainer from '@mui/material/TableContainer' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import Paper from '@mui/material/Paper' -import Switch from '@mui/material/Switch' +import {AppBar, Container, Toolbar} from "@mui/material" import * as Highcharts from 'highcharts' import highchartsItem from 'highcharts/modules/item-series' -import HighchartsReact from 'highcharts-react-official' import {useEffect, useMemo, useState} from "react" -import {GeoJSON, MapContainer, Popup, TileLayer, useMap} from "react-leaflet" - -import bbox from 'geojson-bbox' +import { + SelectionAffichage, + TableauParticipation, + CarteResultats, + HistogrammeVoix, CompositionHemicycle, GroupementParBloc, RetirerSeuil +} from "./includes/composants_elections" +import {TableauResultatsEuropeennes} from "./includes/composants_elections_europeennes" +import {calculerSieges, getNomZone, regrouperVoix} from "./utils" import 'leaflet/dist/leaflet.css' + highchartsItem(Highcharts) -function ResultatsTable({blocs, nuances, listes, resultats, siegesParListe}) { - const voix_listes = resultats?.voix_listes ?? {} - const listes_triees = listes.toSorted((l1, l2) => { - return (voix_listes[l2.numero] || 0) - (voix_listes[l1.numero] || 0) - }) - - return <> - - - - - Numéro - Liste - Nuance - Bloc - Voix - % Inscrit⋅es - % Exprimé⋅es - Sièges - - - - {listes_triees.map((liste) => ( - - ))} - -
-
- -} - - -function ListeRow({liste, voix, resultats, siegesParListe, blocs, nuances}) { - const bloc = blocs.filter(bloc => bloc.nom === liste.bloc)[0] - const nuance = nuances.filter(nuance => nuance.code === liste.nuance)[0] - - return - {liste.numero} - {liste.nom} - - {liste.nuance} - - {liste.bloc} - {voix} - {(100 * voix / resultats.inscrits).toFixed(2)} % - {(100 * voix / resultats.exprimes).toFixed(2)} % - {siegesParListe[liste.numero]} - -} - - -function ParticipationTable({resultats}) { - return <> - - - - - - Nombre - % Inscrit⋅es - % Votant⋅es - - - - - Inscrit⋅es - {resultats.inscrits} - - - - - Abstention - {resultats.abstentions} - {(100 * resultats.abstentions / resultats.inscrits).toFixed(2)} % - - - - Votant⋅es - {resultats.votants} - {(100 * resultats.votants / resultats.inscrits).toFixed(2)} % - - - - Blancs - {resultats.blancs} - {(100 * resultats.blancs / resultats.inscrits).toFixed(2)} % - {(100 * resultats.blancs / resultats.votants).toFixed(2)} % - - - Nuls - {resultats.nuls} - {(100 * resultats.nuls / resultats.inscrits).toFixed(2)} % - {(100 * resultats.nuls / resultats.votants).toFixed(2)} % - - - Exprimés - {resultats.exprimes} - {(100 * resultats.exprimes / resultats.inscrits).toFixed(2)} % - {(100 * resultats.exprimes / resultats.votants).toFixed(2)} % - - -
-
- -} - -function ZoneGeoJSON({typeResultats, resultatsZone, typeZone, listes, blocs, nuances, grouperParBloc = false}) { - const [idZone, nomZone] = useMemo(() => { - if (!resultatsZone[typeZone]) - return ["", ""] - - if (typeZone === "region" || typeZone === "departement" || typeZone === "commune") - return [resultatsZone[typeZone].code_insee, resultatsZone[typeZone].nom] - else if (typeZone === "circonscription") - return [resultatsZone.circonscription.id, `Circonscription ${resultatsZone.circonscription.id}`] - else if (typeZone === "bureau_vote") - return [resultatsZone.bureau_vote.id, resultatsZone.bureau_vote.libelle] - else - return ["", ""] - }, [typeZone, resultatsZone]) - - const voix_listes = resultatsZone?.voix_listes ?? {} - const listes_triees = listes.toSorted((l1, l2) => { - return (voix_listes[l2.numero] || 0) - (voix_listes[l1.numero] || 0) - }) - - const voixParBloc = {} - const voixParNuance = {} - for (let bloc of blocs) { - voixParBloc[bloc.nom] = 0 - } - for (let nuance of nuances) { - voixParNuance[nuance.code] = 0 - } - - for (let liste of listes) { - voixParBloc[liste.bloc] += resultatsZone.voix_listes[liste.numero] || 0 - voixParNuance[liste.nuance] += resultatsZone.voix_listes[liste.numero] || 0 - } - - let couleur = 'grey' - if (grouperParBloc) { - let maxVoix = 0 - for (let bloc of blocs) { - if (voixParBloc[bloc.nom] > maxVoix) { - maxVoix = voixParBloc[bloc.nom] - couleur = bloc.couleur - } - } - } - else { - let maxVoix = 0 - for (let nuance of nuances) { - if (voixParNuance[nuance.code] > maxVoix) { - maxVoix = voixParNuance[nuance.code] - couleur = nuance.couleur - } - } - } - - return - - {nomZone} - - - -} - -function ContenuCarte({typeResultats, resultats, typeZone, listes, blocs, nuances, grouperParBloc = false}) { - const map = useMap() - const [resultatsZones, setResultatsZones] = useState([]) - - const zones = useMemo(() => { - const data = resultats[typeResultats] - if (!data) - return [] - - if (typeZone === "region") - return data?.regions ?? [] - else if (typeZone === "departement") - return data?.departements ?? [] - else if (typeZone === "circonscription") - return data?.circonscriptions ?? [] - else if (typeZone === "commune") - return data?.communes ?? [] - else if (typeZone === "bureau_vote") { - if (typeResultats === "bureau_vote") - return data ? [data.id] : [] - else - return data?.bureaux_vote ?? [] - } - else - return [] - }, [typeResultats, resultats, typeZone]) - - useEffect(() => { - if (typeResultats === "france") - return - - const geometry = resultats.geometry - if (geometry) { - // On centre la carte sur la zone - const geometry_bbox = bbox(geometry) - map.fitBounds([[geometry_bbox[1], geometry_bbox[0]], [geometry_bbox[3], geometry_bbox[2]]]) - } - }, [typeResultats, resultats, map]) - - useEffect(() => { - if (!zones) - return - - setResultatsZones(resultatsZones => []) - - zones.forEach(zone_id => { - fetch(`/data/resultats/europeennes2024/${typeZone}/${zone_id}.json`).then(response => response.json()) - .then(resultatsZone => setResultatsZones(resultatsZones => [...resultatsZones, resultatsZone])) - }) - }, [typeZone, zones, resultats]) - - function getZoneIdentifier(typeZone, zone) { - if (typeZone === "region" || typeZone === "departement" || typeZone === "commune") - return zone.code_insee - else if (typeZone === "circonscription" || typeZone === "bureau_vote") - return zone.id - else - return "" - } - - return <> - {resultatsZones.filter(resultatsZone => resultatsZone.geometry['type']).map(resultatsZone => - )} - -} - -function Carte({typeResultats, resultats, typeZone, listes, blocs, nuances, grouperParBloc = false}) { - const center = typeResultats === "france" ? [46.603354, 1.888334] : [0, 0] - - return <> - - - - - -} - -function SelectionAffichage({typeResultats, typeZone, setTypeZone}) { - const items = useMemo(() => { - const items = [] - if (typeResultats === "france") { - setTypeZone("region") - items.push(Région) - } - - if (typeResultats === "france" || typeResultats === "region") { - if (typeResultats !== "france") - setTypeZone("departement") - items.push(Département) - } - - if (typeResultats === "france" || typeResultats === "region" || typeResultats === "departement") { - if (typeResultats !== "france" && typeResultats !== "region") - setTypeZone("circonscription") - items.push(Circonscription) - } - - if (typeResultats === "departement") { - items.push(Communes) - } - - if (typeResultats === "circonscription" || typeResultats === "commune" || typeResultats === "bureau_vote") { - setTypeZone("bureau_vote") - items.push(Bureau de vote) - } - - return items - }, [typeResultats, setTypeZone]) - - return -} - export default function Election2024() { const {typeResultats, zoneId} = useParams() - const [zoneName, setZoneName] = useState("France") const [grouperParBloc, setGrouperParBloc] = useState(false) const [retirerSeuil, setRetirerSeuil] = useState(false) const [blocs, setBlocs] = useState([]) const [nuances, setNuances] = useState([]) const [listes, setListes] = useState([]) const [resultats, setResultats] = useState([]) - const [siegesParListe, setSiegesParListe] = useState({}) - const [voixParBloc, setVoixParBloc] = useState([]) - const [voixParNuance, setVoixParNuance] = useState([]) - const [siegesParBloc, setSiegesParBloc] = useState([]) - const [siegesParNuance, setSiegesParNuance] = useState([]) - const [categoriesVoix, setCategoriesVoix] = useState([]) - const [dataVoix, setDataVoix] = useState([]) - const [dataSieges, setDataSieges] = useState([]) const [typeZone, setTypeZone] = useState("region") useEffect(() => { - fetch("/data/resultats/europeennes2024/blocs.json").then(response => response.json()) + fetch("/data/resultats/europeennes/2024/blocs.json").then(response => response.json()) .then(data => setBlocs(data)) - fetch("/data/resultats/europeennes2024/nuances.json").then(response => response.json()) + fetch("/data/resultats/europeennes/2024/nuances.json").then(response => response.json()) .then(data => setNuances(data)) - fetch("/data/resultats/europeennes2024/listes.json").then(response => response.json()) + fetch("/data/resultats/europeennes/2024/listes.json").then(response => response.json()) .then(data => setListes(data)) if (typeResultats === "france") { - fetch("/data/resultats/europeennes2024/france.json").then(response => response.json()) + fetch("/data/resultats/europeennes/2024/france.json").then(response => response.json()) .then(data => setResultats(data)) } else { - fetch(`/data/resultats/europeennes2024/${typeResultats}/${zoneId}.json`).then(response => response.json()) + fetch(`/data/resultats/europeennes/2024/${typeResultats}/${zoneId}.json`) + .then(response => response.json()) .then(data => setResultats(data)) } }, [typeResultats, zoneId]) - useEffect(() => { - if (!resultats || resultats.length === 0) - setZoneName("") - else if (typeResultats === "france") - setZoneName("France") - else if (typeResultats === "region") - setZoneName(`Région ${resultats.region.nom}`) - else if (typeResultats === "departement") - setZoneName(`Département ${resultats.departement.nom}`) - else if (typeResultats === "circonscription") - setZoneName(`Circonscription ${resultats.circonscription.id}`) - else if (typeResultats === "commune") - setZoneName(`Commune ${resultats.commune.nom}`) - else if (typeResultats === "bureau_vote") - setZoneName(resultats.bureau_vote.libelle) - }, [typeResultats, resultats]) + const nomZone = useMemo(() => getNomZone(typeResultats, resultats), + [typeResultats, resultats]) - useEffect(() => { - if (!resultats['voix_listes']) - return + const [voixParBloc, voixParNuance] = regrouperVoix(resultats.voix, listes, blocs, nuances) - const parBloc = {} - const parNuance = {} - for (let bloc of blocs) { - parBloc[bloc.nom] = 0 - } - for (let nuance of nuances) { - parNuance[nuance.code] = 0 - } - - for (let liste of listes) { - parBloc[liste.bloc] += resultats?.voix_listes[liste.numero] ?? 0 - parNuance[liste.nuance] += resultats?.voix_listes[liste.numero] ?? 0 - } - - setVoixParBloc(parBloc) - setVoixParNuance(parNuance) - }, [blocs, nuances, listes, resultats]) - - useEffect(() => { - 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]) - } - } - setCategoriesVoix(categories) - setDataVoix(data) - }, [voixParBloc, voixParNuance, blocs, nuances, grouperParBloc]) - - useEffect(() => { - if (!resultats['voix_listes']) - return - - const MAX_SIEGES = 81 - const sieges = {} - const listesElues = [] - let siegesAffectes = 0 - let totalVoix = resultats.exprimes - for (let liste of listes) { - const voix = resultats?.voix_listes[liste.numero] ?? 0 - if (voix / resultats.exprimes < 0.05 && !retirerSeuil) { - // Barre des 5 % non franchie - totalVoix -= voix - sieges[liste.numero] = 0 - } - else { - listesElues.push(liste) - } - } - - if (listesElues.length === 0) - return - - for (let liste of listesElues) { - const voix = resultats?.voix_listes[liste.numero] ?? 0 - sieges[liste.numero] = Math.floor(MAX_SIEGES * voix / totalVoix) - siegesAffectes += sieges[liste.numero] - } - - while (siegesAffectes < MAX_SIEGES) { - // Méthode de la plus forte moyenne pour affecter les sièges restants - let maxMoyenne = 0 - let listeElue = null - for (let liste of listesElues) { - if (sieges[liste.numero] < MAX_SIEGES) { - const voix = resultats?.voix_listes[liste.numero] ?? 0 - const moyenne = voix / (sieges[liste.numero] + 1) - if (moyenne > maxMoyenne) { - maxMoyenne = moyenne - listeElue = liste - } - } - } - sieges[listeElue.numero]++ - siegesAffectes++ - } - - setSiegesParListe(sieges) - }, [listes, resultats, retirerSeuil]) - - useEffect(() => { - const parBloc = {} - const parNuance = {} - for (let bloc of blocs) { - parBloc[bloc.nom] = 0 - } - for (let nuance of nuances) { - parNuance[nuance.code] = 0 - } - - for (let liste of listes) { - parBloc[liste.bloc] += siegesParListe[liste.numero] || 0 - parNuance[liste.nuance] += siegesParListe[liste.numero] || 0 - } - - setSiegesParBloc(parBloc) - setSiegesParNuance(parNuance) - }, [blocs, nuances, listes, siegesParListe]) - - useEffect(() => { - const data = [] - if (grouperParBloc) { - for (let bloc of blocs) { - data.push([bloc.nom, siegesParBloc[bloc.nom], bloc.couleur, bloc.nom]) - } - } - else { - for (let nuance of nuances) { - data.push([nuance.nom, siegesParNuance[nuance.code], nuance.couleur, nuance.nom]) - } - } - setDataSieges(data) - }, [blocs, nuances, siegesParBloc, siegesParNuance, grouperParBloc]) - - const compositonOptions = { - chart: { - type: 'item' - }, - title: { - text: 'Projection eurodéputé⋅es français⋅es 2024' - }, - legend: { - labelFormat: '{name} {y}' - }, - series: [{ - name: 'Nombre de sièges', - keys: ['name', 'y', 'color', 'label'], - data: dataSieges, - dataLabels: { - enabled: false, - format: '{point.label}' - }, - // Circular options - center: ['50%', '88%'], - size: '170%', - startAngle: -100, - endAngle: 100 - }] - } - - const scoreOptions = { - chart: { - type: 'column' - }, - title: { - text: `Résultats des élections européennes 2024 : ${zoneName}`, - }, - tooltip: { - formatter: function () { - return `${this.x} : ${this.y} voix (${(100 * this.y / resultats.exprimes).toFixed(2)} %)
` - } - }, - xAxis: { - categories: categoriesVoix, - }, - series: [{ - name: "Nombre de voix", - keys: ['name', 'y', 'color', 'label'], - data: dataVoix, - }] - } + const siegesParListe = calculerSieges(listes, resultats, retirerSeuil ? 0 : 0.05) + const [siegesParBloc, siegesParNuance] = regrouperVoix(siegesParListe, listes, blocs, nuances) return <> - - setGrouperParBloc(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} - label="Grouper par bloc plutôt que nuance politique" /> - - - setRetirerSeuil(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} - label="Retirer le seuil des 5 %" /> - - - - - - - } - label="Type d'affichage pour la carte" /> - - + + + + + + + + + + + + + + } diff --git a/nupes-elections-front/src/includes/composants_elections.js b/nupes-elections-front/src/includes/composants_elections.js new file mode 100644 index 0000000..d1a558c --- /dev/null +++ b/nupes-elections-front/src/includes/composants_elections.js @@ -0,0 +1,374 @@ +import {trierCandidats, regrouperVoix} 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" +import {useEffect, useMemo, useState} from "react" +import {GeoJSON, MapContainer, Popup, TileLayer, useMap} from "react-leaflet" +import bbox from "geojson-bbox" +import {FormControl, InputLabel, MenuItem, Select} from "@mui/material" +import * as Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; +import Switch from "@mui/material/Switch"; +import FormControlLabel from "@mui/material/FormControlLabel"; + + +export function HistogrammeVoix({titre, resultats, voixParNuance, voixParBloc, blocs, nuances, grouperParBloc}) { + 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]) + } + } + + return [categories, data] + }, [voixParBloc, voixParNuance, blocs, nuances, grouperParBloc]) + + const scoreOptions = { + chart: { + type: 'column' + }, + title: { + text: titre, + }, + tooltip: { + formatter: function () { + return `${this.x} : ${this.y} voix (${(100 * this.y / resultats.exprimes).toFixed(2)} %)
` + } + }, + xAxis: { + categories: categoriesVoix, + }, + series: [{ + name: "Nombre de voix", + keys: ['name', 'y', 'color', 'label'], + data: dataVoix, + }] + } + + return +} + +export function CompositionHemicycle({titre, blocs, nuances, siegesParBloc, siegesParNuance, grouperParBloc}) { + const dataSieges = useMemo(() => { + const data = [] + if (grouperParBloc) { + for (let bloc of blocs) { + data.push([bloc.nom, siegesParBloc[bloc.nom], bloc.couleur, bloc.nom]) + } + } + else { + for (let nuance of nuances) { + data.push([nuance.nom, siegesParNuance[nuance.code], nuance.couleur, nuance.nom]) + } + } + + return data + }, [blocs, nuances, siegesParBloc, siegesParNuance, grouperParBloc]) + + const compositonOptions = { + chart: { + type: 'item' + }, + title: { + text: titre, + }, + legend: { + labelFormat: '{name} {y}' + }, + series: [{ + name: 'Nombre de sièges', + keys: ['name', 'y', 'color', 'label'], + data: dataSieges, + dataLabels: { + enabled: false, + format: '{point.label}' + }, + // Circular options + center: ['50%', '88%'], + size: '170%', + startAngle: -100, + endAngle: 100 + }] + } + + return +} + +/** + * Tableau de participation de l'élection dans la zone concernée + * @param resultats + * @return {JSX.Element} + * @constructor + */ +export function TableauParticipation({resultats}) { + return <> + + + + + + Nombre + % Inscrit⋅es + % Votant⋅es + + + + + Inscrit⋅es + {resultats.inscrits} + + + + + Abstention + {resultats.abstentions} + {(100 * resultats.abstentions / resultats.inscrits).toFixed(2)} % + + + + Votant⋅es + {resultats.votants} + {(100 * resultats.votants / resultats.inscrits).toFixed(2)} % + + + + Blancs + {resultats.blancs} + {(100 * resultats.blancs / resultats.inscrits).toFixed(2)} % + {(100 * resultats.blancs / resultats.votants).toFixed(2)} % + + + Nuls + {resultats.nuls} + {(100 * resultats.nuls / resultats.inscrits).toFixed(2)} % + {(100 * resultats.nuls / resultats.votants).toFixed(2)} % + + + Exprimés + {resultats.exprimes} + {(100 * resultats.exprimes / resultats.inscrits).toFixed(2)} % + {(100 * resultats.exprimes / resultats.votants).toFixed(2)} % + + +
+
+ +} + +export function GroupementParBloc({grouperParBloc, setGrouperParBloc}) { + return setGrouperParBloc(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} + label="Grouper par bloc plutôt que nuance politique" /> +} + +export function RetirerSeuil({retirerSeuil, setRetirerSeuil}) { + return setRetirerSeuil(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} + label="Retirer le seuil des 5 %" /> +} + +export function SelectionAffichage({typeResultats, typeZone, setTypeZone}) { + const items = useMemo(() => { + const items = [] + if (typeResultats === "france") { + setTypeZone("region") + items.push(Région) + } + + if (typeResultats === "france" || typeResultats === "region") { + if (typeResultats !== "france") + setTypeZone("departement") + items.push(Département) + } + + if (typeResultats === "france" || typeResultats === "region" || typeResultats === "departement") { + if (typeResultats !== "france" && typeResultats !== "region") + setTypeZone("circonscription") + items.push(Circonscription) + } + + if (typeResultats === "departement") { + items.push(Communes) + } + + if (typeResultats === "circonscription" || typeResultats === "commune" || typeResultats === "bureau_vote") { + setTypeZone("bureau_vote") + items.push(Bureau de vote) + } + + return items + }, [typeResultats, setTypeZone]) + + return + + Zone à afficher + + + +} + + +function ZoneGeoJSON({typeElection, anneeElection, resultatsZone, typeZone, + candidats, blocs, nuances, grouperParBloc = false}) { + const [idZone, nomZone] = useMemo(() => { + if (!resultatsZone[typeZone]) + return ["", ""] + + if (typeZone === "region" || typeZone === "departement" || typeZone === "commune") + return [resultatsZone[typeZone].code_insee, resultatsZone[typeZone].nom] + else if (typeZone === "circonscription") + return [resultatsZone.circonscription.id, `Circonscription ${resultatsZone.circonscription.id}`] + else if (typeZone === "bureau_vote") + return [resultatsZone.bureau_vote.id, resultatsZone.bureau_vote.libelle] + else + return ["", ""] + }, [typeZone, resultatsZone]) + + const voixCandidats = useMemo(() => resultatsZone?.voix ?? {}, [resultatsZone]) + const candidatsTries = trierCandidats(candidats, voixCandidats) + + const [voixParBloc, voixParNuance] = regrouperVoix(voixCandidats, candidats, blocs, nuances) + + let couleur = 'grey' + if (grouperParBloc) { + let maxVoix = 0 + for (let bloc of blocs) { + if (voixParBloc[bloc.nom] > maxVoix) { + maxVoix = voixParBloc[bloc.nom] + couleur = bloc.couleur + } + } + } + else { + let maxVoix = 0 + for (let nuance of nuances) { + if (voixParNuance[nuance.code] > maxVoix) { + maxVoix = voixParNuance[nuance.code] + couleur = nuance.couleur + } + } + } + + return + + {nomZone} +
    + {candidatsTries.slice(0, 5).map(candidat => +
  • + {candidat.nom} : {voixCandidats[candidat.numero]} ({(100 * voixCandidats[candidat.numero] / resultatsZone.exprimes).toFixed(2)} %) +
  • )} +
+
+
+} + +function ContenuCarte({typeElection, anneeElection, typeResultats, resultats, + typeZone, candidats, blocs, nuances, grouperParBloc = false}) { + const map = useMap() + const [resultatsZones, setResultatsZones] = useState([]) + + const zones = useMemo(() => { + const data = resultats[typeResultats] + if (!data) + return [] + + if (typeZone === "region") + return data?.regions ?? [] + else if (typeZone === "departement") + return data?.departements ?? [] + else if (typeZone === "circonscription") + return data?.circonscriptions ?? [] + else if (typeZone === "commune") + return data?.communes ?? [] + else if (typeZone === "bureau_vote") { + if (typeResultats === "bureau_vote") + return data ? [data.id] : [] + else + return data?.bureaux_vote ?? [] + } + else + return [] + }, [typeResultats, resultats, typeZone]) + + useEffect(() => { + if (typeResultats === "france") + return + + const geometry = resultats.geometry + if (geometry) { + // On centre la carte sur la zone + const geometry_bbox = bbox(geometry) + map.fitBounds([[geometry_bbox[1], geometry_bbox[0]], [geometry_bbox[3], geometry_bbox[2]]]) + } + }, [typeResultats, resultats, map]) + + useEffect(() => { + if (!zones) + return + + setResultatsZones(resultatsZones => []) + + zones.forEach(zoneId => { + fetch(`/data/resultats/${typeElection}/${anneeElection}/${typeZone}/${zoneId}.json`) + .then(response => response.json()) + .then(resultatsZone => setResultatsZones(resultatsZones => [...resultatsZones, resultatsZone])) + }) + }, [typeElection, anneeElection, typeZone, zones, resultats]) + + function getZoneIdentifier(typeZone, zone) { + if (typeZone === "region" || typeZone === "departement" || typeZone === "commune") + return zone.code_insee + else if (typeZone === "circonscription" || typeZone === "bureau_vote") + return zone.id + else + return "" + } + + return <> + {resultatsZones.filter(resultatsZone => resultatsZone.geometry['type']).map(resultatsZone => + )} + +} + +export function CarteResultats({typeElection, anneeElection, typeResultats, resultats, + typeZone, candidats, blocs, nuances, grouperParBloc = false}) { + const center = typeResultats === "france" ? [46.603354, 1.888334] : [0, 0] + + return <> + + + + + +} diff --git a/nupes-elections-front/src/includes/composants_elections_europeennes.js b/nupes-elections-front/src/includes/composants_elections_europeennes.js new file mode 100644 index 0000000..06f703f --- /dev/null +++ b/nupes-elections-front/src/includes/composants_elections_europeennes.js @@ -0,0 +1,66 @@ +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" + +/** + * Composant pour le tableau des résultats des élections européennes par liste + * @param blocs + * @param nuances + * @param listes + * @param resultats + * @param siegesParListe + * @return {JSX.Element} + * @constructor + */ +export function TableauResultatsEuropeennes({blocs, nuances, listes, resultats, siegesParListe}) { + const voix_listes = resultats?.voix_listes ?? {} + const listes_triees = trierCandidats(listes, voix_listes) + + return <> + + + + + Numéro + Liste + Nuance + Bloc + Voix + % Inscrit⋅es + % Exprimé⋅es + Sièges + + + + {listes_triees.map((liste) => ( + + ))} + +
+
+ +} + +function LigneListe({liste, voix, resultats, siegesParListe, blocs, nuances}) { + const bloc = blocs.filter(bloc => bloc.nom === liste.bloc)[0] + const nuance = nuances.filter(nuance => nuance.code === liste.nuance)[0] + + return + {liste.numero} + {liste.nom} + + {liste.nuance} + + {liste.bloc} + {voix} + {(100 * voix / resultats.inscrits).toFixed(2)} % + {(100 * voix / resultats.exprimes).toFixed(2)} % + {siegesParListe[liste.numero]} + +} diff --git a/nupes-elections-front/src/utils.js b/nupes-elections-front/src/utils.js new file mode 100644 index 0000000..7d3f0aa --- /dev/null +++ b/nupes-elections-front/src/utils.js @@ -0,0 +1,96 @@ +export function getNomZone(typeResultats, resultats) { + if (!resultats || resultats.length === 0) + return "" + else if (typeResultats === "france") + return "France" + else if (typeResultats === "region") + return `Région ${resultats.region.nom}` + else if (typeResultats === "departement") + return `Département ${resultats.departement.nom}` + else if (typeResultats === "circonscription") + return `Circonscription ${resultats.circonscription.id}` + else if (typeResultats === "commune") + return `Commune ${resultats.commune.nom}` + else if (typeResultats === "bureau_vote") + return resultats.bureau_vote.libelle +} + +export function trierCandidats(candidats, voix_par_candidat) { + return candidats.toSorted((l1, l2) => { + return (voix_par_candidat[l2.numero] || 0) - (voix_par_candidat[l1.numero] || 0) + }) +} + +export function regrouperVoix(voixCandidats, candidats, blocs, nuances) { + if (!candidats || !voixCandidats || !blocs || !nuances + || candidats.length === 0 || blocs.length === 0 || nuances.length === 0) + return [{}, {}] + + const parBloc = {} + const parNuance = {} + + for (let bloc of blocs) { + parBloc[bloc.nom] = 0 + } + 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 + } + + return [parBloc, parNuance] +} + +export function calculerSieges(listes, resultats, seuil = 0.05) { + if (!resultats['voix']) + return {} + + const MAX_SIEGES = 81 + const sieges = {} + const listesElues = [] + let siegesAffectes = 0 + let totalVoix = resultats.exprimes + for (let liste of listes) { + const voix = resultats?.voix[liste.numero] ?? 0 + if (voix / resultats.exprimes < seuil) { + // Barre des 5 % non franchie + totalVoix -= voix + sieges[liste.numero] = 0 + } + else { + listesElues.push(liste) + } + } + + if (listesElues.length === 0) + return + + for (let liste of listesElues) { + const voix = resultats?.voix[liste.numero] ?? 0 + sieges[liste.numero] = Math.floor(MAX_SIEGES * voix / totalVoix) + siegesAffectes += sieges[liste.numero] + } + + while (siegesAffectes < MAX_SIEGES) { + // Méthode de la plus forte moyenne pour affecter les sièges restants + let maxMoyenne = 0 + let listeElue = null + for (let liste of listesElues) { + if (sieges[liste.numero] < MAX_SIEGES) { + const voix = resultats?.voix[liste.numero] ?? 0 + const moyenne = voix / (sieges[liste.numero] + 1) + if (moyenne > maxMoyenne) { + maxMoyenne = moyenne + listeElue = liste + } + } + } + sieges[listeElue.numero]++ + siegesAffectes++ + } + + return sieges + } diff --git a/nupes/scripts/export_resultats_2024.py b/nupes/scripts/export_resultats_2024.py index 8f91393..65f123d 100644 --- a/nupes/scripts/export_resultats_2024.py +++ b/nupes/scripts/export_resultats_2024.py @@ -22,7 +22,7 @@ def exporter_listes(engine: Engine, verbose: bool = False) -> None: bloc_json = {'id': bloc.id, 'nom': bloc.nom, 'couleur': bloc.couleur} blocs_json.append(bloc_json) - file = DATA_DIR / "resultats" / "europeennes2024" / "blocs.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "blocs.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -36,7 +36,7 @@ def exporter_listes(engine: Engine, verbose: bool = False) -> None: nuance_json = {'code': nuance.code, 'nom': nuance.nom, 'couleur': nuance.couleur} nuances_json.append(nuance_json) - file = DATA_DIR / "resultats" / "europeennes2024" / "nuances.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "nuances.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -57,7 +57,7 @@ def exporter_listes(engine: Engine, verbose: bool = False) -> None: 'bloc': liste.bloc.nom, 'candidats': candidats} listes_json.append(liste_json) - file = DATA_DIR / "resultats" / "europeennes2024" / "listes.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "listes.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -88,11 +88,11 @@ def exporter_resultats_france(engine: Engine, verbose: bool = False) -> None: } resultats_listes = {} - resultats_dict['voix_listes'] = resultats_listes + resultats_dict['voix'] = resultats_listes for voix_liste in resultats_france.voix_listes: resultats_listes[voix_liste.liste.numero] = voix_liste.voix - file = DATA_DIR / "resultats" / "europeennes2024" / "france.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "france.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -136,11 +136,11 @@ def exporter_resultats_regions(engine: Engine, verbose: bool = False) -> None: } resultats_listes = {} - resultats_dict['voix_listes'] = resultats_listes + resultats_dict['voix'] = resultats_listes for voix_liste in resultats_region.voix_listes: resultats_listes[voix_liste.liste.numero] = voix_liste.voix - file = DATA_DIR / "resultats" / "europeennes2024" / "region" / f"{region.code_insee}.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "region" / f"{region.code_insee}.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -149,7 +149,7 @@ def exporter_resultats_regions(engine: Engine, verbose: bool = False) -> None: session.commit() - regions_file = DATA_DIR / "resultats" / "europeennes2024" / "region" / "regions.json" + regions_file = DATA_DIR / "resultats" / "europeennes" / "2024" / "region" / "regions.json" if not regions_file.parent.is_dir(): regions_file.parent.mkdir(parents=True) @@ -191,11 +191,11 @@ def exporter_resultats_departements(engine: Engine, verbose: bool = False) -> No } resultats_listes = {} - resultats_dict['voix_listes'] = resultats_listes + resultats_dict['voix'] = resultats_listes for voix_liste in resultats_departement.voix_listes: resultats_listes[voix_liste.liste.numero] = voix_liste.voix - file = DATA_DIR / "resultats" / "europeennes2024" / "departement" / f"{departement.code_insee}.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "departement" / f"{departement.code_insee}.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -204,7 +204,7 @@ def exporter_resultats_departements(engine: Engine, verbose: bool = False) -> No session.commit() - departements_file = DATA_DIR / "resultats" / "europeennes2024" / "departement" / "departements.json" + departements_file = DATA_DIR / "resultats" / "europeennes" / "2024" / "departement" / "departements.json" if not departements_file.parent.is_dir(): departements_file.parent.mkdir(parents=True) @@ -245,11 +245,11 @@ def exporter_resultats_circonscriptions(engine: Engine, verbose: bool = False) - } resultats_listes = {} - resultats_dict['voix_listes'] = resultats_listes + resultats_dict['voix'] = resultats_listes for voix_liste in resultats_circonscription.voix_listes: resultats_listes[voix_liste.liste.numero] = voix_liste.voix - file = DATA_DIR / "resultats" / "europeennes2024" / "circonscription" / f"{circonscription.id}.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "circonscription" / f"{circonscription.id}.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -258,7 +258,8 @@ def exporter_resultats_circonscriptions(engine: Engine, verbose: bool = False) - session.commit() - circonscriptions_file = DATA_DIR / "resultats" / "europeennes2024" / "circonscription" / "circonscriptions.json" + circonscriptions_file = (DATA_DIR / "resultats" / "europeennes" / "2024" + / "circonscription" / "circonscriptions.json") if not circonscriptions_file.parent.is_dir(): circonscriptions_file.parent.mkdir(parents=True) @@ -299,11 +300,11 @@ def exporter_resultats_communes(engine: Engine, verbose: bool = False) -> None: } resultats_listes = {} - resultats_dict['voix_listes'] = resultats_listes + resultats_dict['voix'] = resultats_listes for voix_liste in resultats_commune.voix_listes: resultats_listes[voix_liste.liste.numero] = voix_liste.voix - file = DATA_DIR / "resultats" / "europeennes2024" / "commune" / f"{commune.code_insee}.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "commune" / f"{commune.code_insee}.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -312,7 +313,7 @@ def exporter_resultats_communes(engine: Engine, verbose: bool = False) -> None: session.commit() - communes_file = DATA_DIR / "resultats" / "europeennes2024" / "commune" / "communes.json" + communes_file = DATA_DIR / "resultats" / "europeennes" / "2024" / "commune" / "communes.json" if not communes_file.parent.is_dir(): communes_file.parent.mkdir(parents=True) @@ -354,11 +355,11 @@ def exporter_resultats_bureaux_vote(engine: Engine, verbose: bool = False) -> No } resultats_listes = {} - resultats_dict['voix_listes'] = resultats_listes + resultats_dict['voix'] = resultats_listes for voix_liste in resultats_bureau_vote.voix_listes: resultats_listes[voix_liste.liste.numero] = voix_liste.voix - file = DATA_DIR / "resultats" / "europeennes2024" / "bureau_vote" / f"{bureau_vote.id}.json" + file = DATA_DIR / "resultats" / "europeennes" / "2024" / "bureau_vote" / f"{bureau_vote.id}.json" if not file.parent.is_dir(): file.parent.mkdir(parents=True) @@ -367,7 +368,7 @@ def exporter_resultats_bureaux_vote(engine: Engine, verbose: bool = False) -> No session.commit() - bureaux_vote_file = DATA_DIR / "resultats" / "europeennes2024" / "bureau_vote" / "bureaux_vote.json" + bureaux_vote_file = DATA_DIR / "resultats" / "europeennes" / "2024" / "bureau_vote" / "bureaux_vote.json" if not bureaux_vote_file.parent.is_dir(): bureaux_vote_file.parent.mkdir(parents=True)