Search code examples
rggplot2mapsgeom-sf

Adding labels to maps with lines


I would like to plot a map using geom_sf() and add labels like the following hand-manipulated map (but we don't need the colors or the legend, that's just the map we had on hand).

map_with_labels

New map:

The map data can be downloaded here. The map consist of three columns: "Kommunenummer" (municipality number), "Kommunenavn" (municipality name), "geometry". The two sites pointed to in the map above are "Volda" (southern) and "Tromsø" (northern) municipalities.

I guess we would need to determine the centroid position of those municipalities and then use something like geom_text_repel() to point to them, but I haven't gotten it to work well. Does anyone have any tips?

Minimal working example:

mapdata <- st_read("~/Downloads/Basisdata_0000_Norge_25833_NorskeFylkerKommunerIllustrasjonsdata2021_GeoJSON/kommuner2021.json")

ggplot(mapdata) +
  geom_sf() +
  theme_minimal() +
  theme(
    panel.grid.major = element_blank(),   # Removes major grid lines
    panel.grid.minor = element_blank(),   # Removes minor grid lines
    axis.text.x = element_blank(),        # Removes x-axis labels (Longitude)
    axis.text.y = element_blank(),        # Removes y-axis labels (Latitude)
    axis.title.x = element_blank(),       # Removes x-axis title
    axis.title.y = element_blank()        # Removes y-axis title
  )

enter image description here


Solution

  • Unless you are working with hundreds of labels, ggrepel is often not the most efficient approach. You can spend an inordinate amount of time getting labels to plot correctly, and correcting issues can create issues elsewhere.

    I generally find it easier to stick with the sf package and create label geometries manually. It can take some trial and error to get the label locations right, but with only a few labels, it's almost always easier to get the desired result compared to using ggrepel. Plus, I am not sure that ggrepel accepts functions from e.g. ggtext to allow plot labels with different sized fonts.

    This repex uses a uniform offset of -/+0.5 decimal degrees for both labels. There's nothing stopping you setting different offsets for each label. As my Norwegian is a little 'rusty', I could not download your linked data, so I have applied this workflow to the built-in nc dataset. Your offsets may differ relative to the CRS of your actual data.

    I have used st_point_on_surface() instead of st_centroid() to ensure the point does not get plotted outside the polygon (which can occur with some irregular-shaped polygons). Note st_point_on_surface() is unlikely to plot the true centroid location in cases where st_centroid() would otherwise have plotted it inside the polygon. If this is undesriable, replace st_point_on_surface() with st_centroid().

    Load packages and example map data:

    library(sf)
    library(dplyr)
    library(ggplot2)
    
    # Example data
    mapdata <- st_read(system.file("shape/nc.shp", package = "sf"))
    

    Generate label geometries:

    # Create 'centroids' of municipalities (you can safely ignore the warning)
    sf_centroids <- mapdata %>%
      filter(NAME %in% c("Watauga", "Swain")) %>% # proxies for your municipalities
      st_point_on_surface()
    
    # Create points for label locations
    sf_labels <- sf_centroids %>%
      mutate(lon = st_coordinates(.)[,1] - 0.5,
             lat = st_coordinates(.)[,2] + 0.5) %>%
      st_drop_geometry() %>%
      st_as_sf(coords = c("lon", "lat")) %>%
      st_set_crs(st_crs(sf_centroids))
    
    # Create lines between label locations and centroids
    sf_lines <- rbind(sf_labels[,"NAME"], 
                      sf_centroids[,"NAME"]) %>%
      group_by(NAME) %>%
      summarise(geometry = st_union(geometry)) %>%
      st_cast("LINESTRING")
    

    Create labels lists and plot:

    # Create labels
    labels1 <- c("Exp's 2-3 - Northern Norway:", "Exp 1 - Sunnmøre:")
    labels2 <- c("Bokmål vs. Northern Norwegian Dialect", "Bokmål")
    
    # Plot labels using geo_sf_text
    ggplot() + 
      geom_sf(data = mapdata) +
      geom_sf(data = sf_lines, linewidth = 0.15) +
      geom_sf(data = sf_labels, shape = 21, fill = "white", size = 0.75) +
      geom_sf(data = sf_centroids, size = 0.75) +
      geom_sf_text(data = sf_labels,
                   aes(label = labels1),
                   size = 2,
                   vjust = -2,
                   fun.geometry = st_centroid,
                   colour = "black") +
      geom_sf_text(data = sf_labels,
                   aes(label = labels2),
                   size = 1.5,
                   vjust = -1,
                   fun.geometry = st_centroid,
                   colour = "black") +
      coord_sf(clip = "off") +
      theme_void()
    

    result