Restructuration

This commit is contained in:
Emmy D'Anello 2024-06-17 18:40:26 +02:00
parent b46a58101b
commit 1844354164
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
5 changed files with 599 additions and 554 deletions

View File

@ -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>% Inscrites</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>% Inscrites</TableCell>
<TableCell>% Votantes</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow key={"Inscrit⋅es"}>
<TableCell>Inscrites</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>Votantes</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='&copy; 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>
</>
}

View 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>% Inscrites</TableCell>
<TableCell>% Votantes</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow key={"Inscrit⋅es"}>
<TableCell>Inscrites</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>Votantes</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='&copy; 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>
</>
}

View File

@ -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>% Inscrites</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>
}

View 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
}

View File

@ -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)