Isochrone Map: Delhi’s Airport driving time

An isochrone showing the time it takes to drive to New Delhi IGI Airport from different parts of India’s National Capital Region

Maps
India
Geocomputation
Haryana
Interactive
Author

Aditya Dahiya

Published

October 4, 2024

Isochrones are lines on a map that connect points where travel time from a specific location is the same. In the context of my map, the isochrones represent driving distances at 10-minute intervals, starting from 10 minutes and extending up to 60 minutes from New Delhi Indira Gandhi International (IGI) Airport Terminal 3. This visualization helps users understand how far they can travel within specific time frames under normal traffic conditions. The map was created using open-source data from OpenRouteService, and the methodology and code were inspired by the work of Milos Popovic, whose detailed tutorial and code are available on GitHub and through his video tutorial.

Figure 1: Driving time isochrone map from New Delhi IGI International Airport Terminal 3, displaying 10-minute intervals from 10 minutes to 60 minutes. Each shaded region represents areas reachable within a specific travel time, providing a clear visualization of driving distances under normal traffic conditions. Data sourced from OpenRouteService and generated using map tiles from Stadia Maps’ Stamen Design.

How I made this graphic?

Loading required libraries, data import & creating custom functions

Code
library(openrouteservice)     # Get catchment areas in sf format
library(leaflet)              # Interactive Maps
library(tidyverse)            # Data Wrangling & ggplot2
# library(maptiles)             # Get map data rasters for background
library(sf)                   # for SF objects in ggplot2
# library(tidyterra)            # Use maptiles with ggplot2
# library(fontawesome)          # Icons display in ggplot2
# library(ggtext)               # Markdown text support for ggplot2
# library(showtext)             # Display fonts in ggplot2

# Mapping travel catchment areas by Milos Popovic 2023/10/10
# Video Link: https://www.youtube.com/watch?v=UNGzJrx8VrE
# Code: https://github.com/milos-agathon/isochrone_maps

# Install the Open Route Service R-package
# remotes::install_github(
#   "GIScience/openrouteservice-r"
# )

# Getting the main parameters

# Seeing the available kinds of modes of travel
# openrouteservice::ors_profile()
# api_key <- "" # API key from https://openrouteservice.org/

# Coordinates of IGI Airport Terminal 3, New Delhi, India
coords <- data.frame(
  lon = 77.0857630708,
  lat = 28.5554206018,
  image = "https://cdn-icons-png.flaticon.com/512/7720/7720736.png"
  )

# Getting the Iso-chrones data from OpenRouteService
# car_delhi <- openrouteservice::ors_isochrones(
#   locations = coords,
#   profile = "driving-car",
#   range = 3600,
#   interval = 600,
#   api_key = api_key,
#   output = "sf"
# )

# write_rds(car_delhi, here::here("data", "delhi_isochrone.rds"))
# Check the size fo the data fetched
# object.size(car_delhi) |> print(units = "Kb")

# For purpose of rendering this page, I have used 
# downloaded data

car_delhi <- read_rds(here::here("data", 
                                 "delhi_isochrone.rds"))

Visualization Parameters

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

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

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

showtext_auto()

# Background Colour
bg_col <- "white"

# Colour for the text
text_col <- "grey5" 

# Colour for highlighted text
text_hil <- "grey10" 

# Define Base Text Size
bts <- 120 

# 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_hil}'>{github_username}  </span>")
social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_hil}'>{xtwitter_username}</span>")

plot_title <- "Driving Time to Delhi's International Airport (Isochrones)"

plot_caption <- paste0(
  "**Data:** openrouteservice.org<br>",
  "**Technique:** Milos Popovic<br>", 
  "**Code:** ", 
  social_caption_1, 
  "**<br>Graphics:** ", 
  social_caption_2
  )

rm(github, github_username, xtwitter, 
   xtwitter_username, social_caption_1, 
   social_caption_2)

Data Analysis and Wrangling

Code
# We need to intersect the bigger and smaller polygons so that 
# they dont overlap, and bigger polygons dont overshadow
sf::sf_use_s2(F)        # Turn off spherical geometry
car_delhi_cropped <- car_delhi |> 
  mutate(mins = as_factor(value / 60)) |> 
  group_by(mins) |> 
  st_intersection() |>
  ungroup()

# Data needed to make a Static Graphic with ggplot2
# car_delhi_merc <- sf::st_transform(
#   car_delhi_cropped,
#   4326
# )
# 
# delhi_layer <- maptiles::get_tiles(
#   car_delhi_merc,
#   provider = "CartoDB.Positron",
#   zoom = 9
# )
# 
# object.size(delhi_layer) |> print(units = "Kb")


# Data needed to make a Static Graphic with ggmap
# library(ggmap)
# Register your Stadia Maps key
# stadia_maps_api_key
# register_stadiamaps(stadia_maps_api_key, write = FALSE)
# 
# background_tiles_bbox <- c(
#   left = 76.45,
#   right = 77.75,
#   top = 29.1,
#   bottom = 27.9
# )
# 
# stamen_tiles_10 <- ggmap::get_stadiamap(
#   background_tiles_bbox,
#   zoom = 10,
#   maptype = "stamen_toner"
# )
# 
# stamen_tiles_11 <- ggmap::get_stadiamap(
#   background_tiles_bbox,
#   zoom = 11,
#   maptype = "stamen_toner"
# )
# 
# mypal <- paletteer::paletteer_d("rcartocolor::Geyser")[c(1:3,5:7)]
# 
# library(magick)
# airport_icon <- magick::image_read("https://cdn-icons-png.flaticon.com/512/7720/7720736.png")
# 
# airport_icon |> 
#   image_background("transparent")

An interactive plot using {leaflet}

Code
# An interactive map with leaflet
pal_fact <- leaflet::colorFactor(
  "RdPu",
  domain = car_delhi_cropped$mins,
  reverse = T,
  na.color = "transparent"
)

leaflet::leaflet(
  car_delhi_cropped
  ) |>
  leaflet::addPolygons(
    fill = T,
    stroke = T,
    color = pal_fact,
    weight = .3,
    fillColor = ~pal_fact(mins),
    fillOpacity = .2
  ) |>
  leaflet::addProviderTiles(
    "CartoDB.Positron"
  ) |>
  leaflet::addLegend(
    "bottomright",
    pal = pal_fact,
    values = car_delhi_cropped$mins,
    labels = car_delhi_cropped$mins,
    opacity = .5,
    title = "Driving Time to IGI Airport (Delhi)"
  )
Figure 2: An interactive isochrone map of driving times to the New Delhi’s International Airport

The Base Plot - static graphics

Code
g <- ggmap(stamen_tiles) +
  geom_sf(
    data = car_delhi_cropped,
    mapping = aes(
      x = NULL, y = NULL,
      geometry = geometry,
      fill = mins
      ),
    alpha = 0.4,
    colour = "transparent"
    )  +
  scale_fill_manual(
    values = mypal
  ) +
  ggimage::geom_image(
    data = coords,
    mapping = aes(
      x = lon, y = lat,
      image = image
    )
  ) +
  guides(
    color = "none",
    fill = guide_legend(
      nrow = 1,
      byrow = T,
      keyheight = unit(20, "mm"),
      keywidth = unit(20, "mm"),
      title.position = "top",
      label.position = "bottom",
      label.hjust = .5
      )
    ) +
  
  labs(
    title = plot_title,
    caption = plot_caption,
    fill = "Driving Time (in minutes)"
  ) +
  theme_void(
    base_size = bts,
    base_family = "body_font"
  ) +
  theme(
    # Overall Plot
    plot.background = element_rect(
      fill = bg_col,
      colour = "transparent",
      linewidth = unit(5, "mm")
    ),
    plot.margin = margin(5,5,5,5, "mm"),
    text = element_text(
      colour = text_hil,
      lineheight = 0.3,
      margin = margin(0,0,0,0, "mm")
    ),
    
    
    # Legend
    legend.position = "inside",
    legend.position.inside = c(1,0),
    legend.margin = margin(3,10,1,10, "mm"),
    legend.background = element_rect(
      fill = alpha(bg_col, 0.2),
      colour = "transparent"
    ),
    legend.title = element_text(
      margin = margin(0,0,5,0, "mm")
    ),
    legend.text = element_text(
      margin = margin(3,0,0,0, "mm")
    ),
    legend.justification = c(1, 1),
    
    # Labels
    plot.caption = element_textbox(
      fill = alpha(bg_col, 0.3),
      box.colour = "transparent",
      hjust = 0,
      family = "caption_font",
      margin = margin(5,0,0,0, "mm"),
      padding = unit(c(2, 2, 2, 2), "mm"),
      size = bts * 0.6,
      lineheight = 0.35
    ),
    plot.title = element_textbox(
      fill = alpha(bg_col, 0.3),
      box.colour = "transparent",
      hjust = 0.5,
      size = bts * 1.5,
      face = "bold",
      family = "caption_font",
      margin = margin(0,0,10,0, "mm"),
      padding = unit(c(2, 2, 2, 2), "mm")
    )
    
  )

ggsave(
  filename = here::here("data_vizs", "delhi_isochrone_2.png"),
  plot = g,
  width = 400,    # Default A4 size page
  height = 500,   # Default A4 size page
  units = "mm",
  bg = bg_col
)

Savings the graphics

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

# Saving a thumbnail

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

Session Info

Code
sessioninfo::session_info()$packages |> 
  as_tibble() |> 
  select(package, 
         version = loadedversion, 
         date, source) |> 
  arrange(package) |> 
  janitor::clean_names(
    case = "title"
  ) |> 
  gt::gt() |> 
  gt::opt_interactive(
    use_search = TRUE
  ) |> 
  gtExtras::gt_theme_espn()
Table 1: R Packages and their versions used in the creation of this page and graphics