# Check if libraries are installed; install if not.
if (!require("pacman")) install.packages("pacman")
::p_load(here, curl, tidyr, dplyr, lubridate) pacman
2 Assignment 2 - Reading On-line Data and Visualizing Hurricane Tracks
EVR-5086 Fall 2025
Assignment 2 - Reading On-line Data and Visualizing Hurricane Tracks
2.1 Data Retrieval and Parsing
The purpose of this code is to access and format the HURDAT2 dataset into an analysis-ready format. Since I expect to explore these data further, I did not want to perform wrangling and formatting only on filtered subsets. Instead, I extracted the header information for each storm record, expanded it, and attached it to each corresponding data line.
There are many possible approaches to this task, including base R methods. I chose to use tidyr and dplyr because their piping syntax allows me to write modular code without overwriting objects or cluttering the global environment. While I could have created a single long script, I prefer a modular workflow where each section has a clear intention. For example, this part of the workflow focuses only on data retrieval and formatting.
At the end of Section 2.1, I save the resulting data frame as an .RDS file. I prefer RDS over RData because RDS requires explicit object naming when loaded, which supports better coding practices and clearer, more reproducible code.
The steps of the code are annotated in the code chunks, and the first chunk defines and loads all the required libraries used in Section 2.1. One limitation of my approach is that effective use or contribution requires familiarity with GitHub, RStudio, and Quarto.
# Specify the source and output file name and location
<- "https://www.nhc.noaa.gov/data/hurdat/hurdat2-1851-2024-040425.txt"
url <- here("assignment2","hurdat.txt")
destfile
# Download and save the dataset
curl_download(url = url, destfile = destfile)
# Read in data and differentiate between headers and data lines
<- readLines("hurdat.txt") # Read text file
lines
# Prep headers
<- lines[grepl("^AL", lines)] # Keep only storm headers
storm_headers <- strsplit(storm_headers, ",") # Split based on ","
header_parts <- do.call(rbind, header_parts) # Convert to matrix
header_matrix <- as.data.frame(header_matrix) # Convert to data frame
header_df colnames(header_df) <- c("storm_id", "name", "rows") # Name columns
# Repeat each header based on the 'rows' column in tidyr
<- header_df |>
header_expand mutate(rows = as.numeric(rows)) |>
uncount(rows)
# Prep data
<- lines[!grepl("^AL", lines)] # Keep only data
storm_data <- strsplit(storm_data, ",") # Split based on ","
hurdat_parts <- do.call(rbind, hurdat_parts) # Convert to matrix
hurdat_matrix <- as.data.frame(hurdat_matrix) # Convert to to data frame
hurdat_df
<- c("yyyymmdd", "hhmm", "record", "status",
hurdat_fields "lat_hemi", "lon_hemi", "wind", "pressure",
"ne34", "se34", "sw34", "nw34",
"ne50", "se50", "sw50", "nw50",
"ne64", "se64", "sw64", "nw64", "radius")
colnames(hurdat_df) <- hurdat_fields # Name columns
# Build analysis ready data set
<- header_expand |>
hurdat_ar bind_cols(hurdat_df) |> # Glue together storm id and name with data
mutate(
yyyymmdd = ymd(yyyymmdd), # Tell R this is a date
hhmm = strptime(hhmm, format = "%H%M"), # Tell R this is a time
hhmm = format(hhmm, "%H:%M"),
lat = as.numeric(substr(lat_hemi, 1, nchar(lat_hemi)-1)), # Remove "S"
lon = as.numeric(substr(lon_hemi, 1, nchar(lon_hemi)-1)), # Remove "W"
lat_hemi = substr(lat_hemi, nchar(lat_hemi), nchar(lat_hemi)),
lon_hemi = substr(lon_hemi, nchar(lon_hemi), nchar(lon_hemi)),
lat = if_else(lat_hemi == "S", -lat, lat), # Make lat negative if "S"
lon = if_else(lon_hemi == "W", -lon, lon) # Make lon negative if "W"
|>
) mutate_at(c(9:25), as.numeric) |> # Make data numeric
mutate(across(where(is.numeric), ~na_if(., -999))) # Replace NAs
# Save formatted data to read into next quarto environment
saveRDS(hurdat_ar, file = here("assignment2", "hurdat.rds"))
2.2 Data Visualization
The data visualization has several exciting features. Because the data set was already formatted into an analysis-ready structure, this part of the assignment focuses only on visualizing a given storm ID. It begins with defining and loading the packages used later in the code. The leaflet and webshot2 packages were new to me. Leaflet allows me to create an interactive map, similar to folium in Python.
Since I am rendering my report to both HTML and PDF, I needed different approaches for each format. In HTML, I was able to embed the leaflet map directly as an htmlwidget. For PDF output, I learned to use conditional content so the widget only displays in HTML, and a static snapshot (generated with webshot2) is included in the PDF. For both versions, I provided a figure caption and alt text to improve clarity and accessibility.
To add more complexity and depth to the track visualization, I incorporated a color scale representing wind speed. This makes the map more informative and highlights storm intensity changes along its path. I think interactive widgets can serve as a useful precursor to fully developed applications for data visualization. I am excited about the continued advancements in interactive figures which allow non-coders to explore and interact with data in more meaningful ways. Although I did not implement dynamic selection in the HTML rendering of this assignment, I looked into some of the latest developments in Quarto Dashboards. For now, I set up an optional user input similar to what we did in Python. When the R code is run interactively, the user is prompted to provide a storm name; otherwise, a default storm ID is used to ensure the code still runs smoothly during rendering.
# Check if libraries are installed; install if not.
if (!require("pacman")) install.packages("pacman")
::p_load(here, stringr, leaflet, webshot2, dplyr) pacman
= "AL092021"
default_name
# If interactive (R console / RStudio), ask the user
if (interactive()) {
<- readline(
storm_id prompt = paste0("Enter a storm ID using ALnnyyy format [default = ", default_name, "]: ")
)if (storm_id == "") storm_id <- default_name
else {
} # If running non-interactively (e.g., knitting to PDF/HTML), use default
<- default_name
storm_id }
# Read in data
<- readRDS(here("assignment2", "hurdat.Rds"))
hurdat_ar
# Create a reusable function
<- function(dat, storm_id, zoom = 4,
track_storm init_location = c(20, -50)) {
# Filter and order the points for the selected storm
<- dat |>
storm filter(storm_id == !!storm_id, !is.na(lat), !is.na(lon)) |>
mutate(status = str_trim(status)) |>
arrange(yyyymmdd, hhmm)
if (nrow(storm) == 0) stop("No points found for this storm_id.")
# Build popup: date + time + status (e.g., "1851-06-25 00:00 — HU")
<- paste0(
popup_txt format(storm$yyyymmdd, "%Y-%m-%d"), " ", storm$hhmm,
" — ", storm$status
)
# Definecolor range
<- colorNumeric(
pal palette = "YlOrRd", # yelloe = weak winds, red = strong winds
domain = storm$wind # The range of wind speeds
)
# Create map
<- leaflet(storm) |>
m addTiles() |>
addPolylines(lng = ~lon, lat = ~lat, color = "blue",
weight = 2.5, opacity = 1) |>
addCircleMarkers(
lng = ~lon,
lat = ~lat,
color = ~pal(wind), # marker color by wind
radius = 5, # size of marker
stroke = FALSE,
fillOpacity = 0.8,
popup = ~paste0(format(yyyymmdd, "%Y-%m-%d"), " ", hhmm,
"<br>Wind: ", wind, " kt",
"<br>Status: ", status)
|>
) addLegend(
"bottomright",
pal = pal,
values = ~wind,
title = "Wind (kt)",
opacity = 1
)
<- here("assignment2", paste0(storm_id, "_map.html"))
file_html ::saveWidget(m, file_html, selfcontained = TRUE)
htmlwidgets
m
}
<- function(m) {
leaflet_png = here("assignment2", "hurricane_tracks_map.png")
file_png <- here("assignment2", "hurricane_tracks_map_tmp.html")
html_tmp ::saveWidget(m, html_tmp, selfcontained = TRUE)
htmlwidgets::webshot(html_tmp, file = file_png, vwidth = 1400,
webshot2vheight = 900, zoom = 1)
return(file_png)
}
<- track_storm(hurdat_ar, storm_id = storm_id)
m m