Affichage des résultats des régions sur une carte
This commit is contained in:
		
							
								
								
									
										33
									
								
								nupes-elections-front/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										33
									
								
								nupes-elections-front/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -17,8 +17,10 @@
 | 
			
		||||
        "@testing-library/user-event": "^13.5.0",
 | 
			
		||||
        "highcharts": "^11.4.3",
 | 
			
		||||
        "highcharts-react-official": "^3.2.1",
 | 
			
		||||
        "leaflet": "^1.9.4",
 | 
			
		||||
        "react": "^18.3.1",
 | 
			
		||||
        "react-dom": "^18.3.1",
 | 
			
		||||
        "react-leaflet": "^4.2.1",
 | 
			
		||||
        "react-router-dom": "^6.23.1",
 | 
			
		||||
        "react-scripts": "5.0.1",
 | 
			
		||||
        "web-vitals": "^2.1.4"
 | 
			
		||||
@@ -4053,6 +4055,17 @@
 | 
			
		||||
        "url": "https://opencollective.com/popperjs"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@react-leaflet/core": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
 | 
			
		||||
      "license": "Hippocratic-2.1",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "leaflet": "^1.9.0",
 | 
			
		||||
        "react": "^18.0.0",
 | 
			
		||||
        "react-dom": "^18.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@remix-run/router": {
 | 
			
		||||
      "version": "1.16.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
 | 
			
		||||
@@ -13913,6 +13926,12 @@
 | 
			
		||||
        "shell-quote": "^1.8.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/leaflet": {
 | 
			
		||||
      "version": "1.9.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
 | 
			
		||||
      "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
 | 
			
		||||
      "license": "BSD-2-Clause"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/leven": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
 | 
			
		||||
@@ -16798,6 +16817,20 @@
 | 
			
		||||
      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-leaflet": {
 | 
			
		||||
      "version": "4.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
 | 
			
		||||
      "license": "Hippocratic-2.1",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@react-leaflet/core": "^2.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "leaflet": "^1.9.0",
 | 
			
		||||
        "react": "^18.0.0",
 | 
			
		||||
        "react-dom": "^18.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-refresh": {
 | 
			
		||||
      "version": "0.11.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,10 @@
 | 
			
		||||
    "@testing-library/user-event": "^13.5.0",
 | 
			
		||||
    "highcharts": "^11.4.3",
 | 
			
		||||
    "highcharts-react-official": "^3.2.1",
 | 
			
		||||
    "leaflet": "^1.9.4",
 | 
			
		||||
    "react": "^18.3.1",
 | 
			
		||||
    "react-dom": "^18.3.1",
 | 
			
		||||
    "react-leaflet": "^4.2.1",
 | 
			
		||||
    "react-router-dom": "^6.23.1",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "web-vitals": "^2.1.4"
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,9 @@ 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)
 | 
			
		||||
 | 
			
		||||
@@ -39,18 +42,7 @@ function ResultatsTable({blocs, nuances, listes, resultats, siegesParListe}) {
 | 
			
		||||
        </TableHead>
 | 
			
		||||
        <TableBody>
 | 
			
		||||
          {listes_triees.map((liste) => (
 | 
			
		||||
            <TableRow key={liste.numero}>
 | 
			
		||||
              <TableCell>{liste.numero}</TableCell>
 | 
			
		||||
              <TableCell>{liste.nom}</TableCell>
 | 
			
		||||
              <TableCell sx={{backgroundColor: nuances.filter(nuance => nuance.code === liste.nuance)[0].couleur, padding: "0.2em"}}></TableCell>
 | 
			
		||||
              <TableCell>{liste.nuance}</TableCell>
 | 
			
		||||
              <TableCell sx={{backgroundColor: blocs.filter(bloc => bloc.nom === liste.bloc)[0].couleur, padding: "0.2em"}}></TableCell>
 | 
			
		||||
              <TableCell>{liste.bloc}</TableCell>
 | 
			
		||||
              <TableCell>{voix_listes[liste.numero] || 0}</TableCell>
 | 
			
		||||
              <TableCell>{(100 * (voix_listes[liste.numero] || 0) / resultats.inscrits).toFixed(2)} %</TableCell>
 | 
			
		||||
              <TableCell>{(100 * (voix_listes[liste.numero] || 0) / resultats.exprimes).toFixed(2)} %</TableCell>
 | 
			
		||||
              <TableCell>{siegesParListe[liste.numero]}</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
            <ListeRow key={liste.numero} liste={liste} voix={voix_listes[liste.numero] || 0} resultats={resultats} siegesParListe={siegesParListe} blocs={blocs} nuances={nuances} />
 | 
			
		||||
          ))}
 | 
			
		||||
        </TableBody>
 | 
			
		||||
      </Table>
 | 
			
		||||
@@ -59,6 +51,25 @@ function ResultatsTable({blocs, nuances, listes, resultats, siegesParListe}) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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}>
 | 
			
		||||
@@ -114,6 +125,96 @@ function ParticipationTable({resultats}) {
 | 
			
		||||
  </>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@@ -321,8 +422,10 @@ export default function Election2024({typeResultats = "france"}) {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: `Résultats des élections européennes 2024 : ${zoneName}`,
 | 
			
		||||
    },
 | 
			
		||||
    legend: {
 | 
			
		||||
      labelFormat: '{name} <span style="opacity: 0.4">{x}</span>'
 | 
			
		||||
    tooltip: {
 | 
			
		||||
      formatter: function () {
 | 
			
		||||
        return `<span>${this.x}</span> : <strong>${this.y}</strong> voix (${(100 * this.y / resultats.exprimes).toFixed(2)} %)<br>`
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    xAxis: {
 | 
			
		||||
      categories: categoriesVoix,
 | 
			
		||||
@@ -355,5 +458,6 @@ export default function Election2024({typeResultats = "france"}) {
 | 
			
		||||
    />
 | 
			
		||||
    <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} />
 | 
			
		||||
  </>
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,11 @@ def exporter_resultats_france(engine: Engine, verbose: bool = False) -> None:
 | 
			
		||||
            session.add(resultats_france)
 | 
			
		||||
 | 
			
		||||
        resultats_dict = {
 | 
			
		||||
            'france': {
 | 
			
		||||
                'regions': [reg.code_insee for reg in session.execute(select(Region)).scalars().all()],
 | 
			
		||||
                'departements': [dpt.code_insee for dpt in session.execute(select(Departement)).scalars().all()],
 | 
			
		||||
                'circonscriptions': [circo.id for circo in session.execute(select(Circonscription)).scalars().all()],
 | 
			
		||||
            },
 | 
			
		||||
            "inscrits": resultats_france.inscrits,
 | 
			
		||||
            "votants": resultats_france.votants,
 | 
			
		||||
            "abstentions": resultats_france.abstentions,
 | 
			
		||||
@@ -105,7 +110,11 @@ def exporter_resultats_regions(engine: Engine, verbose: bool = False) -> None:
 | 
			
		||||
        regions_iterator = tqdm(regions, desc="Régions") if verbose else regions
 | 
			
		||||
        for region in regions_iterator:
 | 
			
		||||
            region_json = {'code_insee': region.code_insee, 'nom': region.libelle,
 | 
			
		||||
                           'departements': [dpt.code_insee for dpt in region.departements]}
 | 
			
		||||
                           'departements': [dpt.code_insee for dpt in region.departements],
 | 
			
		||||
                           'circonscriptions':
 | 
			
		||||
                               [circo.id for circo in session.execute(
 | 
			
		||||
                                   select(Circonscription).join(Departement).filter_by(region_code=region.code_insee))
 | 
			
		||||
                                .scalars().all()]}
 | 
			
		||||
            regions_json.append(region_json)
 | 
			
		||||
 | 
			
		||||
            resultats_region = session.execute(select(ResultatsRegion).filter_by(region_id=region.code_insee)) \
 | 
			
		||||
@@ -318,6 +327,7 @@ def exporter_resultats_bureaux_vote(engine: Engine, verbose: bool = False) -> No
 | 
			
		||||
        iterator = tqdm(bureaux_vote, desc="Bureaux de vote") if verbose else bureaux_vote
 | 
			
		||||
        for bureau_vote in iterator:
 | 
			
		||||
            bureau_vote_json = {'id': bureau_vote.id,
 | 
			
		||||
                                'libelle': bureau_vote.libelle,
 | 
			
		||||
                                'commune': bureau_vote.commune_code,
 | 
			
		||||
                                'circonscription': bureau_vote.circo_code}
 | 
			
		||||
            bureaux_vote_json.append(bureau_vote_json)
 | 
			
		||||
 
 | 
			
		||||
@@ -251,7 +251,7 @@ def importer_bureaux_vote(engine: Engine, verbose: bool = False) -> None:
 | 
			
		||||
            numero_circo = int(bv_dict['codeCirconscription'][len(dpt_code):])
 | 
			
		||||
            code_circo = f"{dpt_code}-{numero_circo:02d}"
 | 
			
		||||
            bv_id = bv_dict['id_bv'].split()[0]
 | 
			
		||||
            bv_libelle = f"Bureau {code_bv}"
 | 
			
		||||
            bv_libelle = f"Bureau {code_bv} de {bv_dict['nomCommune']}"
 | 
			
		||||
 | 
			
		||||
            if not session.execute(select(Commune).filter_by(code_insee=code_commune)).scalar_one_or_none():
 | 
			
		||||
                print("Commune non trouvée avec le code", code_commune, "et le nom", bv_dict['nomCommune'])
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user