[This article was first published on r.iresmi.net, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Paris, France. French and European flags floating in the wind on the Élysée palace entrance gates

French and European flags – CC-BY-NC-ND by Ibrahim Ajaja / World Bank

Election maps are hard: Land doesn’t vote, people do !. So most maps made on these occasions (generally choropleths) are possibly misleading.

Figure 1: Carte des communes de France selon la couleur de la liste arrivée en tête aux élections – Le Monde

A solution is a cartogram, however we generally only see the winner of each constituency.

Figure 2: Each municipality was transformed into a dot, with the area of the dot proportional to the number of voters 👉 a more accurate representation of voting patterns – Karim Douieb

So it’s biased again, especially when several parties have very similar scores.

Here I show a cartogram of the main parties (those who got at least one deputy), aggregated by département; so each département has generally 5 dots whose area is proportional to the number of votes.


Some packages are needed…



Provisional results by communes or département are only available in (many) XML files1, so some data wrangling is necessary.

I get the list of département from INSEE to iterate on.

Département polygons come from Adminexpress COG Carto, simplified beforehand with Mapshaper.

dep <- read_csv("https://www.insee.fr/fr/statistiques/fichier/7766585/v_departement_2024.csv") |>

dep_aex <- read_sf("DEPARTEMENT.shp") |>
  st_transform("EPSG:2154") |> 
  st_make_valid() |>

And the 101 XML files are scraped from the Ministère de l’intérieur. We get the results for 34,929 communes.

if (!file.exists("resultats.parquet")) {
  extraire_dep <- function(dep, reg,...) {
    read_xml(glue("https://www.resultats-elections.interieur.gouv.fr/telechargements/EU2024/resultatsT1/{reg}/{dep}/R1{dep}COM.xml")) |>
      as_list() |> 
      pluck("Election", "Departement", "Communes") |> 
      map(as_tibble) |> 
      list_rbind() |> 
      unnest_wider(Tours) |> 
      unnest_wider(Resultats) |> 
      unnest_wider(Mentions) |> 
      unnest_wider(Abstentions) |> 
      rename(n_abstentions = Nombre) |> 
      select(-RapportInscrits) |> 
      unnest_wider(Votants) |> 
      rename(n_votants = Nombre) |> 
      select(-RapportInscrits) |>
      unnest_wider(Blancs) |> 
      rename(n_blancs = Nombre) |> 
      select(-RapportInscrits, -RapportVotants) |>
      unnest_wider(Nuls) |> 
      rename(n_nuls = Nombre) |> 
      select(-RapportInscrits, -RapportVotants) |>
      unnest_wider(Exprimes) |> 
      rename(n_exprimes = Nombre) |> 
      select(-RapportInscrits, -RapportVotants) |>
      unnest_longer(Listes) |> 
      unnest_wider(Listes) |> 
      mutate(across(everything(), unlist))
  resultats <- dep |>
    pmap(safely(extraire_dep, quiet = FALSE),
         .progress = "   Import") |> 
    map(~ pluck(.x, "result")) |> 
    list_rbind() |> 
    clean_names() |> 
    mutate(across(c(inscrits:n_exprimes, nb_voix), parse_integer),
           across(c(rapport_exprimes, rapport_inscrits),
                  ~ parse_number(.x, locale = locale(decimal_mark = ",")))) |>
  write_parquet(resultats, "resultats.parquet")
  date_fichier <- Sys.Date()
} else {
  resultats <- read_parquet("resultats.parquet")
  date_fichier <- as.Date(file.info("resultats.parquet")$mtime)

Download it here, in parquet format.


We aggregate some political parties together after keeping only the parties having at least one deputy, and compute the votes by département (only for metropolitan France, sorry).

nuances_deputes_elus <- tribble(
  ~cod_nua_liste, ~affiliation,     ~couleur,
  "LRN",          "Extrême droite", "tan4",
  "LENS",         "Centre",         "orange3",
  "LUG",          "Gauche",         "violetred3",
  "LFI",          "Gauche",         "violetred3", 
  "LLR",          "Droite",         "lightblue3",
  "LVEC",         "Verts",          "darkgreen",
  "LREC",         "Extrême droite", "tan4")

res_carto <- resultats |> 
  filter(cod_nua_liste %in% nuances_deputes_elus$cod_nua_liste,
         cod_com < "971",
         nb_voix > 0) |> 
             join_by(cod_nua_liste)) |> 
           dep = str_sub(cod_com, 1, 2))  |> 
  summarise(nb_voix = sum(nb_voix),
            .groups = "drop")

Several methods are available to make a cartogram; I’ll chose a Dorling cartogram. The {cartogram} package does its hard work:

resultats_dorling <- dep_aex |>
  filter(insee_dep < "971") |> 
            join_by(insee_dep == dep)) |> 
  mutate(affiliation = factor(affiliation, 
                              levels = c(
                                "Extrême droite"))) |> 
  arrange(affiliation, insee_dep) |> 
  cartogram_dorling("nb_voix", k = .6)


And we map the result:

palette_affiliation <- nuances_deputes_elus |> 
  distinct(affiliation, couleur) |> 

dep_aex |>
  filter(insee_dep < "971") |> 
  ggplot() +
  geom_sf(fill = NA, color = "#aaaaaa") +
  geom_sf(data = resultats_dorling,
          aes(fill = affiliation, 
              color = affiliation),
          alpha = 0.7) +
  scale_fill_manual(values = palette_affiliation) +
  scale_color_manual(values = set_names(darken(palette_affiliation, 0.2),
                                        names(palette_affiliation))) +
  labs(title = "Élections europénnes 2024",
       subtitle = "France métropolitaine",
       fill = "",
       color = "",
       caption = glue("données MIOM {date_fichier}
                  filtre sur les nuances ayant élu des députés
                  https://r.iresmi.net/ {Sys.Date()}")) +
  guides(fill = guide_legend(label.position = "bottom")) +
  theme_void() +
  theme(legend.position = "bottom",
        legend.key.width = unit(1.5, "cm"),
        legend.key.height = unit(.5, "cm"),
        legend.text = element_text(size = 8),
        plot.caption = element_text(size = 7))
Cartogram for the main political parties
Figure 3: 2024 European election in France

Not perfect but maybe more faithful…


  1. It seems that a nicer dataset is now available.↩︎

To leave a comment for the author, please follow the link and comment on their blog: r.iresmi.net.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)