Restructuration
This commit is contained in:
		| @@ -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 <> | ||||
|     <TableContainer component={Paper}> | ||||
|       <Table sx={{ minWidth: 650 }} aria-label="simple table"> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|             <TableCell>Numéro</TableCell> | ||||
|             <TableCell>Liste</TableCell> | ||||
|             <TableCell colSpan={2}>Nuance</TableCell> | ||||
|             <TableCell colSpan={2}>Bloc</TableCell> | ||||
|             <TableCell>Voix</TableCell> | ||||
|             <TableCell>% Inscrit⋅es</TableCell> | ||||
|             <TableCell>% Exprimé⋅es</TableCell> | ||||
|             <TableCell>Sièges</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           {listes_triees.map((liste) => ( | ||||
|             <ListeRow key={liste.numero} liste={liste} voix={voix_listes[liste.numero] || 0} resultats={resultats} siegesParListe={siegesParListe} blocs={blocs} nuances={nuances} /> | ||||
|           ))} | ||||
|         </TableBody> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   </> | ||||
| } | ||||
|  | ||||
|  | ||||
| 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 <TableRow key={liste.numero}> | ||||
|     <TableCell>{liste.numero}</TableCell> | ||||
|     <TableCell>{liste.nom}</TableCell> | ||||
|     <TableCell sx={{backgroundColor: nuance.couleur, padding: "0.2em"}}></TableCell> | ||||
|     <TableCell>{liste.nuance}</TableCell> | ||||
|     <TableCell sx={{backgroundColor: bloc.couleur, padding: "0.2em"}}></TableCell> | ||||
|     <TableCell>{liste.bloc}</TableCell> | ||||
|     <TableCell>{voix}</TableCell> | ||||
|     <TableCell>{(100 * voix / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|     <TableCell>{(100 * voix / resultats.exprimes).toFixed(2)} %</TableCell> | ||||
|     <TableCell>{siegesParListe[liste.numero]}</TableCell> | ||||
|   </TableRow> | ||||
| } | ||||
|  | ||||
|  | ||||
| function ParticipationTable({resultats}) { | ||||
|   return <> | ||||
|     <TableContainer component={Paper}> | ||||
|       <Table sx={{ minWidth: 650 }} aria-label="simple table"> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell>Nombre</TableCell> | ||||
|             <TableCell>% Inscrit⋅es</TableCell> | ||||
|             <TableCell>% Votant⋅es</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           <TableRow key={"Inscrit⋅es"}> | ||||
|             <TableCell>Inscrit⋅es</TableCell> | ||||
|             <TableCell>{resultats.inscrits}</TableCell> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Abstentions"}> | ||||
|             <TableCell>Abstention</TableCell> | ||||
|             <TableCell>{resultats.abstentions}</TableCell> | ||||
|             <TableCell>{(100 * resultats.abstentions / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Votant⋅es"}> | ||||
|             <TableCell>Votant⋅es</TableCell> | ||||
|             <TableCell>{resultats.votants}</TableCell> | ||||
|             <TableCell>{(100 * resultats.votants / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Blancs"}> | ||||
|             <TableCell>Blancs</TableCell> | ||||
|             <TableCell>{resultats.blancs}</TableCell> | ||||
|             <TableCell>{(100 * resultats.blancs / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell>{(100 * resultats.blancs / resultats.votants).toFixed(2)} %</TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Nuls"}> | ||||
|             <TableCell>Nuls</TableCell> | ||||
|             <TableCell>{resultats.nuls}</TableCell> | ||||
|             <TableCell>{(100 * resultats.nuls / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell>{(100 * resultats.nuls / resultats.votants).toFixed(2)} %</TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Exprimés"}> | ||||
|             <TableCell>Exprimés</TableCell> | ||||
|             <TableCell>{resultats.exprimes}</TableCell> | ||||
|             <TableCell>{(100 * resultats.exprimes / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell>{(100 * resultats.exprimes / resultats.votants).toFixed(2)} %</TableCell> | ||||
|           </TableRow> | ||||
|         </TableBody> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   </> | ||||
| } | ||||
|  | ||||
| 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 <GeoJSON | ||||
|     data={{'type': "Feature", 'geometry': resultatsZone.geometry}} | ||||
|     style={{fillColor: couleur, fillOpacity: 0.5, color: 'white', weight: 1}}> | ||||
|     <Popup> | ||||
|       <strong><a href={`/elections/europeennes/2024/${typeZone}/${idZone}/`}>{nomZone}</a></strong> | ||||
|       <ul> | ||||
|         {listes_triees.slice(0, 5).map(liste => | ||||
|           <li key={liste.numero}>{liste.nom} : {voix_listes[liste.numero]} ({(100 * voix_listes[liste.numero] / resultatsZone.exprimes).toFixed(2)} %)</li>)} | ||||
|       </ul> | ||||
|     </Popup> | ||||
|   </GeoJSON> | ||||
| } | ||||
|  | ||||
| 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 => | ||||
|       <ZoneGeoJSON key={getZoneIdentifier(resultatsZone[typeZone])} | ||||
|                    typeResultats={typeResultats} resultatsZone={resultatsZone} typeZone={typeZone} listes={listes} | ||||
|                    blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc}/>)} | ||||
|   </> | ||||
| } | ||||
|  | ||||
| function Carte({typeResultats, resultats, typeZone, listes, blocs, nuances, grouperParBloc = false}) { | ||||
|   const center = typeResultats === "france" ? [46.603354, 1.888334] : [0, 0] | ||||
|  | ||||
|   return <> | ||||
|     <MapContainer center={center} zoom={6} style={{height: "90vh"}}> | ||||
|       <TileLayer | ||||
|         attribution='© Les contributeur⋅rices <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' | ||||
|         url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" | ||||
|       /> | ||||
|       <ContenuCarte typeResultats={typeResultats} resultats={resultats} typeZone={typeZone} listes={listes} blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc} /> | ||||
|   </MapContainer> | ||||
|   </> | ||||
| } | ||||
|  | ||||
| function SelectionAffichage({typeResultats, typeZone, setTypeZone}) { | ||||
|   const items = useMemo(() => { | ||||
|     const items = [] | ||||
|     if (typeResultats === "france") { | ||||
|       setTypeZone("region") | ||||
|       items.push(<MenuItem value="region">Région</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "france" || typeResultats === "region") { | ||||
|       if (typeResultats !== "france") | ||||
|         setTypeZone("departement") | ||||
|       items.push(<MenuItem value="departement">Département</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "france" || typeResultats === "region" || typeResultats === "departement") { | ||||
|       if (typeResultats !== "france" && typeResultats !== "region") | ||||
|         setTypeZone("circonscription") | ||||
|       items.push(<MenuItem value="circonscription">Circonscription</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "departement") { | ||||
|       items.push(<MenuItem value="commune">Communes</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "circonscription" || typeResultats === "commune" || typeResultats === "bureau_vote") { | ||||
|       setTypeZone("bureau_vote") | ||||
|       items.push(<MenuItem value="bureau_vote">Bureau de vote</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     return items | ||||
|   }, [typeResultats, setTypeZone]) | ||||
|  | ||||
|   return <Select value={typeZone} onChange={event => setTypeZone(event.target.value)}> | ||||
|     {items} | ||||
|   </Select> | ||||
| } | ||||
|  | ||||
| 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} <span style="opacity: 0.4">{y}</span>' | ||||
|     }, | ||||
|     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 `<span>${this.x}</span> : <strong>${this.y}</strong> voix (${(100 * this.y / resultats.exprimes).toFixed(2)} %)<br>` | ||||
|       } | ||||
|     }, | ||||
|     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 <> | ||||
|     <FormGroup> | ||||
|       <FormControlLabel control={<Switch checked={grouperParBloc} onChange={event => setGrouperParBloc(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} | ||||
|                         label="Grouper par bloc plutôt que nuance politique" /> | ||||
|     </FormGroup> | ||||
|     <FormGroup> | ||||
|       <FormControlLabel control={<Switch checked={retirerSeuil} onChange={event => setRetirerSeuil(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} | ||||
|                         label="Retirer le seuil des 5 %" /> | ||||
|     </FormGroup> | ||||
|     <HighchartsReact | ||||
|       id="composition-eurodeputees" | ||||
|       highcharts={Highcharts} | ||||
|       options={scoreOptions} | ||||
|     /> | ||||
|     <HighchartsReact | ||||
|       id="composition-eurodeputees" | ||||
|       highcharts={Highcharts} | ||||
|       options={compositonOptions} | ||||
|     /> | ||||
|     <ResultatsTable blocs={blocs} nuances={nuances} listes={listes} resultats={resultats} siegesParListe={siegesParListe} /> | ||||
|     <ParticipationTable resultats={resultats} /> | ||||
|     <FormGroup> | ||||
|       <FormControlLabel control={<SelectionAffichage typeResultats={typeResultats} typeZone={typeZone} setTypeZone={setTypeZone} />} | ||||
|                         label="Type d'affichage pour la carte" /> | ||||
|     </FormGroup> | ||||
|     <Carte typeResultats={typeResultats} resultats={resultats} typeZone={typeZone} listes={listes} blocs={blocs} nuances={nuances} voixParBloc={voixParBloc} voixParNuance={voixParNuance} grouperParBloc={grouperParBloc} /> | ||||
|     <AppBar position="sticky"> | ||||
|       <Toolbar> | ||||
|         <GroupementParBloc grouperParBloc={grouperParBloc} setGrouperParBloc={setGrouperParBloc} /> | ||||
|         <RetirerSeuil retirerSeuil={retirerSeuil} setRetirerSeuil={setRetirerSeuil} /> | ||||
|         <SelectionAffichage typeResultats={typeResultats} typeZone={typeZone} setTypeZone={setTypeZone} /> | ||||
|       </Toolbar> | ||||
|     </AppBar> | ||||
|     <Container> | ||||
|       <HistogrammeVoix titre={`Résultats des élections européennes 2024 : ${nomZone}`} | ||||
|                        resultats={resultats} voixParNuance={voixParNuance} voixParBloc={voixParBloc} | ||||
|                        blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc} /> | ||||
|       <CompositionHemicycle titre={`Eurodéputé⋅es français⋅es dans l'hémicycle européen 2024 : ${nomZone}`} | ||||
|                             blocs={blocs} nuances={nuances} siegesParBloc={siegesParBloc} siegesParNuance={siegesParNuance} | ||||
|                             grouperParBloc={grouperParBloc} /> | ||||
|       <TableauResultatsEuropeennes blocs={blocs} nuances={nuances} listes={listes} resultats={resultats} siegesParListe={siegesParListe} /> | ||||
|       <TableauParticipation resultats={resultats} /> | ||||
|       <CarteResultats typeElection={"europeennes"} anneeElection={"2024"} | ||||
|                       typeResultats={typeResultats} resultats={resultats} typeZone={typeZone} | ||||
|                       candidats={listes} blocs={blocs} nuances={nuances} | ||||
|                       voixParBloc={voixParBloc} voixParNuance={voixParNuance} grouperParBloc={grouperParBloc} /> | ||||
|     </Container> | ||||
|   </> | ||||
| } | ||||
|   | ||||
							
								
								
									
										374
									
								
								nupes-elections-front/src/includes/composants_elections.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								nupes-elections-front/src/includes/composants_elections.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 `<span>${this.x}</span> : <strong>${this.y}</strong> voix (${(100 * this.y / resultats.exprimes).toFixed(2)} %)<br>` | ||||
|       } | ||||
|     }, | ||||
|     xAxis: { | ||||
|       categories: categoriesVoix, | ||||
|     }, | ||||
|     series: [{ | ||||
|       name: "Nombre de voix", | ||||
|       keys: ['name', 'y', 'color', 'label'], | ||||
|       data: dataVoix, | ||||
|     }] | ||||
|   } | ||||
|  | ||||
|   return <HighchartsReact | ||||
|     highcharts={Highcharts} | ||||
|     options={scoreOptions} | ||||
|   /> | ||||
| } | ||||
|  | ||||
| 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} <span style="opacity: 0.4">{y}</span>' | ||||
|     }, | ||||
|     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 <HighchartsReact | ||||
|     highcharts={Highcharts} | ||||
|     options={compositonOptions} | ||||
|   /> | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Tableau de participation de l'élection dans la zone concernée | ||||
|  * @param resultats | ||||
|  * @return {JSX.Element} | ||||
|  * @constructor | ||||
|  */ | ||||
| export function TableauParticipation({resultats}) { | ||||
|   return <> | ||||
|     <TableContainer component={Paper}> | ||||
|       <Table sx={{ minWidth: 650 }} aria-label="simple table"> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell>Nombre</TableCell> | ||||
|             <TableCell>% Inscrit⋅es</TableCell> | ||||
|             <TableCell>% Votant⋅es</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           <TableRow key={"Inscrit⋅es"}> | ||||
|             <TableCell>Inscrit⋅es</TableCell> | ||||
|             <TableCell>{resultats.inscrits}</TableCell> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Abstentions"}> | ||||
|             <TableCell>Abstention</TableCell> | ||||
|             <TableCell>{resultats.abstentions}</TableCell> | ||||
|             <TableCell>{(100 * resultats.abstentions / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Votant⋅es"}> | ||||
|             <TableCell>Votant⋅es</TableCell> | ||||
|             <TableCell>{resultats.votants}</TableCell> | ||||
|             <TableCell>{(100 * resultats.votants / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Blancs"}> | ||||
|             <TableCell>Blancs</TableCell> | ||||
|             <TableCell>{resultats.blancs}</TableCell> | ||||
|             <TableCell>{(100 * resultats.blancs / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell>{(100 * resultats.blancs / resultats.votants).toFixed(2)} %</TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Nuls"}> | ||||
|             <TableCell>Nuls</TableCell> | ||||
|             <TableCell>{resultats.nuls}</TableCell> | ||||
|             <TableCell>{(100 * resultats.nuls / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell>{(100 * resultats.nuls / resultats.votants).toFixed(2)} %</TableCell> | ||||
|           </TableRow> | ||||
|           <TableRow key={"Exprimés"}> | ||||
|             <TableCell>Exprimés</TableCell> | ||||
|             <TableCell>{resultats.exprimes}</TableCell> | ||||
|             <TableCell>{(100 * resultats.exprimes / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|             <TableCell>{(100 * resultats.exprimes / resultats.votants).toFixed(2)} %</TableCell> | ||||
|           </TableRow> | ||||
|         </TableBody> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   </> | ||||
| } | ||||
|  | ||||
| export function GroupementParBloc({grouperParBloc, setGrouperParBloc}) { | ||||
|   return <FormControlLabel control={<Switch checked={grouperParBloc} onChange={event => setGrouperParBloc(event.target.checked)} inputProps={{ 'aria-label': 'controlled' }} />} | ||||
|                           label="Grouper par bloc plutôt que nuance politique" /> | ||||
| } | ||||
|  | ||||
| export function RetirerSeuil({retirerSeuil, setRetirerSeuil}) { | ||||
|   return <FormControlLabel control={<Switch checked={retirerSeuil} onChange={event => 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(<MenuItem value="region">Région</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "france" || typeResultats === "region") { | ||||
|       if (typeResultats !== "france") | ||||
|         setTypeZone("departement") | ||||
|       items.push(<MenuItem value="departement">Département</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "france" || typeResultats === "region" || typeResultats === "departement") { | ||||
|       if (typeResultats !== "france" && typeResultats !== "region") | ||||
|         setTypeZone("circonscription") | ||||
|       items.push(<MenuItem value="circonscription">Circonscription</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "departement") { | ||||
|       items.push(<MenuItem value="commune">Communes</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     if (typeResultats === "circonscription" || typeResultats === "commune" || typeResultats === "bureau_vote") { | ||||
|       setTypeZone("bureau_vote") | ||||
|       items.push(<MenuItem value="bureau_vote">Bureau de vote</MenuItem>) | ||||
|     } | ||||
|  | ||||
|     return items | ||||
|   }, [typeResultats, setTypeZone]) | ||||
|  | ||||
|   return <FormControl> | ||||
|     <InputLabel> | ||||
|       Zone à afficher | ||||
|   </InputLabel> | ||||
|     <Select value={typeZone} | ||||
|             onChange={event => setTypeZone(event.target.value)} | ||||
|             label="Zone à afficher"> | ||||
|       {items} | ||||
|     </Select> | ||||
|   </FormControl> | ||||
| } | ||||
|  | ||||
|  | ||||
| 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 <GeoJSON | ||||
|     data={{'type': "Feature", 'geometry': resultatsZone.geometry}} | ||||
|     style={{fillColor: couleur, fillOpacity: 0.5, color: 'white', weight: 1}}> | ||||
|     <Popup> | ||||
|       <strong><a href={`/elections/${typeElection}/${anneeElection}/${typeZone}/${idZone}/`}>{nomZone}</a></strong> | ||||
|       <ul> | ||||
|         {candidatsTries.slice(0, 5).map(candidat => | ||||
|           <li key={candidat.numero}> | ||||
|             {candidat.nom} : {voixCandidats[candidat.numero]} ({(100 * voixCandidats[candidat.numero] / resultatsZone.exprimes).toFixed(2)} %) | ||||
|           </li>)} | ||||
|       </ul> | ||||
|     </Popup> | ||||
|   </GeoJSON> | ||||
| } | ||||
|  | ||||
| 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 => | ||||
|       <ZoneGeoJSON key={getZoneIdentifier(resultatsZone[typeZone])} | ||||
|                    typeElection={typeElection} anneeElection={anneeElection} | ||||
|                    resultatsZone={resultatsZone} typeZone={typeZone} candidats={candidats} | ||||
|                    blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc}/>)} | ||||
|   </> | ||||
| } | ||||
|  | ||||
| export function CarteResultats({typeElection, anneeElection, typeResultats, resultats, | ||||
|                                  typeZone, candidats, blocs, nuances, grouperParBloc = false}) { | ||||
|   const center = typeResultats === "france" ? [46.603354, 1.888334] : [0, 0] | ||||
|  | ||||
|   return <> | ||||
|     <MapContainer center={center} zoom={6} style={{height: "90vh"}}> | ||||
|       <TileLayer | ||||
|         attribution='© Les contributeur⋅rices <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' | ||||
|         url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" | ||||
|       /> | ||||
|       <ContenuCarte typeElection={typeElection} anneeElection={anneeElection} typeResultats={typeResultats} | ||||
|                     resultats={resultats} typeZone={typeZone} candidats={candidats} | ||||
|                     blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc} /> | ||||
|   </MapContainer> | ||||
|   </> | ||||
| } | ||||
| @@ -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 <> | ||||
|     <TableContainer component={Paper}> | ||||
|       <Table sx={{ minWidth: 650 }} aria-label="simple table"> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|             <TableCell>Numéro</TableCell> | ||||
|             <TableCell>Liste</TableCell> | ||||
|             <TableCell colSpan={2}>Nuance</TableCell> | ||||
|             <TableCell colSpan={2}>Bloc</TableCell> | ||||
|             <TableCell>Voix</TableCell> | ||||
|             <TableCell>% Inscrit⋅es</TableCell> | ||||
|             <TableCell>% Exprimé⋅es</TableCell> | ||||
|             <TableCell>Sièges</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           {listes_triees.map((liste) => ( | ||||
|             <LigneListe key={liste.numero} liste={liste} voix={voix_listes[liste.numero] || 0} | ||||
|                         resultats={resultats} siegesParListe={siegesParListe} blocs={blocs} nuances={nuances} /> | ||||
|           ))} | ||||
|         </TableBody> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   </> | ||||
| } | ||||
|  | ||||
| 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 <TableRow key={liste.numero}> | ||||
|     <TableCell>{liste.numero}</TableCell> | ||||
|     <TableCell>{liste.nom}</TableCell> | ||||
|     <TableCell sx={{backgroundColor: nuance.couleur, padding: "0.2em"}}></TableCell> | ||||
|     <TableCell>{liste.nuance}</TableCell> | ||||
|     <TableCell sx={{backgroundColor: bloc.couleur, padding: "0.2em"}}></TableCell> | ||||
|     <TableCell>{liste.bloc}</TableCell> | ||||
|     <TableCell>{voix}</TableCell> | ||||
|     <TableCell>{(100 * voix / resultats.inscrits).toFixed(2)} %</TableCell> | ||||
|     <TableCell>{(100 * voix / resultats.exprimes).toFixed(2)} %</TableCell> | ||||
|     <TableCell>{siegesParListe[liste.numero]}</TableCell> | ||||
|   </TableRow> | ||||
| } | ||||
							
								
								
									
										96
									
								
								nupes-elections-front/src/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								nupes-elections-front/src/utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|   } | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user