How to build a travel map in R (Updated 2025)

How to build a travel map in R (Updated 2025)

One of my first posts on this site was how to create a travel map in R. I always wanted to re-visit that map to add new destinations, to make the code more efficient, to use better packages.

In the old map, I found that the grouping by continent was a lot of coding effort for not much pay-off, alphabetical sorting would be easier and intuitive to readers. I also wanted to write the greying out unvisited countries into the code itself instead of photoshopping the flags manually. There were a lot of changes to make.

After much delay, I finally got around to doing just that! Here's the updated map, and if you want to create your own map like this in R you can take the code below and adapt it to your travels.


Code

Setup

pacman::p_load(
  char = c(
    "googlesheets4",
    "rnaturalearth",
    "countrycode",
    "tidyverse", # For everything
    "patchwork", # For better plot layout
    "rappdirs",
    "ggimage",  # For images on plots
    "giscoR",
    "magick",
    "rvest",
    "here",
    "sf",
    "fs"
    )
  )

graph_colours <- c(
  "Visited"        = "#F07C51",
  "Planned"        = "#FFCF66",
  "Travel_outline" = "#000000",
  "Country_fill"   = "#D4CEC4",
  "Border_line"    = "#FFFFFF"
  )

Read

Trips

df_trips <- googlesheets4::read_sheet(URL) %>%
  janitor::clean_names() %>%
  mutate(
    visited = if_else(status == "Visited", TRUE, FALSE),
    place_country = ifelse(
      is.na(place_country),
      paste(place, country, sep = ", "),
      place_country),
    iso2  = countrycode::countrycode(
      sourcevar = country, 
      origin = "country.name", 
      destination = "iso2c", 
      custom_match = c(
        "Kosovo" = "XK", 
        "United Kingdom" = "GB"
        )
      )
    )

Flags

df_flags <- list.files(path = here::here("Input", "Flags")) %>% 
  as_tibble() %>% 
  rename(file_name = value) %>% 
  mutate(
    file_path = paste0(here::here("Input", "Flags"), "/", file_name),
    country = str_extract(file_name, '(?<=-).*(?=\\.)') %>% 
      str_replace_all("-", " ") %>% 
      str_to_title(),
    iso2  = countrycode::countrycode(
      sourcevar = country, 
      origin = "country.name", 
      destination = "iso2c", 
      custom_match = c(
        "Kosovo" = "XK", 
        "Malasya" = "MY",
        "Micronesia" = "FM",
        "United Kingdom" = "GB")
      )
  ) %>% 
  filter(country != "Tuvalu 1")
df_flags <- df_flags %>% 
  filter(
    !is.na(iso2) 
    | country %in% c( # Add 10 so we reach 240 flags total
      #"Basque Country",
      "Somaliland",
      #"Rapa Nui",
      "Scotland", 
      #"Sardinia",
      #"Corsica",
      #"Hawaii",
      #"Sicily",
      "Wales",
      "Tibet"
      ),
    !country %in% c(
      # British Overseas Territories & Crown Dependencies
      "Anguilla",
      "Bermuda",
      "British Indian Ocean Territory",
      "British Virgin Islands",
      "Cayman Islands",
      "Falkland Islands",
      "Gibraltar",
      "Guernsey",
      "Jersey",
      "Montserrat",
      "Pitcairn Islands",
      "Turks And Caicos",
      # American Overseas Territories
      "American Samoa",
      "Northern Marianas Islands",
      "Guam",
      # Australian Overseas Territories
      "Cocos Island",
      "Christmas Island",
      "Norfolk Island",
      # Other
      "Aland Islands",
      "Aruba",
      "French Polynesia",
      "New Caledonia",
      "Northern Cyprus",
      "Sint Maarten"
      )
    ) %>% 
  arrange(country) %>% 
  mutate(
    x = 1 + (row_number() - 1) %%  30, # 22 is the n flags per row
    y = 1 + (row_number() - 1) %/% 30,
    y = rev(y), # To put A at the top, Z at the bottom
    y = if_else(y > max(y + 1) / 2, y + 15, y)
  )

Grey-out unvisited flags

df_flags <- df_flags %>%
  mutate(
    visited = case_when(
      !is.na(iso2) ~ iso2 %in% df_trips$iso2,
      TRUE ~ country %in% df_trips$country, 
      .default = NA
    )
  )

df_flags <- df_flags %>%
  mutate(
    file_grey = paste0(here::here("Input", "Flags", "Grey PNG"), "/grey-", file_name),
    file_plot = if_else(visited, file_path, file_grey)
  )

df_flags %>%
  filter(!fs::file_exists(file_grey)) %>%
  select(file_path, file_grey) %>%
  purrr::pwalk(~{
    img <- magick::image_read(..1)
    img <- magick::image_modulate(img, saturation = 0)  # preserve brightness; set sat=0
    magick::image_write(img, path = ..2, format = "png")
  })

Map data

World map

df_world <- gisco_get_countries(resolution = "60") %>% 
  janitor::clean_names() %>% 
  st_make_valid() %>%
  st_transform("+proj=latlon +lon_0=10") %>% 
  st_wrap_dateline() 

# Remove Antarctica 
df_world <- df_world %>% 
  filter(name_engl != "Antarctica")

States

df_states <- ne_states(
  country = c(
    "United States of America",
    "Australia", 
    "Argentina",
    "Algeria",
    "Brazil",
    "Canada",
    "China",
    "India"
    ), 
  returnclass = "sf") %>% 
  janitor::clean_names() %>% 
  st_make_valid() %>%
  st_transform("+proj=latlon +lon_0=10") %>% 
  st_wrap_dateline() 

Lakes

df_lakes <- rnaturalearth::ne_download(
  scale = "large", 
  type = "lakes", 
  category = "physical"
  ) %>% 
  filter(scalerank == 0)

Graph

Flags

graph_flags <- df_flags %>%  
  ggplot() +
  ggimage::geom_image(
    aes(
      x = x, 
      y = y,
      image = file_plot
      ), 
    size = 0.045
    ) +
  theme_void()

World

# Create map
graph_world <-
  ggplot() +
  ggplot2::geom_sf( # Countries
    data = df_world,
    fill = graph_colours["Country_fill"],
    colour = graph_colours["Border_line"],
    linewidth = 0.25
  ) +
  ggplot2::geom_sf( # State borders
    data = df_states,
    colour = graph_colours["Border_line"], # State border
    fill = NA,
    linewidth = 0.05
  ) +
  ggplot2::geom_sf( # Lakes
    data = df_lakes,
    fill = "#FFFFFF",
    colour = graph_colours["Border_line"],
    linewidth = 0.05
  )

# Add travel dots
graph_world <-
  graph_world +
  geom_point( # Back dots
    data = df_trips,
    aes(x = long, y = lat),
    colour = graph_colours["Travel_outline"],
    size = 1.25
  ) +
  geom_point( # Front dots
    data = df_trips,
    aes(x = long, y = lat, colour = status),
    size = 1
  ) +
  scale_color_manual(
    name = "",
    values = c(
      "Visited" = graph_colours[["Visited"]],
      "Planned" = graph_colours[["Planned"]]
    )
  )

# Crop the map
graph_world <-
  graph_world +
  ggplot2::coord_sf(
    expand = FALSE,
    default_crs = sf::st_crs(4326),
    xlim = c(-172, -169),
    ylim = c(90, -60)
  ) +
  theme(
    axis.text = element_blank(),
    axis.title = element_blank(),
    axis.ticks = element_blank(),
    panel.grid = element_blank(),
    panel.background = element_blank(),
    plot.background = element_blank(),
    legend.position = "none"
  )

Combine Graphs

graph_world_travel_flags <- graph_flags +
  patchwork::inset_element(
    p = graph_world,
    left = 0,
    right = 1,
    top = 0.875,
    bottom = 0.175
  )

Export

ggsave(
  plot = graph_world_travel_flags,
  path = here::here("Output"),
  filename = "graph_travel_and_flags_A4.png",
  device = "png",
  dpi = 600,
  units = "cm",
  width = 36.6,
  height = 16.0
)