Choroplèthe réactif et intéractif avec {ggplot2} dans une appli shiny

twitch
quarto
shiny
ggplot2
graphique
Rnewbies
Author

Marie Vaugoyeau

Published

12 October 2026

Note

Twitch du 12 octobre 2026.

Le code est disponible sur GitHub 😊

Avant de commencer

Un choroplèthe

Tip

Le choroplèthe est une représentation qui permet de visualiser les valeurs d’une variable dans une représentation géographique.
Généralement, c’est la couleur qui permet de connaîtres le niveau de valeur.

Une représentation créée par Charles Dupin au 19ème siècle.

Les données

Les données utilisées sont celles du fichier de données Registre national des installations de production et de stockage d’électricité disponible sur le site de ODRE

Les données sont manipulées grâce aux fonctions de {dplyr} pour réaliser un tableau résumé contenant la puissance maximale cumulée sur toutes les installations en fonction des modalités des colonnes region, filiere et tensionRaccordement

Démarche

➡️ importer des données en ligne
🧹 les préparer
🗺️ récupérer un fond de carte
🎨 créer le choroplèthe avec {ggplot2}
🔁 l’intégrer dans une appli pour le rendre réactif

Maquette de l’appli

Figure 1: maquette simple

Les parties filière et tension sont des sélecteurs pour que l’utilisateurice puisse choisir le type de filière et le(s) tension(s) de raccordement à représenter sur le choroplèthe.

Réalisation

Importer des données

Les données sont importées directement depuis le site grâce à l’utilisation de la fonction read_delim() du package {readr}.
Seules les colonnes d’intérêt sont utilisées grâce à l’argument col_select.

library(tidyverse)

donnees_odre <- read_delim(
  "https://odre.opendatasoft.com/api/explore/v2.1/catalog/datasets/registre-national-installation-production-stockage-electricite-agrege/exports/csv?lang=fr&refine=regime%3A%22En%20service%22&facet=facet(name%3D%22regime%22%2C%20disjunctive%3Dtrue)&timezone=Europe%2FBerlin&use_labels=true&delimiter=%3B",
  col_select = c(region, filiere, tensionRaccordement, maxPuis)
)
Tip

Pour limiter le nombre de lignes utilisées j’ai rtéduis sur le site en sélectionnant dans la partie regime En service.
Puis en cliquant sur les enregistrements sélectionnées (clique droit) :

Préparation des données

Je commence à supprimer les lignes pour lesquelle la région n’est pas précisée (NA) grâce à la fonction drop_na() du package {tidyr}

donnees_odre |> 
  drop_na(region)
# A tibble: 106,733 × 4
   region filiere tensionRaccordement maxPuis
   <chr>  <chr>   <chr>                 <dbl>
 1 Corse  Solaire BT                     99.8
 2 Corse  Solaire BT                     99.6
 3 Corse  Solaire BT                    100  
 4 Corse  Solaire BT                     99.9
 5 Corse  Solaire BT                     99.9
 6 Corse  Solaire BT                    196  
 7 Corse  Solaire BT                    100.0
 8 Corse  Solaire BT                     68.0
 9 Corse  Solaire BT                     53.2
10 Corse  Solaire BT                     90  
# ℹ 106,723 more rows

Comme je souhaite obtenir une table résumée en fonction de la region, la filière et la tensionRaccordement, j’utilise group_by() avant le summarise().

(donnees_odre <- donnees_odre |> 
  drop_na(region) |> 
  group_by(
    region,
    filiere,
    tensionRaccordement
  ) |> 
  summarise(
    puissance_max_cumulee = sum(maxPuis)/1000
  ) |> 
  ungroup())
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by region, filiere, and tensionRaccordement.
ℹ Output is grouped by region and filiere.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(region, filiere, tensionRaccordement))` for
  per-operation grouping (`?dplyr::dplyr_by`) instead.
# A tibble: 330 × 4
   region               filiere     tensionRaccordement puissance_max_cumulee
   <chr>                <chr>       <chr>                               <dbl>
 1 Auvergne-Rhône-Alpes Autre       HTA                                  3.55
 2 Auvergne-Rhône-Alpes Bioénergies 63 kV                               14.8 
 3 Auvergne-Rhône-Alpes Bioénergies BT                                   8.62
 4 Auvergne-Rhône-Alpes Bioénergies HTA                                186.  
 5 Auvergne-Rhône-Alpes Eolien      225 kV                             101.  
 6 Auvergne-Rhône-Alpes Eolien      63 kV                               24   
 7 Auvergne-Rhône-Alpes Eolien      HTA                                676.  
 8 Auvergne-Rhône-Alpes Hydraulique 150 kV                             124   
 9 Auvergne-Rhône-Alpes Hydraulique 225 kV                            3844.  
10 Auvergne-Rhône-Alpes Hydraulique 400 kV                            4022.  
# ℹ 320 more rows
Warning

Il ne faut pas oublier de retirer les groupes avec la fonction ungroup().

Création d’un fond de carte

Le fond de carte est à l’origine un tableau de données contenant le tracé des différentes régions, récupérée via map_data() du package {ggplot2} (nécessite d’avoir le package {maps} présent).

fond_de_carte <-
  map_data("france")

L’affichage de la table montre que les regions sont en fait les départements.
Il faut donc utiliser une table contenant les noms des départements et des régions de France sur le site de data gouv puis réaliser la jointure.

fond_de_carte <-
  map_data("france") |> 
  left_join(
    read_delim(
      "https://www.data.gouv.fr/api/1/datasets/r/987227fb-dcb2-429e-96af-8979f97c9c84"
    ),
    by = join_by("region" == "dep_name")
  )
Rows: 101 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): num_dep, dep_name, region_name

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

J’utilise ensuite la fonction de filtre de {dplyr} pour vérifier qu’il n’y a pas de département non reconnu.

fond_de_carte |> 
  filter(region_name |>  is.na()) |> 
  distinct(region)
                 region
1             Val-Doise
2          Cotes-Darmor
3             Finistere
4              Cote-Dor
5           Haute-Saone
6                Nievre
7        Saone-et-Loire
8           Deux-Sevres
9                Vendee
10                Rhone
11          Puy-de-Dome
12                Isere
13              Correze
14                Drome
15              Ardeche
16               Lozere
17              Herault
18     Bouches-du-Rhone
19      Hautes-Pyrenees
20 Pyrenees-Atlantiques
21               Ariege
22  Pyrenees-Orientales
23         Corse du Sud

23 départements sont mal reconnus.
Principalement à cause des accents.

J’utilise donc la fonction str_replace() du package {stringr} pour modifier les noms des départements dans la table issu du site de data gouv à l’intérieur d’une fonction mutate() de {dplyr} et je ne garde que les colonnes d’intéret.

fond_de_carte <-
  map_data("france") |> 
  left_join(
    read_delim(
      "https://www.data.gouv.fr/api/1/datasets/r/987227fb-dcb2-429e-96af-8979f97c9c84"
    ) |> 
      mutate(
        nom_dept = dep_name |> 
          str_replace_all("[éèëê]", "e") |> 
          str_replace_all("[ôö]", "o") |> 
          str_replace("d'O", "Do") |> 
          str_replace("d'A", "Da") |> 
          str_replace("Corse-du-Sud", "Corse du Sud")
      ),
    by = join_by("region" == "nom_dept")
  ) |> 
  select(long, lat, group, region = region_name)
Rows: 101 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): num_dep, dep_name, region_name

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Tipstr_replace() ou str_replace_all()

Les fonctions Str_replace() et str_replace_all() du package {stringr} permettent de remplacer un pattern par une chaîne de remplacement.

  • str_replace() : ne le fait qu’une seule fois donc str_replace("Pyrénées", "[éèëê]", "e") donne Pyrenées
  • str_replace_all() : le fait autant de fois qu’il rencontre le pattern donc str_replace_all("Pyrénées", "[éèëê]", "e") donne Pyrenees

Paramétrer le choroplèthe avec {ggplot2}

Création de paramètres

Choix de paramètres qui sera ensuite utilisés pour filtrer les données résumées

type_filiere <- "Solaire"
type_tension <- c("HTA", "BT")

Jointure et représentation des données

Jointure du fond de carte avec la table filtrée pour ensuite représenter les données grâce à {ggplot2}

full_join(
  fond_de_carte,
  donnees_odre |> 
    filter(
      filiere == type_filiere,
      tensionRaccordement %in% type_tension
    ),
  by = join_by("region" == "region")
) |> 
  ggplot() +
  aes(
    x = long,
    y = lat,
    group = group,
    fill = puissance_max_cumulee
  ) +
  geom_polygon(color = "black") +
  labs(
    title = 
      paste("Tension de raccordement : ", type_tension |> str_c(collapse = ' / '), " \n Filière : ", type_filiere)
  ) +
  theme_void() +
  scale_fill_gradient(low = "#faebed", high = "#01066b")
Warning in full_join(fond_de_carte, filter(donnees_odre, filiere == type_filiere, : Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 1 of `x` matches multiple rows in `y`.
ℹ Row 17 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
  "many-to-many"` to silence this warning.

Tip

Je modifie le gradient de couleur utilisée qui n’est pas dans le bon sens par défaut dans {ggplot2} : les valeurs les plus faibles doivent avoir la couleur la moins soutenue.

Intégrer la carte dans une appli

Structure de l’appli

Je créé un nouveau fichier .R puis j’ajoute la structure d’une appli shiny.

Tip

Pour générer facilement le squelette d’une appli, taper shinyapp puis taper sur Entrée pour utiliser le snippet.

library(shiny)

ui <- fluidPage(
  
)

server <- function(input, output, session) {
  
}

shinyApp(ui, server)

import des données

Avant la partie ui j’importe les packages et les données nécessaires

#### Avant de commencer ####  

# import des packages ----
library(shiny)
library(tidyverse)
library(bslib)

# import des données ----
## données depuis le site ODRE : https://opendata.reseaux-energies.fr/
donnees_odre <- read_delim(
  "https://odre.opendatasoft.com/api/explore/v2.1/catalog/datasets/registre-national-installation-production-stockage-electricite-agrege/exports/csv?lang=fr&refine=regime%3A%22En%20service%22&facet=facet(name%3D%22regime%22%2C%20disjunctive%3Dtrue)&timezone=Europe%2FBerlin&use_labels=true&delimiter=%3B",
  col_select = c(region, filiere, tensionRaccordement, maxPuis)
) |> 
  drop_na(region) |> 
  group_by(
    region,
    filiere,
    tensionRaccordement
  ) |> 
  summarise(
    puissance_max_cumulee = sum(maxPuis)/1000
  ) |> 
  ungroup()

## création du fond de carte
fond_de_carte <-
  map_data("france") |> 
  left_join(
    read_delim(
      "https://www.data.gouv.fr/api/1/datasets/r/987227fb-dcb2-429e-96af-8979f97c9c84"
    ) |> 
      mutate(
        nom_dept = dep_name |> 
          str_replace_all("[éèëê]", "e") |> 
          str_replace_all("[ôö]", "o") |> 
          str_replace("d'O", "Do") |> 
          str_replace("d'A", "Da") |> 
          str_replace("Corse-du-Sud", "Corse du Sud")
      ),
    by = join_by("region" == "nom_dept")
  ) |> 
  select(long, lat, group, region = region_name)

UI

Dans la partie ui j’ajoute 2 sélecteurs pour la filière et la tension de raccordement.

#### Partie UI ####   
ui <- page_sidebar(
  title = "Puissance max installée en fonction des filières et des tensions de raccordement",
  sidebar = 
    sidebar(
      width = 600,
      radioButtons(
        "filiere",
        "Choix d'une filière",
        choices = donnees_odre |> 
          count(filiere) |> 
          pull(filiere)
      ),
      selectizeInput(
        "tension",
        "Choix d'une ou plusieurs tension de raccordement",
        choices = donnees_odre |> 
          count(tensionRaccordement) |> 
          pull(tensionRaccordement),
        multiple = TRUE
      )
    ),
  plotOutput("carte")
)

server

Création de la carte réactive en fonction des input filiere et tension

#### Partie SERVER ####  
server <- function(input, output, session) {
  
  output$carte <- renderPlotly(
    full_join(
      fond_de_carte,
      donnees_odre |> 
        filter(
          filiere == input$filiere,
          tensionRaccordement %in% input$tension
        ),
      by = join_by("region" == "region")
    ) |> 
      ggplot() +
      aes(
        x = long,
        y = lat,
        group = group,
        fill = puissance_max_cumulee
      ) +
      geom_polygon(color = "black") +
      labs(
        title = 
          paste("Tension de raccordement : ", input$tension |> str_c(collapse = ' / '), " \n Filière : ", input$filiere)
      ) +
      theme_void() +
      scale_fill_gradient(low = "#faebed", high = "#01066b")
  )
  
}

Interactivité

Pour ajouter l’interactivbité j’ai utiliser {plotly} :

#### Avant de commencer ####  

# import des packages ----
library(shiny)
library(tidyverse)
library(bslib)
library(plotly)

# import des données ----
## données depuis le site ODRE : https://opendata.reseaux-energies.fr/
donnees_odre <- read_delim(
  "https://odre.opendatasoft.com/api/explore/v2.1/catalog/datasets/registre-national-installation-production-stockage-electricite-agrege/exports/csv?lang=fr&refine=regime%3A%22En%20service%22&facet=facet(name%3D%22regime%22%2C%20disjunctive%3Dtrue)&timezone=Europe%2FBerlin&use_labels=true&delimiter=%3B",
  col_select = c(region, filiere, tensionRaccordement, maxPuis)
) |> 
  drop_na(region) |> 
  group_by(
    region,
    filiere,
    tensionRaccordement
  ) |> 
  summarise(
    puissance_max_cumulee = sum(maxPuis)/1000
  ) |> 
  ungroup()

## création du fond de carte
fond_de_carte <-
  map_data("france") |> 
  left_join(
    read_delim(
      "https://www.data.gouv.fr/api/1/datasets/r/987227fb-dcb2-429e-96af-8979f97c9c84"
    ) |> 
      mutate(
        nom_dept = dep_name |> 
          str_replace_all("[éèëê]", "e") |> 
          str_replace_all("[ôö]", "o") |> 
          str_replace("d'O", "Do") |> 
          str_replace("d'A", "Da") |> 
          str_replace("Corse-du-Sud", "Corse du Sud")
      ),
    by = join_by("region" == "nom_dept")
  ) |> 
  select(long, lat, group, region = region_name)

#### Partie UI ####   
ui <- page_sidebar(
  title = "Puissance max installée en fonction des filières et des tensions de raccordement",
  sidebar = 
    sidebar(
      width = 600,
      radioButtons(
        "filiere",
        "Choix d'une filière",
        choices = donnees_odre |> 
          count(filiere) |> 
          pull(filiere)
      ),
      selectizeInput(
        "tension",
        "Choix d'une ou plusieurs tension de raccordement",
        choices = donnees_odre |> 
          count(tensionRaccordement) |> 
          pull(tensionRaccordement),
        multiple = TRUE
      )
    ),
  plotlyOutput("carte")
)

#### Partie SERVER ####  
server <- function(input, output, session) {
  
  output$carte <- renderPlotly(
    full_join(
      fond_de_carte,
      donnees_odre |> 
        filter(
          filiere == input$filiere,
          tensionRaccordement %in% input$tension
        ),
      by = join_by("region" == "region")
    ) |> 
      ggplot() +
      aes(
        x = long,
        y = lat,
        group = group,
        fill = puissance_max_cumulee
      ) +
      geom_polygon(color = "black") +
      labs(
        title = 
          paste("Tension de raccordement : ", input$tension |> str_c(collapse = ' / '), " \n Filière : ", input$filiere)
      ) +
      theme_void() +
      scale_fill_gradient(low = "pink", high = "darkblue")
  )
  
}

#### Pour lancer l'appli (A NE PAS SUPPRIMER) ####  
shinyApp(ui, server)

Pour aller plus loin

J’ai dû m’arrêter car j’étais limitée par le temps mais il manque un certain nombre de points :

  • les informations de l’appli sur l’ui (cadre en bas à droite sur la maquette Figure 1)
  • communication avec l’utilisateurice -> le choix d’une tension est obligatoire pour avoir une carte
  • documenter le code de l’appli -> pour facilité la réutilisation et le partage
  • publier l’appli pour la rendre disponible
Tip

RDV le lundi 16 novembre à 14h sur Twitch 😊

Pour finir

Et voilà, n’hésites pas à me faire des retours sur contact@marievaugoyeau.fr ou LinkedIn ✍️

Note

Tu peux aussi t’inscrire à la newsletter pour être au courant du prochain live 📺

Bonne journée 😊

Back to top