This tutorial accompanies the Small Town Big Data blog post Snow Stripes: Rhythms of winter snowfall in Colorado’s famed ski towns.

The code described here culminates in the creation of the Snow Stripes app, available here. Make your own Snow Stripes, no code required!


In a ski town, time revolves around one thing: snow.

This tutorial offers an introduction to making Snow Stripes, a minimalist visualization inspired by the Warming Stripes project by Ed Hawkins. The code I used here is partially inspired by a tutorial from Dominique Roye. But instead of warming global temperatures, we’re using daily snowfall totals from the National Weather Service to capture the vibe of winter in a ski town. In this tutorial we’ll cover:

*This was my first time making a Shiny app. I’ll be sharing the code I used to make the Snow Stripes app, however, I’m far from the right person to teach you how to do it yourself. I’ll share links to the tutorials I used to create mine, as well as my commented code. You’ve now been officially warned not to use me for inspiration on Shiny apps.

To start, I recommend downloading RStudio. Open up a new .R file and start cutting and pasting! This is a good one for beginners as the dataset is unfrightening and I’ve included them all already in .tsv format. in the GitHub repository. The plotting is fairly straightforward and the data cleaning tasks are good ones to learn at the beginning of your journey!


Let’s check out these data.

National Weather Service daily snowfall records are straightforward. They have just two columns: date and snowfall in inches. In these data, sometimes there is an “M” or a “T” thrown in there. That’s when the station could not record a snowfall amount for some reason. What made me fall for this dataset is its longevity: some of the snowfall records go back to 1893. Imagine a winter in the high Rockies before cars and before….chairlifts! While we’ll constrain our analysis to the most reliable years (1920 and later) just knowing how far we can peek into weather history with data is immensely satisfying.

Loading .tsv data and working with dreaded dates

# Use this command to install the packages needed for this tutorial:
# install.packages(c("ggplot2", "lubridate", "dplyr"))
library(ggplot2)
library(lubridate)
library(dplyr)

# Read in the data and rename the columns
steamboat<-read.table(file = 'data/steamboatdaily.tsv', sep = '\t', header = TRUE)
names(steamboat)<-c("date", "snow")

# To start, the snowfall value is a string, so R isn't reading it as a numeric value. Let's make that into a number and change those letters mentioned above into NAs. 
steamboat$snow<-as.numeric(steamboat$snow)

# The date column is a date. But R doesn't know it's a date yet. 
# It's thinks it's a string - to R, it could be a list of literally anything. 
# But we want R to know it's a date so we can manipulate the data by time period. 
# In the lubridate library, as.Date() turns that column into an R-recognized date. 
# The format we enter is how we're telling R to read what's written in the column. 
steamboat$date<-as.Date(steamboat$date, format ="%Y-%m-%d")

#We have a calendar problem. I don't like how Jan. 1 is the begnning of the year. 
# Because, in a ski town, Jan 1 is the MIDDLE of the year. We all know this. 
# So, we're going to make a new column called snow.year, which is a July to July calendar named after the year it ends in. 
# This makes winter the middle of the year, which is akin to how it feels when winter is the centerpiece of common experience.
steamboat$snow.year<-year(steamboat$date) + (month(steamboat$date) >= 7)


# To make it easier to plot, we'll make a new column that just numbers the days in each year. 
steamboat<-steamboat %>% group_by(snow.year) %>% mutate(plotnumber = 1:n())

# While I pulled numbers back to 1900, there are too many NAs in the early data. 
# We'll keep this analysis to the last 100 years for the most reliable data. 
snow<-steamboat[steamboat$snow.year > 1920 & steamboat$snow.year < 2021,]

# Here's what we're working with:
head(snow)
## # A tibble: 6 x 4
## # Groups:   snow.year [1]
##   date        snow snow.year plotnumber
##   <date>     <dbl>     <dbl>      <int>
## 1 1920-07-01     0      1921          1
## 2 1920-07-02     0      1921          2
## 3 1920-07-03     0      1921          3
## 4 1920-07-04     0      1921          4
## 5 1920-07-05     0      1921          5
## 6 1920-07-06     0      1921          6

Earning your Snow Stripes with ggplot2

# I made this color palette using an online color ramp generator so I could play with the color gradient in real time. 
# I highly recommend using online tools like my favorite, RampGenerator.com. 
# You can copy those hex codes into R when you find a combination you like. Or use mine: 
cols<-colorRampPalette(colors=c("#E8DED1","#7FD27F", "#5BA1FF", "#932EDF", "#FF00D0"), bias =2)

# This next object is made of a lot of specific theme elements that ggplot2 needs to make this plot exactly how we want. 
# Mostly, these commands make all the defaults disappear. We can also dial in the fonts we want. 
# Making this a separate object makes it super easy to add to ggplot objects later on. 

theme_strip <- theme_minimal()+
  theme(axis.text.y = element_blank(),
        axis.line.y = element_blank(),
        axis.title = element_blank(),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        plot.title = element_text(size = 14, face = "bold"),
        legend.text = element_text(size=16, color="grey25", family="sans"),
        axis.text.x = element_text(size=10, color="grey25", family="sans", vjust=4 ),
        legend.title = element_text(size=16, color="grey25", family="sans"),
        rect = element_rect(fill="transparent")
  )



# Here is a Snow Stripe! I started with the first year Steamboat Resort operated, the 1963-64 season:
test<-steamboat[steamboat$snow.year==1964,]

ggplot(test, aes(x = plotnumber, y = 1, fill = snow))+
  geom_tile() +
  scale_fill_gradientn(colors=cols(300), na.value = "#E8DED1") +
  scale_x_continuous(breaks=c(1,185,365), labels=c("July", "Jan", "July")) +
  guides(fill = guide_colorbar(barwidth = 1))+ theme_strip + 
  labs(fill="snowfall (in.)")


What about some other ski towns? Let’s load in a few more daily snowfall histories so we can make more Snow Stripes from around Colorado.

Lapply() to the rescue

We want to do what we did to the Steamboat data above and repeat it for the data for each new ski town’s snowfall data. We could use a nice tidy for loop to do that, like I demonstrated in February’s blog post on land cover data. However, most advanced R users will tell you that for loops can be cumbersome and slow. They will eventually convince you that lapply() is the way. Here’s how to appease these masters:

# Let's load in other Colorado ski town snowfall histories
steamboat<-read.table(file = 'data/steamboatdaily.tsv', sep = '\t', header = TRUE)
vail<-read.table(file = 'data/vaildaily.tsv', sep = '\t', header = TRUE)
breck<-read.table(file = 'data/breckdaily.tsv', sep = '\t', header = TRUE)
wp<-read.table(file = 'data/winterparkdaily.tsv', sep = '\t', header = TRUE)
cb<-read.table(file = 'data/crestedbuttedaily.tsv', sep = '\t', header = TRUE)
telluride<-read.table(file = 'data/telluridedaily.tsv', sep = '\t', header = TRUE)
silverton<-read.table(file = 'data/silvertondaily.tsv', sep = '\t', header = TRUE)


# We take each of these objects and make them into a list. The list object 'combineddata' contains the data for all the towns. 
combineddata<-list(steamboat,vail,breck,wp,cb,telluride,silverton)

# You can access parts of the list by position with the [[]] brackets. 
# For example, the Steamboat data is in the first position and can be viewed with:
head(combineddata[[1]])
##   STEAMBOAT.SPRINGS snow
## 1        1900-01-01    M
## 2        1900-01-02    M
## 3        1900-01-03    M
## 4        1900-01-04    M
## 5        1900-01-05    M
## 6        1900-01-06    M
# Now, we take every single step we did to clean the Steamboat data and wrap it in a function-making machine. 
# Meaning, we're making a custom function where the argument (x) is the dataset we want to clean: 

cleaning<-function(x) {
  names(x)<-c("date", "snow")
  x$date<-as.Date(x$date, format ="%Y-%m-%d")
  x$snow<-as.numeric(x$snow)
  x$snow.year<-year(x$date) + (month(x$date) >= 7)
  years<-unique(x$snow.year)
  x<-x %>% group_by(snow.year) %>% mutate(plotnumber = 1:n())
  snow<-x[x$snow.year > 1920 & x$snow.year < 2021,]
  snow
}

# THEN, we pass that entire list of datasets to that function. The 'l' in lapply() means list. 
# So you 'apply' the function to a 'list.' Argument one is the list, argument two is the custom function we made. 
data_list<-lapply(combineddata, FUN = cleaning)

#The 'data_list' object is now a list of perfectly cleaned data all ready for Snow Stripe-making 
names(data_list)<-c("Steamboat", "Vail", "Breckenridge", "Winter Park", "Crested Butte", "Telluride", "Silverton")


#You can make a year of Snow Stripes from any year of any member of this list like so:

listtest<-data_list[["Crested Butte"]][data_list[["Crested Butte"]]$snow.year==2009,]

ggplot(listtest, aes(x = plotnumber, y = 1, fill = snow))+
  geom_tile() +
  scale_fill_gradientn(colors=cols(300), na.value = "#E8DED1") +
  scale_x_continuous(breaks=c(1,185,365), labels=c("July", "Jan", "July")) +
  guides(fill = guide_colorbar(barwidth = 1))+ theme_strip + 
  labs(fill="snowfall (in.)")

#Looks like a decently snowy year in CB. 

Making the Shiny app

I am not qualified to teach you how to build a Shiny app. This was my first time making one, and it got complicated quickly. I highly recommend moving through Shiny’s series of tutorials to build your own, which you can find here.

However, I’m still sharing all the code I used to build the Snow Stripes app so you can peek under the hood. I’ve commented it out so you can use it for reference if you are interested in building your own app. You’ve been forewarned: do as Shiny says, not as I have done.

library(shinythemes)
library(shiny)

# Define UI. 
 # This is what the user of the app will see. It's a hot mess of CSS, HTML and R to get it looking the way I wanted. 
ui <- fluidPage(
  theme = shinytheme("slate"),
  tags$head(tags$style(HTML("a {color: #8565c4; text-decoration: underline}"))),
  
 # This is the title at the top of the page
  titlePanel("Snow Stripes", windowTitle = "Snow Stripes: Rhythms of winter in Colorado ski towns"),
 # The plot goes next 
 plotOutput("map", height=620),
  
  hr(),

 # This next row consists of three columns: One of text and two with drop down menus.   
  fluidRow(
  column(4, h4(em("Bring the rhythms of winter to life with daily snowfall data visualizations for Colorado's renowned ski towns."))),
 # selectInput() is the command for a dropdown menu, for which the choices are the list of dataframes we made earlier in the tutorial. 
  column(4,  selectInput("town", label=h5("Select ski town"), 
                         choices = names(data_list), selected=1)),
 # And the same for years. 
  column(4,  selectInput("year", label=h5("Select snow year (previous July to July)"), 
                        choices = rev(unique(data_list[[1]]$snow.year)), selected=1))),
hr(),
br(),

 # The last few rows are documentation, credits and links. 
fluidRow(style = "padding:10px", em(tags$div(
  "This app is inspired by Ed Hawkins' ",
  tags$a(href="https://showyourstripes.info/", 
         "Warming Stripes project,"), " with code inspiration from Dominique Roye, whose tutorial can be found ",
  tags$a(href="https://dominicroye.github.io/en/2018/how-to-create-warming-stripes-in-r/", "here.")
))),


fluidRow(style = "padding:10px",h5(em(
 tags$div(strong("Data notes:"), "Snowfall data sourced from",
          tags$a(href="https://climate.colostate.edu/data_access.html", "Colorado State University's Colorado Climate Center. "),
          "Snowfall totals are NOT offical ski resort mid-mountain records, 
               but National Weather Service stations often located in town at the base of ski resorts. You might remember some winters as skiing deeper 
          - if you can get your hands on daily official resort totals send them my way!")))),

fluidRow(style = "padding:10px", (em(tags$div("Learn to create these snow stripes (or make any other kind of stripes you want) with the ",
tags$a(href="https://smalltownbigdata.github.io/april2021-snowfall/april2021-snowfall.html", "associated tutorial on GitHub."),
 "Read the associated blog post at ",
tags$a(href="https://www.smalltownbigdata.com/post/colorado-ski-town-snowfall-data", "Small Town Big Data."))))),

fluidRow(style= "text-align: right; padding:10px", h5(em("Nikki Inglis | 2020 | Small Town Big Data.")))
)


# Define server logic
 # This is what runs in the background to produce the plot in the main window,
 # and how the app responds to users interacting to the selection menus
server <- function(input, output, session) {
  
  library(Cairo)
  options(shiny.usecairo=T)
  
  # This is how we make a plot appear in the UI:
  output$map <- renderPlot({
  # This is just our ggplot function we used in the above Snow Stripes tutorial, however the town and year are
  # replaced by the user's choices from the UI code
    ggplot(data_list[[input$town]][data_list[[input$town]]$snow.year==input$year,], aes(x = plotnumber, y = 1, fill = snow))+
    geom_tile() +
    scale_fill_gradientn(colors=cols(300), na.value = "#E8DED1") +
    scale_x_continuous(breaks=c(1,185,365), labels=c("July", "Jan", "July")) +
    guides(fill = guide_colorbar(barwidth = 1))+ theme_strip + 
      theme(rect = element_rect(fill="transparent")) + 
      theme(legend.text = element_text(size=16, color="grey95", family="sans"),
            axis.text.x = element_text(size=10, color="grey95", family="sans" ),
            legend.title = element_text(size=16, color="grey95", family="sans")) +
      labs(fill="snowfall (in.)")}, 
  bg="transparent")

  # This little nugget below is what "updates" the years based on what town is chosen.
  # For example, if Vail is chosen, only 1985 and later will show in the year drop-down menu because it's all that's available.
  observe({
  updateSelectInput(session, "year", choices = rev(unique(data_list[[input$town]]$snow.year)))
  })
}

 # Run the app. This command should pop up a new window in which your app runs on a local server. Check Shiny documentation for more on how to publish your app online! 
shinyApp(ui = ui, server = server)

If you have any questions on Shinies, Stripes, or anything else, email me!.

Next post drops May 4.