464 lines
16 KiB
JavaScript
464 lines
16 KiB
JavaScript
import {useParams} from "react-router-dom"
|
|
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 * as Highcharts from 'highcharts'
|
|
import highchartsItem from 'highcharts/modules/item-series'
|
|
import HighchartsReact from 'highcharts-react-official'
|
|
import {useEffect, useState} from "react"
|
|
import {GeoJSON, MapContainer, Popup, TileLayer, useMap} from "react-leaflet"
|
|
|
|
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 RegionGeoJSON({resultats_region, listes, blocs, nuances, grouperParBloc = false}) {
|
|
const voix_listes = resultats_region?.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] += resultats_region.voix_listes[liste.numero] || 0
|
|
voixParNuance[liste.nuance] += resultats_region.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
|
|
key={resultats_region.region.code_insee}
|
|
data={{'type': "Feature", 'properties': resultats_region, 'geometry': resultats_region.geometry}}
|
|
style={{fillColor: couleur, fillOpacity: 0.5, color: 'white'}}>
|
|
<Popup>
|
|
<strong><a href={`/elections/europeennes/2024/region/${resultats_region.region.code_insee}/`}>{resultats_region.region.nom}</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] / resultats_region.exprimes).toFixed(2)} %)</li>)}
|
|
</ul>
|
|
</Popup>
|
|
</GeoJSON>
|
|
}
|
|
|
|
function ContenuCarte({typeResultats, resultats, listes, blocs, nuances, grouperParBloc = false}) {
|
|
const [regions, setRegions] = useState([])
|
|
|
|
useEffect(() => {
|
|
if (!resultats || !resultats.france || !resultats.france.regions)
|
|
return
|
|
|
|
setRegions(regions => [])
|
|
|
|
resultats.france.regions.forEach(region_code => {
|
|
fetch(`/data/resultats/europeennes2024/regions/${region_code}.json`).then(response => response.json())
|
|
.then(region => setRegions(regions => [...regions, region]))
|
|
})
|
|
}, [typeResultats, resultats])
|
|
|
|
const map = useMap()
|
|
|
|
return <>
|
|
{regions.map(region => <RegionGeoJSON key={region.region.code_insee} resultats_region={region} listes={listes} blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc} />)}
|
|
</>
|
|
}
|
|
|
|
function Carte({typeResultats, resultats, listes, blocs, nuances, grouperParBloc = false}) {
|
|
const center = [46.603354, 1.888334]
|
|
|
|
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} listes={listes} blocs={blocs} nuances={nuances} grouperParBloc={grouperParBloc} />
|
|
</MapContainer>
|
|
</>
|
|
}
|
|
|
|
export default function Election2024({typeResultats = "france"}) {
|
|
const {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([])
|
|
|
|
useEffect(() => {
|
|
fetch("/data/resultats/europeennes2024/blocs.json").then(response => response.json())
|
|
.then(data => setBlocs(data))
|
|
fetch("/data/resultats/europeennes2024/nuances.json").then(response => response.json())
|
|
.then(data => setNuances(data))
|
|
fetch("/data/resultats/europeennes2024/listes.json").then(response => response.json())
|
|
.then(data => setListes(data))
|
|
|
|
if (typeResultats === "france") {
|
|
fetch("/data/resultats/europeennes2024/france.json").then(response => response.json())
|
|
.then(data => setResultats(data))
|
|
}
|
|
else {
|
|
fetch(`/data/resultats/europeennes2024/${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 === "regions")
|
|
setZoneName(`Région ${resultats.region.nom}`)
|
|
else if (typeResultats === "departements")
|
|
setZoneName(`Département ${resultats.departement.nom}`)
|
|
else if (typeResultats === "circonscriptions")
|
|
setZoneName(`Circonscription ${resultats.circonscription.nom}`)
|
|
else if (typeResultats === "communes")
|
|
setZoneName(`Commune ${resultats.commune.nom}`)
|
|
else if (typeResultats === "bureaux_vote")
|
|
setZoneName(resultats.bureau_vote.libelle)
|
|
}, [typeResultats, resultats])
|
|
|
|
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] += 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, grouperParBloc])
|
|
|
|
useEffect(() => {
|
|
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,
|
|
}]
|
|
}
|
|
|
|
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} />
|
|
<Carte typeResultats={typeResultats} resultats={resultats} listes={listes} blocs={blocs} nuances={nuances} voixParBloc={voixParBloc} voixParNuance={voixParNuance} grouperParBloc={grouperParBloc} />
|
|
</>
|
|
}
|