Cartes du monde avec R : projections, graticules et palettes
cartographie, projections, sf, rnaturalearth, ggplot2, R
Cartes du monde avec R : projections, graticules et palettes
Mercator, Equal Earth et les autres : comprendre les projections cartographiques et les manier avec ggplot2.
Pourquoi la projection choisie change tout
Vous avez peut-être vu passer l’info : le 4 septembre 2026, l’Assemblée générale des Nations unies a adopté une résolution portée par le Togo encourageant les États, écoles et organisations internationales à délaisser la projection de Mercator au profit de représentations plus fidèles aux superficies réelles, comme la projection Equal Earth. Ce n’est pas la première fois qu’un grand acteur prend ses distances avec Mercator : Google Maps l’a fait dès 2018 pour ses vues à l’échelle mondiale, en basculant sur un globe 3D plutôt qu’une carte plate. Le choix d’une projection cartographique change la façon dont on perçoit la taille réelle des pays.
Et pour cause, aplatir une sphère sans la déformer est mathématiquement impossible. Chaque projection fait donc un choix, elle préserve certaines propriétés au prix d’en déformer d’autres :
- les surfaces (un pays deux fois plus grand qu’un autre en réalité doit apparaître deux fois plus grand sur la carte) ;
- les angles et les formes (un contour de pays ne doit pas être distordu) ;
- les distances (la longueur d’un segment sur la carte doit refléter la distance réelle).
Aucune projection ne préserve les trois à la fois. C’est là que Mercator et Equal Earth prennent des chemins différents.
Mercator : des angles parfaits, des surfaces qui explosent
La projection de Mercator, créée en 1569 pour la navigation maritime, préserve les angles : une ligne droite sur la carte correspond à un cap constant au compas, très utile en mer. Mais cette préservation des angles a un coût élevé sur les surfaces, qui grandissent démesurément à mesure qu’on s’éloigne de l’équateur.
Avec cette projection, le Groenland paraît aussi grand que l’Afrique, alors qu’en réalité l’Afrique fait environ 14 fois sa taille. C’est le reproche le plus connu fait à cette projection, elle donne une importance visuelle disproportionnée aux régions proches des pôles, au détriment des régions équatoriales.
Pourquoi l’Antarctique est souvent absent sur une carte Mercator
L’échelle de Mercator diverge à mesure qu’on approche des pôles. L’Antarctique, qui s’étend jusqu’à -90° de latitude, devient démesurément et inutilisablement grand sous cette projection. C’est pourquoi ce continent est traité à part avec cette projection. Le plus souvent, il est tout simplement exclu des données avant de générer la figure.
Equal Earth : des surfaces fidèles, des formes qui bougent un peu
La projection Equal Earth (Šavrič et al. 2019) part du problème inverse : elle préserve les proportions de surface entre les pays. Le Groenland y retrouve une taille comparable à sa réalité, au prix de formes et d’angles légèrement déformés loin de l’équateur.
L’Afrique retrouve ainsi sa vraie surface, tandis que le Groenland rétrécit en conséquence. Rien n’a changé dans les données, uniquement la façon de les projeter sur un plan.
Pourquoi Equal Earth et pas une autre projection équivalente ?
Equal Earth n’est pas la première projection à préserver les surfaces : Mollweide (1805) ou Gall-Peters (1855) le font aussi. Elle a été conçue spécifiquement pour donner des formes de continents plus naturelles à l’œil que ses prédécesseurs, tout en gardant cette propriété. C’est ce compromis esthétique qui explique son adoption récente par plusieurs médias et organisations.
Mais comment on fait ça concrètement, avec R ?
Faire des cartes du monde avec R
Pour ce tutoriel, on utilise trois packages qui se combinent naturellement :
sf(simple features) pour manipuler des données géographiques (points, lignes, polygones) ;rnaturalearthpour récupérer directement les contours des pays du monde ;ggplot2, qu’on ne présente plus, avec sa fonctiongeom_sf()dédiée aux objets spatiaux.
Récupérer les contours du monde
rnaturalearth télécharge et met en cache des données géographiques prêtes à l’emploi. Pas besoin de chercher un shapefile sur internet.
world <- ne_countries(scale = "medium", returnclass = "sf")L’argument scale contrôle le niveau de détail des contours ("small", "medium" ou "large"). Pour une carte du monde entière, "medium" suffit largement : "large" alourdit inutilement le fichier sans gain visuel à cette échelle.
world est un objet sf : un tableau de données classique, avec une colonne spéciale geometry qui contient les polygones de chaque pays.
world |>
select(name, continent, pop_est, geometry) |>
head()Simple feature collection with 6 features and 3 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: -73.36621 ymin: -22.40205 xmax: 109.4449 ymax: 41.9062
Geodetic CRS: WGS 84
name continent pop_est geometry
1 Zimbabwe Africa 14645468 MULTIPOLYGON (((31.28789 -2...
2 Zambia Africa 17861030 MULTIPOLYGON (((30.39609 -1...
3 Yemen Asia 29161922 MULTIPOLYGON (((53.08564 16...
4 Vietnam Asia 96462106 MULTIPOLYGON (((104.064 10....
5 Venezuela South America 28515829 MULTIPOLYGON (((-60.82119 9...
6 Vatican Europe 825 MULTIPOLYGON (((12.43916 41...
Une première carte
Avec un objet sf, on peut utiliser la fonction geom_sf() qui comprend nativement la colonne geometry.
ggplot(world) +
geom_sf(fill = "#6BAED6", color = "white", linewidth = 0.1) +
coord_sf(expand = FALSE) +
theme_void() +
theme(panel.background = element_rect(fill = "#E4EFF6", color = NA))
Ici on utilise panel.background dans theme() pour colorer les océans, cela fonctionne car cette carte est sans projection. Pour les autres cas, nous verrons plus bas comment faire.
Par défaut, geom_sf() affiche les coordonnées géographiques (longitude, latitude) telles quelles, sans appliquer de projection. C’est ce qu’on appelle la projection plate carrée : chaque degré de longitude et chaque degré de latitude occupent le même espace sur l’écran. Simple, mais ce n’est pas neutre pour autant : les distorsions de surface aux pôles y sont déjà bien visibles.
Changer de projection
C’est coord_sf() qui gère la projection, via son argument crs (coordinate reference system, le système de référence des coordonnées). Pour Equal Earth, il n’existe pas encore de code EPSG largement adopté : on passe par une chaîne PROJ, qui décrit directement les paramètres de la projection.
ggplot() +
geom_sf(data = globe, fill = "#E4EFF6", color = NA) +
geom_sf(data = world, fill = "#6BAED6", color = "white", linewidth = 0.1) +
coord_sf(crs = "+proj=eqearth", expand = FALSE) +
theme_void()
Utilisez un code EPSG dès qu’il existe pour votre projection, comme "EPSG:3857" pour Mercator : c’est plus lisible et moins sujet aux erreurs. Passez par une chaîne PROJ (comme +proj=eqearth ou +proj=robin) quand la projection n’a pas de code EPSG standard ou que vous avez besoin de paramètres spécifiques (méridien central personnalisé par exemple).
Autre exemple de chaîne PROJ courante : Robinson, une projection qui ne préserve ni les surfaces ni les angles à la perfection, mais cherche un compromis visuel entre les deux. Elle a longtemps servi de référence pour les cartes du monde généralistes : la National Geographic Society l’a utilisée de 1988 à 1998, avant de lui préférer la projection Winkel Tripel, qui réduit encore un peu plus la déformation près des pôles.
ggplot() +
geom_sf(data = globe, fill = "#E4EFF6", color = NA) +
geom_sf(data = world, fill = "#6BAED6", color = "white", linewidth = 0.1) +
coord_sf(crs = "+proj=robin", expand = FALSE) +
theme_void()
Les touches finales qui font la différence
Un fond qui épouse la projection
Sans fond du tout, une carte projetée devient vite perturbante à lire : impossible de savoir où s’arrête la Terre et où commence le vide.
ggplot(world) +
geom_sf(fill = "#6BAED6", color = "white", linewidth = 0.1) +
coord_sf(crs = "+proj=eqearth", expand = FALSE) +
theme_void()
Le contour elliptique de la projection Equal Earth se devine à peine. Sur toutes les autres cartes de cet article, l’arrière-plan bleu clair comble ce vide en suivant la forme réelle de la projection. On pourrait croire que theme(panel.background = ...) suffit, mais ce n’est pas le cas : ce réglage colorie toujours un rectangle, quelle que soit la projection, puisqu’il agit sur le panel ggplot2 et non sur la carte elle-même.
La solution consiste à tracer le fond comme une géométrie à part entière, projetée dans le même système que les pays : un rectangle couvrant tout le globe en longitude/latitude (-180° à 180°, -90° à 90°), mais construit comme un contour tracé point par point plutôt que 4 coins isolés, pour que la reprojection le courbe correctement plutôt que de garder des côtés droits.
globe <- st_sfc(
st_polygon(list(cbind(
c(seq(-180, 180, 1), rep(180, 181), seq(180, -180, -1), rep(-180, 181)),
c(rep(-90, 361), seq(-90, 90, 1), rep(90, 361), seq(90, -90, -1))
))),
crs = "EPSG:4326" # sytème longitude / latitude
)Il suffit ensuite de tracer globe en premier, avant les pays.
ggplot() +
geom_sf(data = globe, fill = "#E4EFF6", color = NA) +
geom_sf(data = world, fill = "#6BAED6", color = "white", linewidth = 0.1) +
coord_sf(crs = "+proj=eqearth", expand = FALSE) +
theme_void()
coord_sf() projette globe exactement comme il projette world : le rectangle en lat/lon devient l’ellipse, le cercle ou les lobes propres à chaque projection, sans code supplémentaire. Le découpage en pas de 1° (plutôt que 4 coins) est ce qui permet cette courbure : sf ne courbe que les segments qu’on lui fournit, pas les lignes entre deux points éloignés.
Des couleurs sélectionnées avec soin
Une carte muette (juste des contours) est rarement le but final. On veut le plus souvent représenter une variable par pays : c’est la carte choroplèthe. Prenons un exemple qui n’a besoin d’aucune source externe : le nombre de voisins frontaliers de chaque pays, calculé directement à partir des géométries de world.
voisins <- st_touches(world)
world$n_voisins <- lengths(voisins)
ggplot() +
geom_sf(data = globe, fill = "#E4EFF6", color = NA) +
geom_sf(data = world, aes(fill = n_voisins), color = "darkgray", linewidth = 0.1) +
coord_sf(crs = "+proj=eqearth", expand = FALSE) +
scale_fill_viridis_c(name = "Voisins") +
theme_void()
st_touches() renvoie, pour chaque pays de world, la liste des autres pays qui partagent au moins un point de frontière avec lui ; lengths() compte simplement la taille de chaque sous-liste. Pas de jointure ni de fichier externe : toute l’information est déjà dans la géométrie des pays.
Qui a le plus de voisins ?
La Chine arrive en tête avec 17 voisins terrestres, suivie par la Russie avec 14. À l’autre extrême, les îles ressortent en clair : 0 voisin, ce que st_touches() calcule correctement sans traitement particulier. À prendre comme illustration plutôt que comme référence géopolitique : le tracé des frontières disputées varie selon les jeux de données, rnaturalearth fait ses propres choix à ce sujet.
Le choix de la palette n’est pas un détail cosmétique : mal choisie, elle peut inverser la lecture des données ou les rendre illisibles pour une partie des lecteurs, comme le détaille mon article une palette de couleurs, ça ne se choisit pas au hasard. Ici, le nombre de voisins est une variable numérique avec un ordre naturel, ce qui appelle une palette séquentielle comme viridis car perceptuellement uniforme et lisible pour les personnes daltoniennes.
Un graticule qui ne se cache pas derrière le fond
Le graticule, c’est la grille de méridiens et de parallèles en filigrane sur la plupart des cartes. coord_sf() peut en calculer un automatiquement via le thème (panel.grid), mais panel.grid se dessine toujours derrière les couches geom_sf() : sur les cartes de cet article, il existe bien mais reste caché sous le fond opaque globe.
La solution consiste à ne pas dépendre du thème du tout, et à tracer le graticule comme une géométrie à part entière avec st_graticule(), au même titre que world ou globe : son ordre d’apparition dans le code contrôle alors réellement ce qui se superpose à quoi.
graticule <- st_graticule(
lon = seq(-180, 180, 20),
lat = seq(-80, 80, 20),
crs = "EPSG:4326" # sytème longitude / latitude
)
ggplot() +
geom_sf(data = globe, fill = "#E4EFF6", color = NA) +
geom_sf(data = graticule, color = "grey75", linewidth = 0.2) +
geom_sf(data = world, fill = "#6BAED6", color = "white", linewidth = 0.1) +
coord_sf(crs = "+proj=eqearth", expand = FALSE) +
theme_void()
st_graticule() calcule les lignes en EPSG:4326 selon les intervalles fournis via lon et lat. Trois couches empilées dans l’ordre voulu : globe, graticule, world. Ainsi la grille reste visible sur l’océan sans passer devant les pays.
head(graticule)Simple feature collection with 6 features and 10 fields
Attribute-geometry relationships: constant (3), NAs (7)
Geometry type: LINESTRING
Dimension: XY
Bounding box: xmin: -180 ymin: -89.91 xmax: -80 ymax: 89.91
Geodetic CRS: WGS 84 (CRS84)
degree type degree_label geometry x_start y_start
1 -180 E "180"*degree LINESTRING (-180 -89.91, -1... -180 -89.91
2 -160 E "160"*degree*W LINESTRING (-160 -89.91, -1... -160 -89.91
3 -140 E "140"*degree*W LINESTRING (-140 -89.91, -1... -140 -89.91
4 -120 E "120"*degree*W LINESTRING (-120 -89.91, -1... -120 -89.91
5 -100 E "100"*degree*W LINESTRING (-100 -89.91, -1... -100 -89.91
6 -80 E " 80"*degree*W LINESTRING (-80 -89.91, -80... -80 -89.91
x_end y_end angle_start angle_end plot12
1 -180 89.91 90 90 TRUE
2 -160 89.91 90 90 TRUE
3 -140 89.91 90 90 TRUE
4 -120 89.91 90 90 TRUE
5 -100 89.91 90 90 TRUE
6 -80 89.91 90 90 TRUE
Chaque ligne du graticule porte, en plus de sa géométrie, une colonne degree_label et des colonnes x_start/y_start qui donnent un point d’ancrage sur la ligne. Ce sont ces colonnes qu’on va exploiter dans la section suivante pour annoter les axes.
Annoter les axes
Une fois le graticule visible, reste la question des labels. st_graticule() calcule directement des étiquettes prêtes à l’emploi : chaque ligne porte une colonne degree_label déjà formatée avec point cardinal ("120"*degree*W…) en syntaxe plotmath, plus des colonnes x_start/y_start qui donnent une position d’ancrage.
Le graticule repasse ici à un intervalle de 60° plutôt que 20° : à 20°, chaque ligne porterait une étiquette, soit 18 labels par axe, ce qui serait illisible à cette taille de carte.
graticule <- st_graticule(lon = seq(-180, 180, 60), lat = seq(-80, 80, 20), crs = "EPSG:4326")
# Labels de parallèles (type "N"), ancrés sur le bord gauche (méridien -180°) :
# chaque parallèle porte un label côté ouest, qu'on récupère via x_start/y_start
labels_lat <- graticule |>
filter(type == "N") |>
st_drop_geometry() |>
st_as_sf(coords = c("x_start", "y_start"), crs = "EPSG:4326")
# Labels de méridiens (type "E"), replacés sur le bord bas (parallèle -80°)
# plutôt qu'à leur point d'ancrage d'origine, pour aligner tous les labels
# de longitude sur une même ligne en bas de carte
labels_lon <- graticule |>
filter(type == "E", degree != -180) |>
st_drop_geometry() |>
mutate(y_start = -80) |>
st_as_sf(coords = c("x_start", "y_start"), crs = "EPSG:4326")
ggplot() +
geom_sf(data = globe, fill = "#E4EFF6", color = NA) +
geom_sf(data = graticule, color = "grey75", linewidth = 0.2) +
geom_sf(data = world, fill = "#6BAED6", color = "white", linewidth = 0.1) +
# nudge_x/nudge_y décalent les labels hors des pays plutôt que dessus
geom_sf_text(data = labels_lat, aes(label = degree_label), parse = TRUE, size = 2.5, color = "grey40", hjust = 1, nudge_x = -600000) +
geom_sf_text(data = labels_lon, aes(label = degree_label), parse = TRUE, size = 2.5, color = "grey40", vjust = 1, nudge_y = -600000) +
coord_sf(crs = "+proj=eqearth", expand = FALSE, clip = "off") +
theme_void() +
theme(plot.margin = margin(t = 5, r = 15, b = 15, l = 35, unit = "pt"))
Pour transformer les coordonnées x_start/y_start en un nouveau point à afficher, st_drop_geometry() retire d’abord la géométrie existante (la ligne), pour ne garder qu’un tableau classique ; st_as_sf() en recrée ensuite une, cette fois ponctuelle, à partir des colonnes x_start/y_start désignées via coords. Sans ce passage par un tableau intermédiaire, sf chercherait à combiner la géométrie de ligne déjà présente avec la nouvelle géométrie de point, ce qui échouerait.
Le point d’ancrage du méridien -180° est exclu de labels_lon : sans ce filtre, il tomberait au même endroit projeté que le premier label de labels_lat (80°S), dans le coin bas-gauche de la carte, et les deux labels se superposeraient. parse = TRUE dans geom_sf_text() interprète degree_label comme une expression plotmath pour afficher correctement ° et les points cardinaux, et coord_sf(clip = "off") + plot.margin évitent que hjust/vjust/nudge_x/nudge_y ne poussent les labels hors du cadre de tracé.
Deux détails à ajuster pour le français
degree_label est généré en anglais ("120"*degree*W plutôt que O) : il faut substituer W par O, ou reconstruire le label soi-même depuis la colonne degree (paste0(abs(degree), "°")) pour une version sans point cardinal, plus compacte mais moins explicite sur le sens est/ouest.
Quelle projection pour quel usage ?
Mercator reste pertinente pour la navigation ou le web (elle préserve les angles à tout niveau de zoom), Equal Earth pour comparer des surfaces entre pays, Robinson pour un rendu généraliste sans enjeu de précision. Le vrai réflexe : identifier la propriété (surface, angle, distance) qui compte pour le message, puis choisir la projection en conséquence.
Envie d’aller plus loin en cartographie avec R ? Dans le prochain article, on sort des sentiers battus avec trois projections plus exotiques : vue satellite, vue polaire et écorce d’orange, avec tous les pièges qu’il a fallu déjouer pour les faire fonctionner.