Global Airports and Flights

Global Airports, Busiest Airports, Longest Flights

Maps
A4 Size Viz
Author

Aditya Dahiya

Published

May 19, 2024

Mapping the World’s Airports including the busiest; and the longest flights

This comprehensive map provides a visual representation of the global aviation landscape, showcasing 7,698 airports across 237 countries and territories. The airport data is sourced from the OpenFlights database, accessible via the airportr package (Shkolnik 2019), which offers detailed information on over 15 million flights and more than 4,000 airlines. Additionally, the map highlights the 50 busiest airports of 2023, marked with larger orange dots, using passenger traffic data from Wikipedia. The 10 longest flight routes (data from OAG) are also depicted, plotted using R’s geosphere package (Hijmans 2022).

This visualization reveals that the longest flights often originate from the busiest airports, predominantly located in developed countries or major transit hubs like the UAE.

The global distribution of 7,698 airports across 237 countries and territories, based on data from the OpenFlights database. Highlighted are the 50 busiest airports of2023 by passenger traffic, marked with larger dots. Additionally, the map shows the 10 longest flight routes in the world. Interestingly, the longest flights typically originate from these busiest airports, which are predominantly located in developed countries or significant transit hubs like the UAE.

The global distribution of 7,698 airports across 237 countries and territories, based on data from the OpenFlights database. Highlighted are the 50 busiest airports of2023 by passenger traffic, marked with larger dots. Additionally, the map shows the 10 longest flight routes in the world. Interestingly, the longest flights typically originate from these busiest airports, which are predominantly located in developed countries or significant transit hubs like the UAE.

How I made this graphic?

Loading required libraries, data import & creating custom functions.

Code
# Data Import and Wrangling Tools
library(tidyverse)            # All things tidy

# Final plot tools
library(scales)               # Nice Scales for ggplot2
library(fontawesome)          # Icons display in ggplot2
library(ggtext)               # Markdown text support for ggplot2
library(showtext)             # Display fonts in ggplot2
library(colorspace)           # Lighten and Darken colours
library(ggthemes)             # Themes for ggplot2
library(patchwork)            # Combining plots

# Mapping tools
library(sf)                   # All spatial objects in R
library(airportr)             # Airports Data for the world
library(rnaturalearth)        # World Map

# Data on Airports: R Package airportr
# https://cran.r-project.org/web/packages/airportr/vignettes/Introduction_to_Airportr.html  

Get World Map and Map of India

Code
world_map <- rnaturalearth::ne_countries(
  scale = "large",
  returnclass = "sf"
) |> 
  filter(sovereignt != "Antarctica")

india_map <- read_sf(
  here::here(
    "data", "india_map", "India_State_Boundary.shp"
    )
  ) |> 
  st_simplify(dTolerance = 100) |> 
  rmapshaper::ms_dissolve() |>     # removing internal state borders
  rmapshaper::ms_simplify(keep = 0.2)

Data Analysis, Wrangling and web-scraping for additional datapoints

Code
# Names of airports around the world
df <- airportr::airports |> 
  janitor::clean_names() |> 
  select(name, iata, city, country, 
         cc2 = country_code_alpha_2, latitude,
         lat = latitude, lon = longitude) |> 
  rename(airport = name) |> 
  mutate(latitude = lat, longitude = lon) |> 
  st_as_sf(coords = c("lon", "lat"), crs = 4326)


# List of 10 longest flights in the world
longest_flights <- tibble(
  from = c("JFK", "EWR", "AKL", "LHR", "DFW",
           "AKL", "AKL", "LAX", "BLR", "IAH"),
  to = c("SIN", "SIN", "DOH", "PER", "MEL",
         "JFK", "DXB", "SIN", "SFO", "SYD"),
  airlines = c("Singapore Airlines", "Singapore Airlines",
               "Qatar Airways", "Qantas Airways",
               "Qantas Airways", "Air New Zealand",
               "Emirates", "Singapore Airlines", 
               "Air India", "United Airlines"),
  distance = c(15332, 15329, 14526, 14499, 14468,
               14209, 14193, 14096, 13982, 13829)
)


# Extracting the busiest airports in the world
library(rvest)
# Define the URL of the Wikipedia page
url <- "https://en.wikipedia.org/wiki/List_of_busiest_airports_by_passenger_traffic"
# Read the HTML content of the page and
# Extract the table with the 2023 statistics
busiest_airports <- read_html(url) |> 
  html_node(xpath = '//*[@id="mw-content-text"]/div[1]/table[1]') %>% 
  html_table(fill = TRUE) |> 
  as_tibble() |> 
  janitor::clean_names() |> 
  separate_wider_delim(
    cols = code_iata_icao,
    delim = "/",
    names = c("iata", "icao")
  ) |> 
  select(-rankchange, -percent_change) |> 
  mutate(totalpassengers = parse_number(totalpassengers))


# Plotting the longest routes in the world
longest_routes <- longest_flights |> 
  left_join(df |> select(from = iata, 
                         from_lat = latitude, 
                         from_lon = longitude) |> 
              as_tibble() |> 
              select(-geometry)) |> 
  left_join(df |> select(to = iata, 
                         to_lat = latitude, 
                         to_lon = longitude) |> 
              as_tibble() |> 
              select(-geometry))

longest_route_airports <- unique(longest_routes$from, longest_routes$to)


# Technique Credits: https://github.com/xmc811/flightplot/blob/master/R/main.R
# Credits: Mingchu Xu
# https://www.linkedin.com/in/mingchu-xu-467a0946/
# On Twitter: @xmc811
routes <- geosphere::gcIntermediate(
  p1 = longest_routes |> select(from_lon, from_lat),
  p2 = longest_routes |> select(to_lon, to_lat),
  n = 1000,
  breakAtDateLine = TRUE,
  addStartEnd = TRUE,
  sp = TRUE) |> 
  sf::st_as_sf() |> 
  mutate(rank = row_number())

# Test Plot
# ggplot() +
#   geom_sf(
#     data = ne_countries(returnclass = "sf") |> 
#       filter(sovereignt != "Antarctica"),
#     fill  = "white"
#   ) +
#   geom_sf(
#     data = routes
#   ) +
#   coord_sf(crs = "ESRI:54030") +
#   ggthemes::theme_map()

# Joining Airports Dataset with the busiest airports dataset
plotdf <- df |> 
  left_join(busiest_airports) |> 
  mutate(
    busiest = iata %in% busiest_airports$iata,
    longest = iata %in% longest_route_airports,
    alpha_var = iata %in% c(busiest_airports$iata,
                        longest_route_airports)
  ) |> 
  mutate(totalpassengers = replace_na(totalpassengers, 1e7)) |> 
  arrange(alpha_var)

Visualization Parameters

Code
# Font for titles
font_add_google("Farsan",
  family = "title_font"
) 

# Font for the caption
font_add_google("Saira Extra Condensed",
  family = "caption_font"
) 

# Font for plot text
font_add_google("Saira Condensed",
  family = "body_font"
) 

showtext_auto()

# Background Colour
mypal <- c("#188a8d", "#ebfcfa", "white", "#013840", "#f77919")
# orange_palette <- paletteer::paletteer_c(
#   "grDevices::Oranges", 
#   n = 30)[8:17]

orange_palette <- paletteer::paletteer_d(
  "MetBrewer::Signac", 
  direction = -1)[1:10]

bg_col <- mypal[2] |> lighten(0.5)
# Colour for the text using FAA's offical website colour
text_col <- mypal[4]  

# Colour for highlighted text
text_hil <- "#005f6b"
text_hil2 <- mypal[5]

# Define Base Text Size
ts <- 70 

# Caption stuff for the plot
sysfonts::font_add(
  family = "Font Awesome 6 Brands",
  regular = here::here("docs", "Font Awesome 6 Brands-Regular-400.otf")
)
github <- "&#xf09b"
github_username <- "aditya-dahiya"
xtwitter <- "&#xe61b"
xtwitter_username <- "@adityadahiyaias"
social_caption_1 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{github};</span> <span style='color: {text_col}'>{github_username}  </span>")
social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_col}'>{xtwitter_username}</span>")

Plot Text

Code
plot_title <- "Airports Worldwide: Busiest Hubs and Longest Routes"

plot_caption <- paste0(
  "**Code:** ", 
  social_caption_1, 
  " |  **Graphics:** ", 
  social_caption_2
  )

plot_subtitle <- glue::glue("The global distribution of 7,698 airports across 237 countries and territories, based on data from the OpenFlights database. Highlighted are <b style='color:{mypal[5]}'>the 50 busiest airports</b> of<br>2023 by passenger traffic, marked with larger dots. Additionally, the map shows the <b>10 longest flight routes</b> in the world. Interestingly, the longest flights typically<br>originate from these busiest airports, which are predominantly located in developed countries or significant transit hubs like the UAE.")

text_annotation_1 <- "About the Data: The data for the map is sourced from the {rnaturalearth} R-package, the airport locations are from the OpenFlights database, accessed through the {airportr} R-package, which provides comprehensive information on global airports, flights, and airlines. The 50 busiest airports for the year 2023 were identified using passenger traffic data from a specific Wikipedia page, while the 10 longest flights for 2023 were sourced from Wikipedia too. For visualizing the longest flight routes, R's {geosphere} package was employed, which allows for advanced geocomputation using technique by Mingchu Xu (X: @xmc811). These techniques collectively enable a detailed and accurate depiction of global airport locations, major airports by passenger traffic, and extensive flight routes."

Base Plot

Code
# Final Plot: A Composite world map with all Airports and 
# the busiest Airports in big size (with name and annual passengers)
# And, in a different colour, the 10 longest flights

g_base <-  ggplot() +
  geom_sf(
    data = world_map,
    fill  = mypal[3],
    colour = "grey50",
    linewidth = 0.1
  ) +
  geom_sf(
    data = india_map,
    fill = mypal[3],
    colour = "grey50",
    linewidth = 0.2
  ) +
  geom_sf(
    data = routes,
    mapping = aes(colour = as_factor(rank)),
    alpha = 0.9,
    linewidth = 1
  ) +
  scale_colour_manual(values = orange_palette) +
  ggnewscale::new_scale_colour() +
  geom_sf(
    data = plotdf,
    mapping = aes(
      fill = busiest,
      size = busiest,
      alpha = alpha_var
    ),
    colour = mypal[4],
    pch = 21
  ) +
  scale_fill_manual(values = c(mypal[4], mypal[5])) +
  scale_alpha_manual(values = c(0.2, 0.9)) +
  scale_size_manual(values = c(1, 5)) +
  # coord_sf(crs = "ESRI:54030") +
  coord_sf(ylim = c(-90, 90)) +
  labs(
    title = plot_title,
    subtitle = plot_subtitle,
    caption = plot_caption
  ) +
  ggthemes::theme_map(
    base_family = "body_font",
    base_size = ts
  ) +
  theme(
    plot.background = element_rect(
      fill = bg_col,
      colour = "transparent"
    ),
    panel.background = element_rect(
      fill = bg_col,
      colour = "transparent"
    ),
    legend.position = "none",
    plot.title = element_text(
      family = "title_font",
      hjust = 0,
      colour = text_hil,
      size = 3.6 * ts,
      margin = margin(5,0,5,0, "mm")
    ),
    plot.subtitle = element_markdown(
      family = "body_font",
      lineheight = 0.38,
      hjust = 0,
      colour = text_col,
      margin = margin(0,0,0,0, "mm")
    ),
    plot.caption = element_textbox(
      hjust = 1,
      family = "caption_font",
      colour = text_col,
      margin = margin(30,0,0,0, "mm")
    )
  )

Insets and Annotation

Code
# Busiest Airports - Horizontal Bar Plot

inset1 <- busiest_airports |> 
  filter(rank <= 10) |> 
  mutate(iso2c = c("US", "AE", "US", "UK", "JP",
                   "US", "TR", "US", "US", "IN")) |>
  mutate(iso2c = str_to_lower(iso2c)) |> 
  ggplot(
    aes(
      x = totalpassengers,
      y = fct_rev(as_factor(rank))
    )
  ) +
  geom_col(fill = mypal[5], alpha = 0.5) +
  geom_richtext(
    aes(
      x = 0,
      label = paste0(airport, ", ", location, ", **", country, "**")
    ),
    hjust = 0,
    nudge_x = 1e6,
    family = "caption_font",
    fill = "transparent",
    label.size = NA,
    colour = text_col,
    size = 18
  ) +
  geom_text(
    aes(label = number(totalpassengers, big.mark = ",")),
    hjust = 1,
    nudge_x = -1e6,
    family = "caption_font",
    size = 18
  ) +
  scale_x_continuous(
    expand = expansion(0),
    labels = label_number(scale_cut = cut_short_scale())
  ) +
  scale_y_discrete(expand = c(0,0)) +
  coord_cartesian(clip = "off") +
  labs(
    y = "Airport Ranking",
    x = NULL,
    title = "World's Busiest Airports (by passengers, 2023)"
  ) +
  theme_minimal(
    base_family = "body_font",
    base_size = ts
  ) +
  theme(
    plot.background = element_rect(
      fill = "transparent", 
      colour = "transparent"
    ),
    panel.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    ),
    panel.grid = element_blank(),
    panel.grid.major.x = element_line(
      linewidth = 0.3, linetype = 3
    ),
    axis.line = element_blank(),
    plot.title = element_text(
      margin = margin(0,0,2,0,"mm"),
      colour = text_col),
    axis.text = element_text(
      margin = margin(0,0,0,0, "mm"),
      colour = text_col,
      lineheight = 0.35
      ),
    axis.title = element_text(
      margin = margin(0,0,0,0, "mm"),
      colour = text_col, 
      lineheight = 0.35
      ),
    axis.ticks = element_blank(),
    axis.ticks.length = unit(0, "mm"),
    panel.border = element_blank(),
    plot.margin = unit(c(0,0,0,0), "mm"),
    panel.spacing = unit(0, "mm")
  )

inset2 <- longest_flights |> 
  left_join(df |> rename(from = iata, from_city = city)) |> 
  left_join(df |> rename(to_city = city), join_by(to == iata)) |> 
  arrange(desc(distance)) |> 
  mutate(rank = row_number()) |>
  select(rank, from_city, to_city, airlines, distance) |> 
  mutate(
    from_city = if_else(
      str_detect(from_city, "Dallas"), 
      "Dallas", 
      from_city)
  ) |> 
  ggplot(
    aes(
      y = fct_rev(as_factor(rank)),
      x = distance - 12000,
      fill = as_factor(rank),
      colour = as_factor(rank)
    )
  ) +
  geom_col(width = 0.05) +
  geom_point(aes(x = 0), size = 3) +
  geom_point(size = 3) +
  geom_text(
    aes(x = 0, label = from_city),
    hjust = 1,
    nudge_x = -50,
    family = "caption_font",
    size = 20
  ) +
  geom_text(
    aes(label = to_city, x = distance - 12000),
    hjust = 0,
    nudge_x = +50,
    family = "caption_font",
    size = 20
  ) +
  geom_richtext(
    aes(x = 100, label = paste0(airlines, 
                                "  ---  **", 
                                distance, 
                                " km**")),
    family = "body_font",
    hjust = 0,
    nudge_y = +0.2,
    label.size = NA,
    fill = "transparent",
    size = 15
  ) +
  labs(title = "Longest Operational Flights (2023)") +
  scale_x_continuous(expand = expansion(c(0.2, 0.2))) +
  scale_fill_manual(values = orange_palette) +
  scale_colour_manual(values = orange_palette) +
  coord_cartesian(clip = "off") +
  theme_void(base_family = "body_font", base_size = ts) +
  theme(
    legend.position = "none",
    plot.title = element_text(
      margin = margin(0,0,0,0, "mm"),
      hjust = 0.5,
      colour = text_col
    )
  )

# QR Code for the plot
url_graphics <- paste0(
  "https://aditya-dahiya.github.io/projects_presentations/data_vizs/",
  # The file name of the current .qmd file
  "airports_flights",         
  ".html"
)
# remotes::install_github('coolbutuseless/ggqr')
# library(ggqr)
plot_qr <- ggplot(
  data = NULL, 
  aes(x = 0, y = 0, label = url_graphics)
  ) + 
  ggqr::geom_qr(
    colour = text_hil, 
    fill = bg_col,
    size = 1.8
    ) +
  coord_fixed() +
  theme_void() +
  theme(plot.background = element_rect(
    fill = NA, 
    colour = NA
    )
  )

text_inset <- ggplot() +
  annotate(
    geom = "text",
    x = 0, y = 0,
    label = str_wrap(text_annotation_1, 60),
    family = "caption_font",
    hjust = 0,
    size = 18,
    colour = text_col,
    lineheight = 0.3
  ) +
  coord_cartesian(clip = "off") +
  theme_void() +
  theme(
    plot.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    ),
    panel.background = element_rect(
      fill = "transparent",
      colour = "transparent"
    )
  )

Compiling Final Plot

Code
g <- g_base +
  
  # Add inset1 to the plot
  inset_element(
    p = inset1, 
    left = 0.33, 
    right = 0.78,
    bottom = 0,
    top = 0.3, 
    align_to = "full",
    clip = FALSE,
    on_top = TRUE
  ) +
  
  # Add inset2 to the plot
  inset_element(
    p = inset2, 
    left = 0, 
    right = 0.33,
    bottom = 0,
    top = 0.3, 
    align_to = "full",
    clip = FALSE,
    on_top = TRUE
  ) +
  
  # Add text inset to the plot
  inset_element(
    p = text_inset, 
    left = 0.58, 
    right = 1,
    bottom = 0.08,
    top = 0.28, 
    align_to = "full",
    clip = TRUE,
    on_top = TRUE
  ) +
  
  # Add QR Code to the plot
  inset_element(
    p = plot_qr, 
    left = 0.85, 
    right = 0.95,
    bottom = 0.8,
    top = 0.95, 
    align_to = "full",
    clip = FALSE
  ) +
  
  # Basix Plot Annotations
  plot_annotation(
    theme = theme(
      plot.background = element_rect(
        fill = bg_col, 
        colour = NA, 
        linewidth = 0
      )
    )
  )

Savings the graphics

Code
ggsave(
  filename = here::here("data_vizs", "a4_airports_flights.png"),
  plot = g,
  width = 2 * 297,    # Default A4 size page
  height = 2 * 210,   # Default A4 size page
  units = "mm",
  bg = bg_col
)

library(magick)
# Saving a thumbnail for the webpage
image_read(here::here("data_vizs", "a4_airports_flights.png")) |> 
  image_resize(geometry = "400") |> 
  image_write(here::here("data_vizs", "thumbnails", 
                         "airports_flights.png"))

References

Hijmans, Robert J. 2022. “Geosphere: Spherical Trigonometry.” https://CRAN.R-project.org/package=geosphere.
Shkolnik, Dmitry. 2019. “Airportr: Convenience Tools for Working with Airport Data.” https://CRAN.R-project.org/package=airportr.