30DayMapChallenge Days 02 & 03 - Lines & Polygons

Map exploration using JFK December 2024 flight data
30DayMapChallenge
Data visualization
R
ggplot2
Geospatial
Author

Seth Kasowitz

Published

November 3, 2025

Unable to get to a computer yesterday, this challenge is off to a rocky start. To help catch up I am cheating a bit, using a single data set to tackle days 2 (Lines) and 3 (Polygons) for the 30DayMapChallenge.

Flight data from the (Bureau of Transportation Statistics)[https://www.bts.gov/].

I am focusing on flights to and from JFK airport in New York.

1 Setup

Show the code
library(anyflights)
library(tidyverse)
library(geosphere)
library(maps)
library(ggrepel)

2 Load Data

Show the code
flights <- read_csv(
  'D:/Projects/FlightData/On_Time_Reporting_Carrier_On_Time_Performance_(1987_present)_2024_12.csv'
)
Show the code
glimpse(flights)
Rows: 590,581
Columns: 110
$ Year                            <dbl> 2024, 2024, 2024, 2024, 2024, 2024, 20…
$ Quarter                         <dbl> 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,…
$ Month                           <dbl> 12, 12, 12, 12, 12, 12, 12, 12, 12, 12…
$ DayofMonth                      <dbl> 25, 26, 27, 28, 29, 30, 31, 19, 20, 21…
$ DayOfWeek                       <dbl> 3, 4, 5, 6, 7, 1, 2, 4, 5, 6, 7, 1, 2,…
$ FlightDate                      <date> 2024-12-25, 2024-12-26, 2024-12-27, 2…
$ Reporting_Airline               <chr> "9E", "9E", "9E", "9E", "9E", "9E", "9…
$ DOT_ID_Reporting_Airline        <dbl> 20363, 20363, 20363, 20363, 20363, 203…
$ IATA_CODE_Reporting_Airline     <chr> "9E", "9E", "9E", "9E", "9E", "9E", "9…
$ Tail_Number                     <chr> "N902XJ", "N491PX", "N915XJ", "N917XJ"…
$ Flight_Number_Reporting_Airline <dbl> 5125, 5125, 5125, 5125, 5125, 5125, 51…
$ OriginAirportID                 <dbl> 11995, 11995, 11995, 11995, 11995, 119…
$ OriginAirportSeqID              <dbl> 1199502, 1199502, 1199502, 1199502, 11…
$ OriginCityMarketID              <dbl> 31995, 31995, 31995, 31995, 31995, 319…
$ Origin                          <chr> "GSO", "GSO", "GSO", "GSO", "GSO", "GS…
$ OriginCityName                  <chr> "Greensboro/High Point, NC", "Greensbo…
$ OriginState                     <chr> "NC", "NC", "NC", "NC", "NC", "NC", "N…
$ OriginStateFips                 <chr> "37", "37", "37", "37", "37", "37", "3…
$ OriginStateName                 <chr> "North Carolina", "North Carolina", "N…
$ OriginWac                       <dbl> 36, 36, 36, 36, 36, 36, 36, 22, 22, 22…
$ DestAirportID                   <dbl> 12953, 12953, 12953, 12953, 12953, 129…
$ DestAirportSeqID                <dbl> 1295304, 1295304, 1295304, 1295304, 12…
$ DestCityMarketID                <dbl> 31703, 31703, 31703, 31703, 31703, 317…
$ Dest                            <chr> "LGA", "LGA", "LGA", "LGA", "LGA", "LG…
$ DestCityName                    <chr> "New York, NY", "New York, NY", "New Y…
$ DestState                       <chr> "NY", "NY", "NY", "NY", "NY", "NY", "N…
$ DestStateFips                   <chr> "36", "36", "36", "36", "36", "36", "3…
$ DestStateName                   <chr> "New York", "New York", "New York", "N…
$ DestWac                         <dbl> 22, 22, 22, 22, 22, 22, 22, 36, 36, 36…
$ CRSDepTime                      <chr> "1240", "1150", "1150", "1100", "1150"…
$ DepTime                         <chr> "1235", "1146", "1144", "1214", "1141"…
$ DepDelay                        <dbl> -5, -4, -6, 74, -9, -12, -5, -7, -3, -…
$ DepDelayMinutes                 <dbl> 0, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ DepDel15                        <dbl> 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ DepartureDelayGroups            <dbl> -1, -1, -1, 4, -1, -1, -1, -1, -1, -1,…
$ DepTimeBlk                      <chr> "1200-1259", "1100-1159", "1100-1159",…
$ TaxiOut                         <dbl> 11, 15, 13, 13, 17, 14, 13, 21, 31, 53…
$ WheelsOff                       <chr> "1246", "1201", "1157", "1227", "1158"…
$ WheelsOn                        <chr> "1350", "1305", "1301", "1333", "1303"…
$ TaxiIn                          <dbl> 6, 5, 6, 7, 8, 9, 6, 4, 5, 4, 4, 3, 6,…
$ CRSArrTime                      <chr> "1429", "1336", "1336", "1244", "1336"…
$ ArrTime                         <chr> "1356", "1310", "1307", "1340", "1311"…
$ ArrDelay                        <dbl> -33, -26, -29, 56, -25, -31, -27, -25,…
$ ArrDelayMinutes                 <dbl> 0, 0, 0, 56, 0, 0, 0, 0, 1, 4, 0, 0, 2…
$ ArrDel15                        <dbl> 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1,…
$ ArrivalDelayGroups              <dbl> -2, -2, -2, 3, -2, -2, -2, -2, 0, 0, -…
$ ArrTimeBlk                      <chr> "1400-1459", "1300-1359", "1300-1359",…
$ Cancelled                       <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ CancellationCode                <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Diverted                        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ CRSElapsedTime                  <dbl> 109, 106, 106, 104, 106, 106, 109, 121…
$ ActualElapsedTime               <dbl> 81, 84, 83, 86, 90, 87, 87, 103, 125, …
$ AirTime                         <dbl> 64, 64, 64, 66, 65, 64, 68, 78, 89, 74…
$ Flights                         <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
$ Distance                        <dbl> 461, 461, 461, 461, 461, 461, 461, 461…
$ DistanceGroup                   <dbl> 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,…
$ CarrierDelay                    <dbl> NA, NA, NA, 0, NA, NA, NA, NA, NA, NA,…
$ WeatherDelay                    <dbl> NA, NA, NA, 0, NA, NA, NA, NA, NA, NA,…
$ NASDelay                        <dbl> NA, NA, NA, 0, NA, NA, NA, NA, NA, NA,…
$ SecurityDelay                   <dbl> NA, NA, NA, 0, NA, NA, NA, NA, NA, NA,…
$ LateAircraftDelay               <dbl> NA, NA, NA, 56, NA, NA, NA, NA, NA, NA…
$ FirstDepTime                    <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ TotalAddGTime                   <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ LongestAddGTime                 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ DivAirportLandings              <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ DivReachedDest                  <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ DivActualElapsedTime            <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ DivArrDelay                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ DivDistance                     <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1Airport                     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1AirportID                   <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1AirportSeqID                <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1WheelsOn                    <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1TotalGTime                  <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1LongestGTime                <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1WheelsOff                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div1TailNum                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2Airport                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2AirportID                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2AirportSeqID                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2WheelsOn                    <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2TotalGTime                  <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2LongestGTime                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2WheelsOff                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div2TailNum                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3Airport                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3AirportID                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3AirportSeqID                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3WheelsOn                    <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3TotalGTime                  <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3LongestGTime                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3WheelsOff                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div3TailNum                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4Airport                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4AirportID                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4AirportSeqID                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4WheelsOn                    <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4TotalGTime                  <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4LongestGTime                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4WheelsOff                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div4TailNum                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5Airport                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5AirportID                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5AirportSeqID                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5WheelsOn                    <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5TotalGTime                  <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5LongestGTime                <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5WheelsOff                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ Div5TailNum                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ ...110                          <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
Show the code
airports <- get_airports(dir = tempdir()) |>
  select(faa, name, lat, lon)

glimpse(airports)
Rows: 1,251
Columns: 4
$ faa  <chr> "AAF", "AAP", "ABE", "ABI", "ABL", "ABQ", "ABR", "ABY", "ACK", "A…
$ name <chr> "Apalachicola Regional Airport", "Andrau Airpark", "Lehigh Valley…
$ lat  <dbl> 29.72750, 29.72250, 40.65210, 32.41130, 67.10630, 35.04020, 45.44…
$ lon  <dbl> -85.02750, -95.58830, -75.44080, -99.68190, -157.85699, -106.6090…

3 Day 2 - Lines

3.1 Filter Data

I am interested in inbound and outbound flights for JFK during holiday travel at the end of the year.

Show the code
jfk_flights <- flights |>
  filter(
    between(FlightDate, ymd('2024-12-20'), ymd('2024-12-31')) &
      (Origin == "JFK" | Dest == "JFK")
  ) |>
  select(
    FlightDate,
    Reporting_Airline,
    Origin,
    OriginCityName,
    OriginState,
    Dest,
    DestCityName,
    DestState
  )

glimpse(jfk_flights)
Rows: 7,428
Columns: 8
$ FlightDate        <date> 2024-12-20, 2024-12-21, 2024-12-22, 2024-12-23, 202…
$ Reporting_Airline <chr> "9E", "9E", "9E", "9E", "9E", "9E", "9E", "9E", "9E"…
$ Origin            <chr> "BGR", "BGR", "BGR", "BGR", "BGR", "BGR", "BGR", "BG…
$ OriginCityName    <chr> "Bangor, ME", "Bangor, ME", "Bangor, ME", "Bangor, M…
$ OriginState       <chr> "ME", "ME", "ME", "ME", "ME", "ME", "ME", "ME", "ME"…
$ Dest              <chr> "JFK", "JFK", "JFK", "JFK", "JFK", "JFK", "JFK", "JF…
$ DestCityName      <chr> "New York, NY", "New York, NY", "New York, NY", "New…
$ DestState         <chr> "NY", "NY", "NY", "NY", "NY", "NY", "NY", "NY", "NY"…

3.2 Count routes and join on airport location data

Show the code
flight_counts <- jfk_flights |>
  count(Origin, Dest) |>
  left_join(airports |> select(-name), by = join_by('Origin' == 'faa')) |>
  rename(origin_lat = lat, origin_lon = lon) |>
  left_join(airports |> select(-name), by = join_by('Dest' == 'faa')) |>
  rename(dest_lat = lat, dest_lon = lon)

3.3 Slice most frequent routes for labels

Show the code
top_routes <- flight_counts |>
  slice_max(order_by = n, n = 10) |>
  distinct(Dest, .keep_all = TRUE)

3.4 Clean up count data

Show the code
flight_counts <- flight_counts |>
  filter(
    !is.na(origin_lat),
    !is.na(origin_lon),
    !is.na(dest_lat),
    !is.na(dest_lon)
  ) |>
  filter(!(origin_lat == dest_lat & origin_lon == dest_lon))

arriving_counts <- flight_counts |>
  filter(Dest == "JFK")
departing_counts <- flight_counts |>
  filter(Origin == "JFK")

3.5 Calculate great circle arcs between airport locations

Show the code
great_circle_arcs <- flight_counts |>
  # Add a unique identifier for each flight path
  mutate(id = row_number()) %>%
  # Ensure all numeric values are actually finite
  filter_at(vars(origin_lat:dest_lon), all_vars(is.finite(.))) %>%
  rowwise() %>%
  do({
    # Capture the current row's ID and count before calculating the arc
    current_id <- .$id
    current_n <- .$n

    gc <- gcIntermediate(
      c(.$origin_lon, .$origin_lat),
      c(.$dest_lon, .$dest_lat),
      n = 50,
      addStartEnd = TRUE,
      breakAtDateLine = TRUE
    )

    # Robustly handle matrix and vector outputs, and explicitly attach ID and N
    if (is.matrix(gc)) {
      as.data.frame(gc) %>%
        rename(long = lon) %>%
        # Explicitly attach the captured ID and N
        mutate(id = current_id, n = current_n)
    } else {
      # Handle case where gc is a simple vector (start/end points only)
      tibble(
        long = c(.$origin_lon, .$dest_lon),
        lat = c(.$origin_lat, .$dest_lat)
      ) %>%
        # Explicitly attach the captured ID and N
        mutate(id = current_id, n = current_n)
    }
  }) %>%
  ungroup()

3.6 Prepare map data, focusing on continental US

Show the code
# Get world map data
world_map <- map_data("world")

# Filter the map to focus on the continental US (approximate boundaries)
us_map <- world_map %>%
  filter(region %in% c("USA", "Canada", "Mexico"))

3.7 Create the map

Show the code
ggplot() +
  geom_polygon(
    data = us_map,
    aes(
      x = long,
      y = lat,
      group = group
    ),
    fill = 'grey85',
    color = 'grey55',
    linewidth = 0.1
  ) +
  geom_path(
    data = great_circle_arcs,
    aes(x = long, y = lat, group = id, alpha = n, linewidth = n),
    color = 'dodgerblue'
  ) +
  geom_point(
    data = flight_counts,
    aes(x = dest_lon, y = dest_lat),
    color = "#D55E00",
    size = 1.5
  ) +
  geom_point(
    data = flight_counts,
    aes(x = origin_lon, y = origin_lat),
    color = "#009E73",
    size = 1.5
  ) +
  geom_label_repel(
    data = top_routes,
    aes(x = dest_lon, y = dest_lat, label = Dest)
  ) +
  coord_sf(xlim = c(-125, -65), ylim = c(25, 50), expand = FALSE) +
  labs(
    title = "2024 End of Year Domestic Connections to JFK"
  ) +
  theme_minimal() +
  theme(
    legend.position = 'none',
    plot.title = element_text(face = "bold"),
    panel.grid.major = element_line(color = "grey95"),
    panel.grid.minor = element_line(color = "grey95"),
    axis.text = element_blank(), # Remove lat/long tick labels for a cleaner map
    axis.title = element_blank() # Remove axis titles)
  )

4 Day 3 - Polygons

Rather than looking at specific airports at the very end of the year, this simple choropleth presents where flights from JFK departed to in December of 2024.

4.1 Filter, Aggregate, and Join with map data

Show the code
states_counts <- flights |>
  filter(
    Year == 2024, Origin == "JFK"
  ) |>
  select(
    FlightDate,
    Reporting_Airline,
    Origin,
    OriginCityName,
    OriginState,
    Dest,
    DestCityName,
    DestState
  ) |>
    count(DestState)

us_states <-
    tibble(state_name = tolower(state.name), state_abb = state.abb)

us_states_map <- map_data("state")

jfk_departing_map_data <- us_states_map |>
    rename(state_name = region) |>
    left_join(us_states, by = "state_name") |>
    left_join(states_counts, by = join_by("state_abb" == "DestState"))

jfk_departing_map_data$n[is.na(jfk_departing_map_data$n)] <- 0

4.2 Create the map

Show the code
ggplot(jfk_departing_map_data,
aes(x = long, y = lat, group = group, fill = n)) +
    geom_polygon(color = 'white', linewidth = 0.1) +
    scale_fill_gradientn(colors = RColorBrewer::brewer.pal(9, "YlGnBu"), 
    name = "Flights from JFK") +
    coord_map("bonne", lat0 = 45) +
    labs(title = "December 2024 Flights From JFK by Destination State") +
    theme_minimal() +
    theme(
    plot.title = element_text(face = "bold"),
    axis.title = element_blank(),   
    axis.text = element_blank(),     
    panel.grid = element_blank()     
  )

Citation

BibTeX citation:
@online{kasowitz2025,
  author = {Kasowitz, Seth},
  title = {30DayMapChallenge {Days} 02 \& 03 - {Lines} \& {Polygons}},
  date = {2025-11-03},
  url = {https://sethkasowitz.com/posts/2025-11-03_30DayMapChallenge_Days2-3/},
  langid = {en}
}
For attribution, please cite this work as:
Kasowitz, Seth. 2025. “30DayMapChallenge Days 02 & 03 - Lines & Polygons.” November 3, 2025. https://sethkasowitz.com/posts/2025-11-03_30DayMapChallenge_Days2-3/.