Speed Dominance in WWII: The Rise of the Jet Age

A visual comparison of top speeds and production numbers of World War II fighter aircraft reveals the Messerschmitt Me 262’s unmatched dominance in speed, marking a revolutionary leap in aviation technology during its era.

{magick}
Images
Geopolitics
Author

Aditya Dahiya

Published

January 28, 2025

Figure 1: This graphic visualizes the top speeds of WWII fighter aircraft at the time of the Messerschmitt Me 262’s introduction, with aircraft names listed on the y-axis in descending order of speed. The x-axis represents top speed (in km/h), and each bar includes the approximate production numbers in parentheses. Country flags are placed near the y-axis for easy identification, and accompanying aircraft images provide a visual connection to these iconic machines.

Source of Data

The information provided is compiled using ChatGPT from the following sources:

  1. “The Complete Book of Fighters” by William Green and Gordon Swanborough
  2. “Jane’s Fighting Aircraft of World War II” by Leonard Bridgman.
  3. Aviation History Online Museum: A reliable online source for historical aircraft specifications.
  4. Military Factory: A database of military equipment, including fighter aircraft used in WWII.

How I made this graphic?

Loading required libraries, data import & creating custom functions.

Code
# Data Wrangling & Plotting Tools
library(tidyverse)            # All things tidy
library(magick)               # Processing Images in R
library(httr)                 # Getting images from the Web

# Plot touch-up 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(patchwork)            # Compiling Plots


library(tibble)

# Create a tibble with fighter aircraft details
dfww2 <- tibble::tibble(
  Country = c(
    "Germany", "United Kingdom", "United States", 
    "Japan", "Soviet Union",
    "Italy", "France", "Germany", "United Kingdom", "United States",
    "Japan", "Soviet Union", "Germany", 
    "United States", "United Kingdom",
    "Japan", "United States", "Soviet Union", 
    "Italy", "Germany"
  ),
  Aircraft_Name = c(
    "Messerschmitt Me 262", "Supermarine Spitfire", 
    "North American P-51 Mustang", "Mitsubishi A6M Zero",
    "Yakovlev Yak-3", "Macchi C.202 Folgore", 
    "Dewoitine D.520", "Focke-Wulf Fw 190", 
    "Hawker Tempest", "Republic P-47 Thunderbolt", 
    "Kawasaki Ki-61 Hien", "Lavochkin La-5",
    "Heinkel He 162", "Lockheed P-38 Lightning", 
    "Gloster Meteor", "Nakajima Ki-84 Hayate",
    "Curtiss P-40 Warhawk", "Mikoyan-Gurevich MiG-3", 
    "Reggiane Re.2005 Sagittario", "Messerschmitt Bf 109"
  ),
  Top_Speed_kmh = c(
    870, 595, 703, 533, 655, 600, 540, 652, 695, 686, 591, 648, 890, 667, 668, 631, 579, 640, 628, 640
  ),
  Approx_Numbers_Produced = c(
    1430, 20351, 15886, 10939, 4848, 11500, 905, 20000, 1702, 15636, 2754, 9920, 320, 10037, 3947, 3514, 13738, 3172, 750, 33984
  )
) |> 
  janitor::clean_names() |> 
  filter(aircraft_name != "Heinkel He 162") |> 
  filter(approx_numbers_produced > 1000) |> 
  mutate(id = row_number())

Visualization Parameters

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

# Font for the caption
font_add_google("Stint Ultra Condensed",
  family = "caption_font"
) 

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

showtext_auto()

mypal <- paletteer::paletteer_d("MexBrewer::Revolucion")

# A base Colour
bg_col <- mypal[5]
seecolor::print_color(bg_col)

# Colour for highlighted text - 1
text_hil <- mypal[1]
seecolor::print_color(text_hil)

# Colour for the text - 1
text_col <- mypal[1]
seecolor::print_color(text_col)

# Define Base Text Size
bts <- 80 

# 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_caption <- paste0(
  "**Data:** Military Factory & Aviation History Online Museum", 
  " |  **Code:** ", 
  social_caption_1, 
  " |  **Graphics:** ", 
  social_caption_2
  )
rm(github, github_username, xtwitter, 
   xtwitter_username, social_caption_1, 
   social_caption_2)

# Add text to plot-------------------------------------------------
plot_title <- "Jet Power Takes Off (1944)"

plot_subtitle <- str_wrap("The **Messerschmitt Me 262**, the world’s first operational jet fighter, far outclassed its contemporaries in speed, despite its relatively low production numbers. The graphic compares the top speeds of key fighter aircraft in 1944 during WW-II.", 90) |> 
  str_replace_all("\\\n", "<br>")

Download images of the fighter aircraft

Code
# Get a custom google search engine and API key
# Tutorial: https://developers.google.com/custom-search/v1/overview
# Tutorial 2: https://programmablesearchengine.google.com/

# From:https://developers.google.com/custom-search/v1/overview
# google_api_key <- "LOAD YOUR GOOGLE API KEY HERE"

# From: https://programmablesearchengine.google.com/controlpanel/all
# my_cx <- "GET YOUR CUSTOM SEARCH ENGINE ID HERE"

# Improved function to download and save food images
download_aircraft_images <- function(i) {
  
  api_key <- google_api_key
  cx <- my_cx
  
  # Build the API request URL with additional filters
  url <- paste0(
    "https://www.googleapis.com/customsearch/v1?q=",
    URLencode(paste0(dfww2$aircraft_name[i], 
                     " fighter aircraft side photo white background")),
    "&cx=", cx,
    "&searchType=image",
    "&key=", api_key,
    # "&imgSize=large",       # Restrict to medium-sized images
    # "&imgType=photo",
    "&num=1"                 # Fetch only one result
  )
  
  # Make the request
  response <- GET(url)
  if (response$status_code != 200) {
    warning("Failed to fetch data for Aircraft: ", 
            dfww2$aircraft_name[i])
    return(NULL)
  }
  
  # Parse the response
  result <- content(response, "parsed")
  
  # Extract the image URL
  if (!is.null(result$items)) {
    image_url <- result$items[[1]]$link
  } else {
    warning("No results found for aircraft: ", dfww2$aircraft_name[i])
    return(NULL)
  }
  
  # Process the image
  im <- magick::image_read(image_url) |> 
      image_resize("x300") # Resize image
    # Save the image
    image_write(
      image = im,
      path = here::here("data_vizs", 
                  paste0("temp_ww2_aircraft_", i, ".png")),
      format = "png"
    )
}

# Iterate through each state and download images
for (i in 1:nrow(dfww2)) {
  download_aircraft_images(i)
}

# Custom Search for some aircrafts ---------------------------------------
# Improved function to download and save food images
download_custom_aircraft_images <- function(i, urli) {
  
  url <- urli
  
  # Process the image
  im <- magick::image_read(url) |> 
      image_resize("x300") # Resize image
    # Save the image
    image_write(
      image = im,
      path = here::here("data_vizs", 
                  paste0("temp_ww2_aircraft_", i, ".png")),
      format = "png"
    )
}

# Iterate through each state and download images
# 3, 5, 6, 7, 9, 12, 15, 18
download_custom_aircraft_images(3, "https://upload.wikimedia.org/wikipedia/commons/4/47/375th_Fighter_Squadron_North_American_P-51D-5-NA_Mustang_44-13926.jpg")

download_custom_aircraft_images(5, "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQerja4s90LG-vf5eIDLoqVU9NU8Udfwvg7HA&s")

download_custom_aircraft_images(6, "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSZqNZvBcj0xUA_PBu6YV55TS-29YHcKY4HmA&s")

download_custom_aircraft_images(7, "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRjYM_jIA5c5DVc6lDNzuFj7s9uz_ruA5bqRA&s")

download_custom_aircraft_images(9, "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTB-TvF7xHMKHCbjrhGL_SLMeNCSN45grqg0Q&s")

download_custom_aircraft_images(12, "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS_VJo0FwR2vck7r4HBTBJ0xQ0zmwzTC6IOuQ&s")

download_custom_aircraft_images(15, "https://render.fineartamerica.com/images/rendered/square-product/small/images/rendered/default/acrylic-print///hangingwire/break/images-medium-5/old-exterminator-p-40-warhawk-white-background-craig-tinder.jpg")

# Improve images into Circular background----------------------------
for(i in c(3, 5, 6, 7, 9, 12, 15)) {
  # Load the image
  im <- image_read(
    here::here(
      "data_vizs",
      paste0("temp_ww2_aircraft_", i, ".png")
    )
  )
  
  # Calculate the new dimension for the square
  max_dim <- max(image_info(im)$width, image_info(im)$height)
  
  # Create a blank white canvas of square size
  canvas <- image_blank(width = max_dim, 
                        height = max_dim, 
                        color = "white")
  
  # Composite the original image onto the center of the square canvas
  image_composite(canvas, im, gravity = "center") |> 
    # Crop the image into a circle 
    # (Credits: https://github.com/doehm/cropcircles)
    cropcircles::circle_crop(
      border_colour = text_col,
      border_size = 2
    ) |>
    image_read() |> 
    image_background(color = bg_col) |> 
    image_resize("x400") |> 
    # Save or display the result
    image_write(
      here::here(
        "data_vizs", 
        paste0("temp_ww2_aircraft_", i, ".png")
        )
    )
}

Download flags of countries (since ggflags (Auguie, Goldie, and Thériault 2024) does not have Soviet Union flag)

Code
# Get a custom google search engine and API key
# Tutorial: https://developers.google.com/custom-search/v1/overview
# Tutorial 2: https://programmablesearchengine.google.com/

# From:https://developers.google.com/custom-search/v1/overview
# google_api_key <- "LOAD YOUR GOOGLE API KEY HERE"

# From: https://programmablesearchengine.google.com/controlpanel/all
# my_cx <- "GET YOUR CUSTOM SEARCH ENGINE ID HERE"

# Improved function to download and save food images
download_flag <- function(i) {
  
  api_key <- google_api_key
  cx <- my_cx
  
  # Build the API request URL with additional filters
  url <- paste0(
    "https://www.googleapis.com/customsearch/v1?q=",
    URLencode(paste0("Flag of ", dfww2$country[i])),
    "&cx=", cx,
    "&searchType=image",
    "&key=", api_key,
    "&num=1"                 # Fetch only one result
  )
  
  # Make the request
  response <- GET(url)
  if (response$status_code != 200) {
    warning("Failed to fetch data for: ", 
            dfww2$country[i])
    return(NULL)
  }
  
  # Parse the response
  result <- content(response, "parsed")
  
  # Extract the image URL
  if (!is.null(result$items)) {
    image_url <- result$items[[1]]$link
  } else {
    warning("No results found for: ", dfww2$country[i])
    return(NULL)
  }
  
  # Process the image
  im <- magick::image_read(image_url) |> 
    # Crop the image into a circle 
    # (Credits: https://github.com/doehm/cropcircles)
    cropcircles::circle_crop(
      border_colour = text_col,
      border_size = 2
    ) |>
    image_read() |> 
    image_background(color = bg_col) |> 
    image_resize("x300") |> 
    # Save or display the result
    image_write(
      here::here(
        "data_vizs", 
        paste0("temp_ww2_flag_", i, ".png")
        )
    )
}

# Iterate through each state and download images
for (i in 1:nrow(dfww2)) {
  download_flag(i)
}

Improve the final tibble with country flag codes and images

Code
df3 <- dfww2 |> 
  mutate(
    code = countrycode::countrycode(
      sourcevar = country,
      origin = "country.name",
      destination = "iso2c"
    ),
    code = str_to_lower(code)
  )

The Base Plot

Code
g <- df3 |>
  ggplot(
    mapping = aes(
      x = top_speed_kmh,
      y = reorder(aircraft_name, top_speed_kmh)
    )
  ) +
  geom_segment(
    mapping = aes(x = 500, xend = top_speed_kmh),
    linewidth = 2.5,
    colour = alpha(text_col, 0.3)
  ) +
  # ggflags::geom_flag(
  #   mapping = aes(
  #     country = code,
  #     x = 500
  #   ),
  #   size = 20
  # ) +
  
  # Adding image of the country flags
  ggimage::geom_image(
    mapping = aes(
      image = paste0("data_vizs/temp_ww2_flag_", id, ".png"),
      x = 500
    )
  ) +
  
  ggimage::geom_image(
    mapping = aes(
      image = paste0("data_vizs/temp_ww2_aircraft_", id, ".png")
    )
  ) +
  
  
  geom_text(
    mapping = aes(
      label = paste0(top_speed_kmh, "")
    ),
    colour = text_col,
    vjust = 0.5,
    nudge_x = 20,
    family = "title_font",
    size = bts / 2.7,
    hjust = 0,
    fontface = "bold"
  ) +
  geom_text(
    mapping = aes(
      label = paste0(
        "(", 
        scales::number(approx_numbers_produced, big.mark = ","), 
        ")"
      )
    ),
    colour = text_col,
    nudge_y = -0.38,
    nudge_x = 20,
    family = "body_font",
    size = bts / 7,
    hjust = 0
  ) +
  annotate(
    geom = "label",
    x = 800, y = 4,
    hjust = 0.5,
    vjust = 0.5,
    label = str_wrap(
      "The number near the aircraft image represent top speed (in km/h). The numbers in small font brackets represents the approximate number of aircrafts produced.", 
      20
    ),
    family = "body_font",
    lineheight = 0.35,
    size = bts / 3,
    colour = text_col,
    fill = alpha("white", 0.3),
    label.size = NA,
    label.padding = unit(0.5, "lines"),
    label.r = unit(0.25, "lines")
  ) +
  scale_x_continuous(
    expand = expansion(0)
  ) +
  coord_cartesian(
    xlim = c(500, 950),
    clip = "off"
  ) +
  labs(
    title = plot_title,
    subtitle = plot_subtitle,
    caption = plot_caption,
    y = NULL, x = "Top Speed (km/h)"
  ) +
  theme_minimal(
    base_family = "body_font",
    base_size = bts
  ) +
  theme(
    
    # Overall plot
    panel.grid = element_blank(),
    panel.grid.major.x = element_line(
      linewidth = 0.3,
      linetype = 3,
      colour = alpha(text_col, 0.5)
    ),
    plot.margin = margin(5,5,5,5, "mm"),
    plot.title.position = "plot",
    text = element_text(
      colour = text_col,
      lineheight = 0.3
    ),
    
    # Labels
    plot.title = element_text(
      family = "title_font",
      hjust = 0.5,
      size = 3 * bts,
      margin = margin(5,0,5,0, "mm")
    ),
    plot.subtitle = element_textbox(
      halign = 0.5,
      hjust = 0.5,
      margin = margin(5,0,0,0, "mm"),
      lineheight = 0.4
    ),
    plot.caption = element_textbox(
      family = "caption_font",
      halign = 0.8,
      hjust = 0.8,
      margin = margin(0,0,0,0, "mm")
    ),
    
    # Axis
    axis.text.y = element_text(
      margin = margin(0,15,0,5, "mm"),
      colour = text_col,
      family = "caption_font",
      face = "bold",
      size = 1.2 * bts
    ),
    axis.text.x = element_blank(),
    axis.title = element_blank(),
    axis.ticks.length = unit(0, "mm")
  )

ggsave(
  filename = here::here(
    "data_vizs",
    "viz_ww2_fighter_aircrafts.png"
  ),
  plot = g,
  width = 400,
  height = 500,
  units = "mm",
  bg = bg_col
)

Savings the thumbnail for the webpage

Code
# Saving a thumbnail

library(magick)
# Saving a thumbnail for the webpage
image_read(here::here("data_vizs", 
                      "viz_ww2_fighter_aircrafts.png")) |> 
  image_resize(geometry = "x400") |> 
  image_write(
    here::here(
      "data_vizs", 
      "thumbnails", 
      "viz_ww2_fighter_aircrafts.png"
    )
  )
# List all files in the folder
files <- list.files("data_vizs", full.names = TRUE)

# Filter files starting with "temp_ww2_"
files_to_remove <- files[grepl("^temp_ww2_", basename(files))]

# Remove the filtered files
file.remove(files_to_remove)

Session Info

Code
# Data Wrangling & Plotting Tools
library(tidyverse)            # All things tidy
library(sf)                   # Simple Features in R

# Plot touch-up 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(patchwork)            # Compiling Plots

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

References

Auguie, Baptiste, James Goldie, and Rémi Thériault. 2024. “Ggflags: Plot Flags of the World in Ggplot2.” https://github.com/jimjam-slam/ggflags.