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

I 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 serviced by the airline. I always wanted such a map for my own travel adventures. In this article I explain how to create a map of your own travels in the style of the Emirates Airlines route map.

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. To prevent errors from the Google maps API, I have added a while loop that runs until all destiationshave en geocoded.

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. This code also removes any return flights and splilts flights that crossed the date line.

You can view the recent version of the code and associated files in GitHub. In another post I have used the same principle to create a route map of flights between islands in the Pacific Ocean using the schedules from several internatioal airlines.

Create Air Travel Route Maps.
My personal Air Travel Route Maps in ggplot: A Visual Travel Diary
library(tidyverse)
library(ggmap)
library(ggrepel)

flights <- read_csv(flights.csv)
airports_file <- "airports.csv"
if (file.exists(airports_file)) {
  airports <- read_csv(airports_file)
  } else {
  airports <- data_frame(airport = NA, lon = NA, lat= NA)
}

destinations <- unique(c(flights$From, flights$To))
new_destinations <- destinations[!destinations %in% airports$airport]
while (length(new_destinations) > 0) {
    new_airports <- geocode(new_destinations) %>%
      mutate(airport = new_destinations) %>%
      select(airport, lon, lat)
    airports <- rbind(airports, new_airports) %>%
      filter(!is.na(lon) | !is.na(lat))
    new_destinations <- destinations[!destinations %in% airports$airport]
}
write_csv(airports, "airports.csv")

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 <- flights %>%
  filter(From != "R") %>%
  select(From, To)

flights <- merge(flights, airports, by.x = "From", by.y="airport")
flights <- merge(flights, airports, by.x = "To", by.y = "airport")
flights <- flights %>% 
  select(From, To, lon.x, lat.x, lon.y, lat.y) %>% 
  as_data_frame()

circ <- which(abs(flights$lon.y - flights$lon.x) > 180)
flights[circ,]
flights$lon.y[circ] <- ifelse(flights$lon.y[circ] < 0, 180, -180)
flights$lat.y[circ] <- rowSums(flights[circ, c("lat.x", "lat.y")]) / 2
leg2 <- airports %>%
  filter(airport %in% flights$To[circ]) %>%
  mutate(From = rep("", length(circ))) %>%
  mutate(lon.x = -flights$lon.y[circ], lat.x = flights$lat.y[circ]) %>%
  select(From, To = airport, lon.x, lat.x, lon.y = lon, lat.y = lat)
flights <- rbind(flights, leg2)

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)

worldmap <- borders("world", colour = "#efede1", fill = "#efede1") 
ggplot() + worldmap + 
    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 = .4) + 
    theme_void()

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

  1. This is great. How can I include the number of passengers in the first visualization?

    1. 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.

      <img src="https://r.prevos.net/wp-content/uploads/sites/11/2017/03/passengers.png&quot;

      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&quot;
      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", dpi = 300)

  2. 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?

      1. 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.

          1. 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.

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

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

  4. 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.

    1. The curves are not a projection of the great-circle distance. The curve is controlled by the curve geom in ggplot2. You can control the curvature in gplot. Please note that the purpose of this map is not to create a geospatially accurate representation of flight paths but simply to illustrate my journeys.

      1. 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!

        1. 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))
          
  5. Hi I tried this but the output quality was not good (route line was blurry and the world map looked a bit deformed?

    I also had a problem with query limit when converting the cities to latitude and longitude – is there a way to just put in the latitude and longitude myself, manually in excel or something?

    Thanks

    1. Also is there a way to zoom into a region of a map ? (eg if I am only interested in N America and Europe rather than the whole world? I cant find a way to zoom in or crop as otherwise the map annotations and curves become overcrowded.

      1. Hi Joha,

        You can obviously create the list of airports and their coordinates manually, but what would be the fun in that? I have recently also experienced issues with query limits. To circumvent this issue, I have updated the code to keep querying until all airports have been geocoded.

        You can zoom the map by passing xlim and ylim to the ggplot command. This example zooms on Australia. You need the coord_fixed function to prevent deformation.

        xlim(110, 155) + ylim(-45, -10) + 
        coord_fixed()
        

        You can specify the resolution in the ggsave command to improve the quality of the image.

        View the current version of the code on GitHub.

        Peter

        1. Hi Peter, Thank you for your help – this is great stuff, if only I can get it to work with my novice skills.

          Where do I put the zoom code in the original code and do I need to install any packages for it to work ? as currently when I put it before the ggsave, it says “non-numeric argument to binary operator”.

          And I still didn’t figure out how to save in high resolution using ggsave.

          By the way, the map of sydney routes with different thickness is really cool, but how can I try it with other data (such as a csv with to, from and passenger number columns?)

          Many, many thanks!

          Again I found this really cool using R to create a map – keep it up Peter.

          1. Hi Joha,

            You can add the xlim etc code to the end of the gglot command with a plus sign. ggplot i a layered function and you can keep adding things with the plus sign. Further below there is an example in a comment about Australia.

            To save in better resolution use ggsave("filename.ext", dpi = 300). Read the ggsave documentation for details.

            Always happy to help a newby, once you get to know R you will get hooked 🙂

        2. Thanks again,

          The quality looks better now, but I have some small issues :

          the lines are very crowded and also the map is spherical rather than flat -which makes the curved lines look out of place – any solution?

          Also for the Australia one, I haven’t found out how I can use my data rather than wikipedia to set thickness? Can I not put the data that determines how thick the lines are in one of the original csv files?

          Many thanks!

          1. Hi Joha,

            If lines are crossed then you must have a lot of data. I have no idea why your map would be spherical. If you share your code then I am happy to have a look. Use the contact page for sharing

            Setting the thickness depends on what you like to visualise. In the Australia map it was passenger numbers. What is in your data? You could add thickness to indicate the number of times you took the route. Whatever variable is in the data frame, ggmap can use it for line thickness.

            Peter.

        3. here is the code (I dont have that many lines, but its mostly lots of lines going from point A to other points). The final output of mine as not as good as the one on this webpage.

          Flightpath map

          https://r.prevos.net/create-air-travel-route-maps/

          Init

          library(tidyverse)
          library(ggmap)
          library(ggrepel)

          Read flight and airports lists

          flights <- read.csv(“Geography/flights.csv”, stringsAsFactors = FALSE)
          airports <- read.csv(“Geography/airports.csv”, stringsAsFactors = FALSE)

          Lookup coordinates

          destinations <- unique(c(flights$From, flights$To))
          new_destinations 0) {
          coords <- geocode(new_destinations)
          airports <- rbind(airports, data.frame(airport = new_destinations, coords))
          write.csv(airports, “Geography/airports.csv”, row.names = FALSE)
          }

          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 %
          filter(From != “R”) %>%
          select(From, To)

          Add coordinates to flight list

          flights <- merge(flights, airports, by.x="From", by.y="airport")
          flights <- merge(flights, airports, by.x="To", by.y="airport")
          flights %
          select(From, To, lon.x, lat.x, lon.y, lat.y) %>%
          as_data_frame()

          Circumnaviation

          circ 180)
          flights[circ, ]
          flights$lon.y[circ] <- ifelse(flights$lon.y[circ] < 0, 180, -180)
          flights$lat.y[circ] <- rowSums(flights[circ, c(“lat.x”, “lat.y”)]) / 2
          flights <- rbind(flights,
          data_frame(From = rep(“”, length(circ)),
          To = flights$To[circ],
          lon.x = -flights$lon.y[circ],
          lat.x = flights$lat.y[circ],
          lon.y = airports[airports$airport %in% flights$To[circ], “lon”],
          lat.y = airports[airports$airport %in% flights$To[circ], “lat”])
          )

          Remove country names

          airports$airport <- as.character(airports$airport)
          comma 0)] 0)], 1, comma[comma > 0] – 1)

          Plot flight routes

          worldmap <- borders(“world”, colour=”#efede1″, fill=”#efede1″) # create a layer of borders
          ggplot() + worldmap + xlim(-20, 30) + ylim(10, 62) +
          coord_fixed()+

          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 = 1.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()
          )

          ggsave(“Geography/flights_map.png”,dpi=300)

          geom_curve curvature test

          data.frame(x1 = c(1, 3),
          y1 = c(2, 3),
          x2 = c(2, 2),
          y2 = c(1, 2)) %>%
          ggplot() + geom_curve(aes(x = x1, y = y1, xend = x2, yend = y2))

          1. Hi Peter, yes it’s the code from your github page but for some reason the output image is spherical rather than flat and generally not as hq as your one.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.