Mapping Severe Thunderstorm Outlook in R using Leaflet

[This article was first published on Andy Pickering, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Introduction

Living on the Colorado front range in summer means dealing with a chance of thunderstorms almost every afternoon, some of which can become severe.

In this post I’ll go over how I mapped the severe thunderstorm risk outlook from the NOAA Storm Prediction Center , using R and leaflet.

Code
suppressPackageStartupMessages(library(sf))
library(leaflet)
library(rvest)
library(stringr)
library(htmltools)

Data

The day 1 (ie today) severe thunderstorm risk (Convective Outlook) can be found at the website: NOAA Storm Prediction Center . This website provides a map where you can toggle a bunch of layers on/off, but you cannot zoom in/out to get a more detailed view of where the warning areas fall relative to other non-major cities. Depending on where the risk falls, it can sometimes be difficult to tell exactly where the boundaries are, or what category your home town is in. My goal was to try to view the data in an interactive map where I can zoom in/out and see more detail. There are 2 ways of doing this:

  1. View the data in Google Earth: Above the map there is an icon you can click to download a kmz file that can be opened in Google Earth.

  2. Download the data as shapefiles (a link is provided above the map next to the kml download) and process them into a map.

In this post I am focusing on working with the shapefiles and creating an interactive map using R and leaflet.

Downloading the data

The shapefiles can be downloaded manually by clicking the link on the website, but the link changes whenever the forecast is updated. I wanted to do this in a more programmatic way and be able to automatically find the link for the latest forecast without having to manually copy and paste the link each time I run it. I experimented with using the SelectorGadget to isolate the link, but found it easier to create a list of all the links in the page using the rvest (Wickham 2022) package and then find the link containing the shapefile (ends in .shp.zip).

Code
base_url <- "https://www.spc.noaa.gov"


# Define a function to make a list of all links on a webpage
get_all_links <- function(page_url){
  
  # read the html from the website for day 1 outlook
  html <- rvest::read_html(page_url)
  
  # create a list of all the hyperlinks on the website
  links <- rvest::html_attr(html_nodes(html, "a"), "href")
  
}

links <- get_all_links(page_url=paste0(base_url,"/products/outlook/day1otlk.html"))

# find the link for the shapefile (they only one that ends in 'shp.zip') 
shp_link <- links[which(stringr::str_ends(links,'shp.zip'))]
shp_url <- paste0(base_url,shp_link)
print(paste('The latest shapefile as of ',Sys.time(),' is ',shp_url))
[1] "The latest shapefile as of  2023-07-06 10:20:49  is  https://www.spc.noaa.gov/products/outlook/archive/2023/day1otlk_20230706_1300-shp.zip"
Code
# filename of shapefile
shp_fname <- basename(shp_url)
#print(shp_fname)

# base filename (remove *-shp.zip*) to use to load files later
basefname <- stringr::str_remove(shp_fname,"-shp.zip")
#print(basefname)


Now that we have the link for the latest forecast, we can download the file (zip file) and unzip.

  • The unzipped folder contains shapefiles files for tornado,wind, and hail threat, but I will focus on just the categorical risk for severe thunderstorms (this is what you have probably seen on the weather forecast on the news). The shapfile I am interested in ends in cat.shp

  • Then we can use the sf (Pebesma 2018) package to read the shapefile into R

Code
# destination to save downloaded file to
dest_file <- file.path('.','data',shp_fname)
# download the zip file containing shapefiles
download.file(url=shp_url,destfile = dest_file, method="curl",quiet = TRUE)

# unzip into a separate folder using base filename
unzip(dest_file,exdir = file.path('.','data',basefname) )

# read shapefile into R w/ sf package
cat_file <- stringr::str_remove(basename(shp_url),"-shp.zip")
dat <- sf::st_read(file.path('.','data',basefname,paste0(basefname,'_cat.shp')))
Reading layer `day1otlk_20230706_1300_cat' from data source 
  `/Users/andy/Projects/Andy_Pickering_Portfolio/posts/Storm_Prediction_Center/data/day1otlk_20230706_1300/day1otlk_20230706_1300_cat.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 4 features and 8 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -123.85 ymin: 24.169 xmax: -67.471 ymax: 49.561
Geodetic CRS:  WGS 84

Examine the object extracted from the shapefile:

Code
class(dat)
[1] "sf"         "data.frame"
Code
dat
Simple feature collection with 4 features and 8 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -123.85 ymin: 24.169 xmax: -67.471 ymax: 49.561
Geodetic CRS:  WGS 84
  DN        VALID       EXPIRE        ISSUE LABEL                     LABEL2
1  2 202307061300 202307071200 202307061244  TSTM General Thunderstorms Risk
2  3 202307061300 202307071200 202307061244  MRGL              Marginal Risk
3  4 202307061300 202307071200 202307061244  SLGT                Slight Risk
4  5 202307061300 202307071200 202307061244   ENH              Enhanced Risk
   stroke    fill                       geometry
1 #55BB55 #C1E9C1 POLYGON ((-92.46012 48.6844...
2 #005500 #66A366 POLYGON ((-109.69 47.18, -1...
3 #DDAA00 #FFE066 POLYGON ((-105.19 45.5, -10...
4 #FF6600 #FFA366 POLYGON ((-102.51 39.91, -9...

Mapping the data

Now that we have the shapefile converted into a R object, we can make our map. I’ll be using the Leaflet (Cheng, Karambelkar, and Xie 2023) package to create a nice interactive map.

  • One caveat is that the number of categories is not constant; it can vary (up to 5 categorgies) depending on the forecast. So we need to use a for loop to plot whichever categories are present (there is a row in the sf object for each risk category present in the forecast.
  • We now have an interactive map (try zooming in/out and hovering over the different areas with the cursor!).
  • NOTE this map is for the forecast at the time this post was rendered, not when you are reading it
Code
#| fig-cap: Map Showing Severe Weather Prediction Risk

# extract bounding box values from shapefile to set map bounds
bb <- as.list(st_bbox(dat))

# make base map
m <- leaflet() %>% 
      addTiles()

# add layers (1 for each risk category present in forecast)
for (i in 1:length(dat$geometry)){
m <- addPolygons(map = m, data = dat$geometry[i],
                 color=dat$fill[i],
                 label = dat$LABEL2[i])  
}

m <- m %>% 
  setMaxBounds(lng1 = bb$xmin,lng2 = bb$xmax,lat1 = bb$ymin, lat2=bb$ymax) %>% 
  addLegend(labels=dat$LABEL2,colors = dat$fill)

m

SessionInfo

Code
sessionInfo()
R version 4.2.3 (2023-03-15)
Platform: x86_64-apple-darwin17.0 (64-bit)
Running under: macOS Big Sur ... 10.16

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
[1] htmltools_0.5.5 stringr_1.5.0   rvest_1.0.3     leaflet_2.1.2  
[5] sf_1.0-13      

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.10        compiler_4.2.3     pillar_1.9.0       class_7.3-22      
 [5] tools_4.2.3        digest_0.6.31      jsonlite_1.8.5     evaluate_0.21     
 [9] lifecycle_1.0.3    tibble_3.2.1       pkgconfig_2.0.3    rlang_1.1.1       
[13] DBI_1.1.3          cli_3.6.1          rstudioapi_0.14    crosstalk_1.2.0   
[17] curl_5.0.1         yaml_2.3.7         xfun_0.39          fastmap_1.1.1     
[21] e1071_1.7-13       xml2_1.3.4         httr_1.4.6         dplyr_1.1.2       
[25] knitr_1.43         generics_0.1.3     vctrs_0.6.2        htmlwidgets_1.6.2 
[29] classInt_0.4-9     grid_4.2.3         tidyselect_1.2.0   glue_1.6.2        
[33] R6_2.5.1           fansi_1.0.4        rmarkdown_2.22     selectr_0.4-2     
[37] magrittr_2.0.3     ellipsis_0.3.2     units_0.8-2        renv_0.17.3       
[41] KernSmooth_2.23-21 utf8_1.2.3         stringi_1.7.12     proxy_0.4-27      

References

Cheng, Joe, Bhaskar Karambelkar, and Yihui Xie. 2023. “Leaflet: Create Interactive Web Maps with the JavaScript ’Leaflet’ Library.” https://CRAN.R-project.org/package=leaflet.
Pebesma, Edzer. 2018. Simple Features for r: Standardized Support for Spatial Vector Data 10. https://doi.org/10.32614/RJ-2018-009.
Wickham, Hadley. 2022. “Rvest: Easily Harvest (Scrape) Web Pages.” https://CRAN.R-project.org/package=rvest.
To leave a comment for the author, please follow the link and comment on their blog: Andy Pickering.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.