Create Air Travel Route Maps in ggplot: A Visual Travel Diary

Create Air Travel Route Maps: Emirates Route MapI have been lucky to fly to a few countries around the world. Like any other bored traveller, I thumb through the airline magazines and look at the air travel route maps. These maps are beautifully stylised depictions of the world with gently curved lines between the many destinations. I always wanted such a map for my own travel adventures.

Create Air Travel Route Maps using ggplot2

The first step was to create a list of all the places I have flown between at least once. Paging through my travel photos and diaries, I managed to create a pretty complete list. The structure of this document is simply a list of all routes (From, To) and every flight only gets counted once. The next step finds the spatial coordinates for each airport by searching Google Maps using the geocode function from the ggmap package. In some instances, I had to add the country name to avoid confusion between places.

# Read flight list
flights <- read.csv("flights.csv", stringsAsFactors = FALSE)

# Lookup coordinates
library(ggmap)
airports <- unique(c(flights$From, flights$To))
coords <- geocode(airports)
airports <- data.frame(airport=airports, coords)

We now we have a data frame of airports with their coordinates and can create air travel route maps. The data frames are merged so that we can create air travel route maps using the curve geom. The borders function of ggplot2 creates the map data. The ggrepel package helps to prevent overplotting of text.

# Add coordinates to flight list
flights <- merge(flights, airports, by.x="To", by.y="airport")
flights <- merge(flights, airports, by.x="From", by.y="airport")

# Plot flight routes
library(ggplot2)
library(ggrepel)
worldmap <- borders("world", colour="#efede1", fill="#efede1") # create a layer of borders
ggplot() + worldmap + 
 geom_curve(data=flights, aes(x = lon.x, y = lat.x, xend = lon.y, yend = lat.y), col = "#b29e7d", size = 1, curvature = .2) + 
 geom_point(data=airports, aes(x = lon, y = lat), col = "#970027") + 
 geom_text_repel(data=airports, aes(x = lon, y = lat, label = airport), col = "black", size = 2, segment.color = NA) + 
 theme(panel.background = element_rect(fill="white"), 
 axis.line = element_blank(),
 axis.text.x = element_blank(),
 axis.text.y = element_blank(),
 axis.ticks = element_blank(),
 axis.title.x = element_blank(),
 axis.title.y = element_blank()
 )

I also tried to use ggmap package to display the maps to get a satellite image background. This did not work because the curve geom struggles with the map projection methods used in ggmap. Another problem is that the flight from Auckland to Los Angeles is drawn the wrong way. I hope no flat-earthers will see this map because they might use it as proof that the world is flat.

You can view the recent version of the code and associated files in GitHub.

20 thoughts on “Create Air Travel Route Maps in ggplot: A Visual Travel Diary

  1. Hi, great map and post. What was your solution to get the paths to curve in different (appropriate) directions? The code you provide has all the curves going the same direction.

      • Yes, I see that they are from the curve geom, however that geom accepts either a positive or negative curve value. The example plot you show has both positive and negative curves in a way that limits how frequently they overlap – I was curious how you did this (random sign for curvature? Something more defined?)

        Thanks!

        • Upon experimentation it seems that the curve geom adjusts the sign of the curvature depending on whether the line goes up or down. See below:

          data.frame(x1 = c(1, 3), 
                     y1 = c(2, 3), 
                     x2 = c(2, 2),
                     y2 = c(1, 2)) %>% 
            ggplot() + 
              geom_curve(data = df, aes(x = x1, y = y1, xend = x2, yend = y2))
          
  2. Pingback: Pacific Island Hopping using R and iGraph – Cloud Data Architect

  3. Pingback: Pacific Island Hopping using R and iGraph | The Devil is in the Data

  4. Update: the code in this post has two issue: a return flight is shown as two lines and flights that travel across the pacific are drawn wrong. The old version also contained some names of countries to ensure Google picks the correct coordinate. This new version of the code fixes these issues:

    # Remove return flights
    d <- vector()
    for (i in 1:nrow(flights)) {
        d <- which(paste(flights$From, flights$To) %in% paste(flights$To[i], flights$From[i]))
        flights$From[d] <- "R"
    }
    flights <- subset(flights, From != "R")
    
    # Circumnaviation
    circ <- which(abs(flights$lon.y - flights$lon.x) > 180)
    flights$lon.y[circ] <- 180
    flights$lat.y[circ] <- sum(flights[circ, c("lat.x", "lat.y")])/2
    
    flights[circ,]
    flights <- rbind(flights, data.frame(From = rep("", length(circ)),
                              To = flights$To[circ],
                              lon.x = -180,
                              lat.x = flights$lat.y[circ],
                              lon.y = airports[airports$airport == flights$To[circ], "lon"],
                              lat.y = airports[airports$airport == flights$To[circ], "lat"]
                              ))
    flights$To[circ] <- ""
    
    # Remove country names
    airports$airport <- as.character(airports$airport)
    comma <- regexpr(",", airports$airport)
    airports$airport[which(comma > 0)] <- substr(airports$airport[which(comma > 0)], 1, comma[comma > 0] - 1)
    

    This code creates a cleaner and correct version of the map.

  5. Pingback: This Week in Data Science (March 14, 2017) – Be Analytics

  6. Pingback: Linkdump #32 | WZB Data Science Blog

  7. I’ve been looking for code to do a route map – thank you. I’m having a problem getting the code to work in my environment. I get the following error: Error in eval(expr, envir, enclos) : object ‘lon.x’ not found. Any thoughts?

      • I was able to resolve the problem by moving the geom_curve call after the geom_text_repel call. Everything works great now. Thank you for sharing.

        I’m finding it helpfull to change the line width to 0.2 – to my eye, it looks cleaner.

          • Here is the ggplot2 call that I am using:

            ggplot() + worldmap + 
            #  geom_curve(data=flights, aes(x = lon.x, y = lat.x, xend = lon.y, yend = lat.y), col = "#b29e7d", size = 1, curvature = .2) + 
              geom_point(data=airports, aes(x = lon, y = lat), col = "#970027") + 
              geom_text_repel(data=airports, aes(x = lon, y = lat, label = airport), col = "black", size = 2, segment.color = NA) + 
              geom_curve(data=flights, aes(x = lon.x, y = lat.x, xend = lon.y, yend = lat.y), col = "#b29e7d", size = .2, curvature = .2) +
              theme(panel.background = element_rect(fill="white"), 
                    axis.line = element_blank(),
                    axis.text.x = element_blank(),
                    axis.text.y = element_blank(),
                    axis.ticks = element_blank(),
                    axis.title.x = element_blank(),
                    axis.title.y = element_blank()
              )
            

            It is almost identical to your code except:

            a) the geom_curve call is reordered
            b) the size in the geom_curve call has been changed to 0.2

            Thanks again for posting this – I love it.

    • Hi, I have used some data from a Wikipedia page on airline travel in Australia and visualised it by varying the line thickness. This map visualises domestic passenger numbers from Sydney.

      This is the code:

      # Passegener volumes
      library(rvest)
      url <- "https://en.wikipedia.org/wiki/List_of_the_busiest_air_routes_in_Australia_by_passenger_traffic"
      sydneytraffic <- url %>%
          read_html() %>%
          html_nodes(xpath = '//*[@id="mw-content-text"]/table[1]') %>%
          html_table()
      sydneytraffic <- sydneytraffic[[1]]
      sydneytraffic <- data.frame(From = "Sydney", To = sydneytraffic$`To Country / City`[-11], Passengers = sydneytraffic$`Passengers
      Jun 2012`[-11])
      
      # Lookup coordinates
      library(ggmap)
      airports <- unique(c("Sydney", as.character(sydneytraffic$To)))
      coords <- geocode(airports)
      airports <- data.frame(airport=airports, coords)
      
      # Add coordinates to flight list
      sydneytraffic <- merge(sydneytraffic, airports, by.x="To", by.y="airport")
      sydneytraffic <- merge(sydneytraffic, airports, by.x="From", by.y="airport")
      sydneytraffic$Passengers <- as.numeric(gsub(",", "", as.character(sydneytraffic$Passengers)))
      
      sydneytraffic$linew <- sydneytraffic$Passengers/min(sydneytraffic$Passengers)
      
      # Plot flight routes
      library(ggplot2)
      library(ggrepel)
      worldmap <- borders("world", colour="#efede1", fill="#efede1") # create a layer of borders
      ggplot() + worldmap + 
          geom_curve(data=sydneytraffic, aes(x = lon.x, y = lat.x, xend = lon.y, yend = lat.y), col = "#b29e7d", 
                     size = sydneytraffic$linew/2, curvature = .2) + 
          geom_point(data=airports, aes(x = lon, y = lat), col = "#970027") + 
          geom_text_repel(data=airports, aes(x = lon, y = lat, label = airport), col = "black", size = 2, segment.color = NA) + 
          xlim(110, 155) + ylim(-45, -10) + coord_fixed() + 
          theme(panel.background = element_rect(fill="white"), 
                axis.line = element_blank(),
                axis.text.x = element_blank(),
                axis.text.y = element_blank(),
                axis.ticks = element_blank(),
                axis.title.x = element_blank(),
                axis.title.y = element_blank()
          )
      ggsave("passengers.png")
      
  8. Pingback: Create Air Travel Route Maps in ggplot: A Visual Travel Diary – Mubashir Qasim

Feel free to share your thoughts about this article