Geographical data

Maps

Roland Krause

MADS6

Wednesday, 1 April 2026

Introduction

Maps

Learning objectives

  • Paths to simple maps with ggplot2
  • Limitations of default maps
  • Projected maps with standard geographic packages
    • spatial features with sf
  • Learn about the Coordinate Reference System (CRS)
  • Drawing choropleths

Material

  • maps and ggplot2 for simple maps
  • sf for spatial maps
  • rnaturalearth for sf compatible data sources
  • Claus O. Wilke’s Data visualisation guides
  • Geocomputating with R

Basic maps

Basic map data in R

Basics from maps

library(maps) # R Built-in library
iso3166|> gt()
ggplot(world) + # from maps
  geom_sf()

Basic maps with ggplot

ggplot knows the world

library(ggplot2)

gworld <- ggplot2::map_data("world")
luxembourg <- map_data("world", region = "Luxembourg")

gworld |> 
  filter(region == "Luxembourg") |> 
  slice_sample(n = 10)

… which we can easily visualise

ggplot(gworld) +
  geom_line(aes(x = long, y = lat))

Basic maps with ggplot

ggplot knows the world

library(ggplot2)

gworld <- map_data("world")

gworld |> 
  slice_sample(n = 10)

… which we can easily visualise if groups are respected

ggplot(gworld) +
  geom_polygon(aes(x = long, y = lat, group = group))

geom_polygon() give us countries

ggplot(gworld) +
  geom_polygon(aes(x=long, 
                   y = lat, 
                   group = group),
               color = "white") 

geom_map()

ggplot(gworld) +
  geom_map(map = gworld, 
           data = gworld,
           aes(map_id = region,
               fill = region)) +
  expand_limits(x = gworld$long, y = gworld$lat) +
  guides(fill = "none") +
  scale_fill_discrete_qualitative(palette = "Dark 2")

Caution

map() != mapping()

Great map, but input data is limited. And geom_map() is barely explained.

Zooming in on Luxembourg

msa_2230 = data.frame(long = c(5.949440), 
                      lat = c(49.504432))
ggplot() +
  geom_map(data = gworld,
    map = gworld,
           aes(map_id = region,
               fill = region),
           color = "black") +
  geom_point(data = msa_2230, 
    aes(long, lat)) +
  expand_limits(x = gworld$long, y = gworld$lat) +
  coord_cartesian(xlim = c(5.5, 6.75), 
                  ylim = c(48.5, 51)) + 
  scale_fill_discrete_qualitative(palette = "Dark 2") +
  guides(fill = "none")

Projections

Parallels (latitude) and meridians (longitude)

There are many ways to project onto a 2D plane

There are many ways to project onto a 2D plane

Mercator projection

Shapes are preserved, areas are severely distorted.

There are many ways to project onto a 2D plane

Goode homolosine

Areas are preserved, shapes are somewhat distorted

Projecting the US

Note

Alaska, Hawaii, and the lower 48 are far apart; difficult to show on one map

Projecting the US

A fair, area-preserving projection

Simple features - sf

The sf package: Simple Features in R

Simple features in R

#euro_map <- world[world$continent == "Europe", ] 
euro_map <- rnaturalearth::ne_countries(scale = 110, 
                                        returnclass = 'sf', 
                                        continent = "Europe")

euro_map

Plotting Europe

Code
ggplot(euro_map) +  geom_sf() 

Plotting Europe geographically

Code
limits_x <- c(-25, 40)
limits_y <- c(32, 72.5)

euro_map |> 
  ggplot() +
  geom_sf() +
  coord_sf(xlim = limits_x,
           ylim = limits_y)

GPD per person in Europe

Code
euro_map |>
  ggplot() +
   geom_sf(aes(fill = gdp_md/pop_est)) + 
    coord_sf(xlim = limits_x,
           ylim = limits_y) +
  guides(fill = guide_legend(title = "GDP/Population")) +
  theme(legend.position = "bottom") +
  scale_fill_binned_sequential()

GPD per person in Europe - white borders

euro_map |>
  ggplot() +
  geom_sf(aes(fill = gdp_md/pop_est),
          colour = "white") + 
  coord_sf(xlim = limits_x,
           ylim = limits_y) +
  guides(fill = guide_legend(title = "GDP/Population"))+
  theme(legend.position = "bottom") +
  scale_fill_binned_sequential()

Select Luxembourg

euro_map |> 
   filter(name == "Luxembourg") |> 
   relocate(geometry)

#lux <-  world[world$name_long == "Luxembourg", ]

Plotting Luxembourg

ggplot(
euro_map |> 
   filter(name == "Luxembourg") |> 
   relocate(geometry)) + geom_sf()

More resolution is needed

euro_map50 <- rnaturalearth::ne_countries(
  scale = 50, 
  type = 'map_units', 
  returnclass = 'sf')

euro_map50 |> filter(name == "Luxembourg") |> 
  ggplot() +
  geom_sf()

High-resolution maps from Shapefiles

f = system.file("shapes/world.gpkg", package = "spData")
world = read_sf(f)
lux_shp <- sf::st_read("data/LIMADM_COMMUNES.shp")
ggplot(lux_shp) + 
  geom_sf(aes(fill = CANTON)) +
  theme_bw() -> lux_commune_map
lux_commune_map +
#  paletteer::scale_fill_paletteer_d("rtist::vangogh")
  scale_fill_discrete_qualitative(palette = "Dark 3")

Four color theorem

Code
commune <- c(
  Redange = "#00A4E1",
  Grevenmacher = "#00A4E1",
  Luxembourg = "#777",
  Vianden = "#00A4E1",
  Mersch = "#DC021B",
  "Esch-sur-Alzette" = "#00A4E1",
  Wiltz = "#DC021B",
  Clervaux = "#FFF",
  Diekirch = "#777",
  Remich =  "#FFF",
  Capellen = "#FFF",
  Echternach = "#FFF"
)

lux_shp |> 
  group_by(CANTON) |> 
  mutate(n = row_number(),
    canton_label = if_else(n ==1, CANTON, ""),
    # centroid of each elements (county level)
    lon = st_coordinates(st_centroid(geometry))[,1],
    lat = st_coordinates(st_centroid(geometry))[,2],
    # median lon/lat of county centroids. It's a hack.
    med_lon = median(lon),
    med_lat = median(lat))  |> 
  ungroup() |> 
ggplot() + 
  geom_sf(aes(fill = CANTON) , 
          alpha = 0.7, 
          show.legend = FALSE, linewidth = 0.1) +
  theme_bw()  + scale_fill_manual(values = commune) +
  geom_text(aes(x = med_lon, y = med_lat, label = canton_label), size = 4) +
  # geom_sf_text does not take x, y
  labs(title = "The cantons of Luxembourg",
       x = NULL, 
       y = NULL) 

Projection

CRS 4326

euro_map |> 
  ggplot() +
 geom_sf(aes(fill = gdp_md/pop_est)) + 
    coord_sf(xlim = c(-15, 35),
           ylim = c(32, 72.5), 
           crs = 4326) + #<< 
  guides(fill = "none")

CRS 4181

euro_map |> 
  ggplot() +
 geom_sf(aes(fill = gdp_md/pop_est)) + 
    coord_sf(xlim = c(-15, 35),
           ylim = c(32, 72.5),
           crs = 4181) + #<<
  guides(fill = "none")

Luxembourg by different CRS

CRS 4326

lux_commune_map + coord_sf(crs = 4326)

CRS 4100

lux_commune_map + coord_sf(crs = 4100)

Transforming using the Coordinate Reference System (CRS)

(ggplot(data = world) +
  geom_sf(mapping = aes(fill = continent)) +
  scale_fill_viridis_d(name = "Continent",
                      na.value = "grey50") +
  theme(legend.position = "bottom") -> continent_map)

We want to add our spot; just using coordinates will not work after transformations.

msa_2230
  • coord_sf(crs = 4100)

Projections

continent_map +
  # Converting our position
  geom_sf(data =
            st_as_sf(msa_2230, 
                     coords = c("long", "lat"), 
                     crs = 4181), #<< Spatial reference
          color = "red") 
st_as_sf(msa_2230,
         coords = c("long", "lat"), 
         crs = 4181)

CRS4181: Luxembourg 1930

View from the pole

continent_map +
  # Converting our position
  geom_sf(data =
            st_as_sf(msa_2230, 
                     coords = c("long", "lat"), 
                     crs = 4181), color = "red") +
  # rotating globe
  coord_sf(crs = 3572) #<<

Removing Antarctica

world |> 
  filter(continent != "Antarctica") |> 
ggplot() +
  geom_sf() +
  geom_sf(mapping = aes(fill = continent)) +
  scale_fill_viridis_d(name = "Continent",
                      na.value = "grey50") +
  # Converting our position
  geom_sf(data =
            st_as_sf(msa_2230, 
                     coords = c("long", "lat"), 
                     crs = 4181), color = "red") +
  # rotating globe
  coord_sf(crs = 3572) #<<

Before we stop

We looked at

  • Basic maps in ggplot
  • Spatial features in sf
  • Data from the Natural Earth project in R
  • Importing Shapefiles for high-resolution maps
  • Using the Coordinate Reference System

Acknowledgements

  • Claus O. Wilke

Thank for your attention!