A heatmap as calendar

Recently I was looking for a visual representation to show the daily changes of temperature, precipitation and wind in an application xeo81.shinyapps.io/MeteoExtremosGalicia (in Spanish), which led me to use a heatmap in the form of a calendar. The shiny application is updated every four hours with new data showing calendars for each weather station. The heatmap as a calendar allows you to visualize any variable with a daily time reference.

Packages

In this post we will use the following packages:

Package Description
tidyverse Collection of packages (visualization, manipulation): ggplot2, dplyr, purrr, etc.
lubridate Easy manipulation of dates and times
ragg ragg provides a set of high quality and high performance raster devices
# instalamos los paquetes si hace falta
if(!require("tidyverse")) install.packages("tidyverse")
if(!require("ragg")) install.packages("ragg")
if(!require("lubridate")) install.packages("lubridate")

# paquetes
library(tidyverse)
library(lubridate)
library(ragg)

For those with less experience with tidyverse, I recommend the short introduction on this blog post.

Data

In this example we will use the daily precipitation of Santiago de Compostela for this year 2020 (until December 20) download.

# import the data
dat_pr <- read_csv("precipitation_santiago.csv")
dat_pr
## # A tibble: 355 x 2
##    date          pr
##    <date>     <dbl>
##  1 2020-01-01   0  
##  2 2020-01-02   0  
##  3 2020-01-03   5.4
##  4 2020-01-04   0  
##  5 2020-01-05   0  
##  6 2020-01-06   0  
##  7 2020-01-07   0  
##  8 2020-01-08   1  
##  9 2020-01-09   3.8
## 10 2020-01-10   0  
## # ... with 345 more rows

Preparation

In the first step we must 1) complement the time series from December 21 to December 31 with NA, 2) add the day of the week, the month, the week number and the day. Depending on whether we want each week to start on Sunday or Monday, we indicate it in the wday() function.

dat_pr <- dat_pr %>% 
          complete(date = seq(ymd("2020-01-01"), 
                              ymd("2020-12-31"), 
                              "day")) %>%
          mutate(weekday = wday(date, label = T, week_start = 1), 
                 month = month(date, label = T, abbr = F),
                 week = isoweek(date),
                 day = day(date))

In the next step we need to make a change in the week of the year, which is because in certain years there may be, for example, a few days at the end of the year as the first week of the following year. We also create two new columns. On the one hand, we categorize precipitation into 14 classes and on the other, we define a white text color for darker tones in the heatmap.

dat_pr <- mutate(dat_pr, 
                 week = case_when(month == "December" & week == 1 ~ 53,
                                  month == "January" & week %in% 52:53 ~ 0,
                                  TRUE ~ week),
                 pcat = cut(pr, c(-1, 0, .5, 1:5, 7, 9, 15, 20, 25, 30, 300)),
                 text_col = ifelse(pcat %in% c("(15,20]", "(20,25]", "(25,30]", "(30,300]"), 
                                   "white", "black")) 
      
dat_pr  
## # A tibble: 366 x 8
##    date          pr weekday month    week   day pcat    text_col
##    <date>     <dbl> <ord>   <ord>   <dbl> <int> <fct>   <chr>   
##  1 2020-01-01   0   Wed     January     1     1 (-1,0]  black   
##  2 2020-01-02   0   Thu     January     1     2 (-1,0]  black   
##  3 2020-01-03   5.4 Fri     January     1     3 (5,7]   black   
##  4 2020-01-04   0   Sat     January     1     4 (-1,0]  black   
##  5 2020-01-05   0   Sun     January     1     5 (-1,0]  black   
##  6 2020-01-06   0   Mon     January     2     6 (-1,0]  black   
##  7 2020-01-07   0   Tue     January     2     7 (-1,0]  black   
##  8 2020-01-08   1   Wed     January     2     8 (0.5,1] black   
##  9 2020-01-09   3.8 Thu     January     2     9 (3,4]   black   
## 10 2020-01-10   0   Fri     January     2    10 (-1,0]  black   
## # ... with 356 more rows

Visualization

First we create a color ramp from Brewer colors.

# color ramp
pubu <- RColorBrewer::brewer.pal(9, "PuBu")
col_p <- colorRampPalette(pubu)

Second, before building the chart, we define a custom theme as a function. To do this, we specify all the elements and their modifications with the help of the theme() function.

theme_calendar <- function(){

 theme(aspect.ratio = 1/2,
       
       axis.title = element_blank(),
       axis.ticks = element_blank(),
       axis.text.y = element_blank(),
       axis.text = element_text(family = "Montserrat"),
       
       panel.grid = element_blank(),
       panel.background = element_blank(),
       
       strip.background = element_blank(),
       strip.text = element_text(family = "Montserrat", face = "bold", size = 15),
       
       legend.position = "top",
       legend.text = element_text(family = "Montserrat", hjust = .5),
       legend.title = element_text(family = "Montserrat", size = 9, hjust = 1),
       
       plot.caption =  element_text(family = "Montserrat", hjust = 1, size = 8),
       panel.border = element_rect(colour = "grey", fill=NA, size=1),
       plot.title = element_text(family = "Montserrat", hjust = .5, size = 26, 
                                 face = "bold", 
                                 margin = margin(0,0,0.5,0, unit = "cm")),
       plot.subtitle = element_text(family = "Montserrat", hjust = .5, size = 16)
  )
}

Finally, we build the final chart using geom_tile() and specify the day of the week as the X axis and the week number as the Y axis. As you can see in the variable of the week number (-week), I change the sign so that the first day of each month is in the first row. With geom_text() we add the number of each day with its color according to what we defined previously. In guides we make the adjustments of the colorbar and in scale_fill/colour_manual() we define the corresponding colors. An important step is found in facet_wrap() where we specify the facets composition of each month. The facets should have free scales and the ideal would be a 4 x 3 facet distribution. It is possible to modify the position of the day number to another using the arguments nudge_* in geom_text() (eg bottom-right corner: nudge_x = .35, nudge_y = -.25).

    ggplot(dat_pr, 
           aes(weekday, -week, fill = pcat)) +
      geom_tile(colour = "white", size = .4)  + 
      geom_text(aes(label = day, colour = text_col), size = 2.5) +
      guides(fill = guide_colorsteps(barwidth = 25, 
                                     barheight = .4,
                                    title.position = "top")) +
       scale_fill_manual(values = c("white", col_p(13)),
                         na.value = "grey90", drop = FALSE) +
       scale_colour_manual(values = c("black", "white"), guide = FALSE) + 
       facet_wrap(~ month, nrow = 4, ncol = 3, scales = "free") +
       labs(title = "How is 2020 being in Santiago?", 
             subtitle = "Precipitation",
             caption = "Data: Meteogalicia",
             fill = "mm") +
       theme_calendar()

To export we will use the ragg package, which provides higher performance and quality than the standard raster devices provided by grDevices.

ggsave("pr_calendar.png", height = 10, width = 8, device = agg_png())

In other heatmap calendars I have added the predominant wind direction of each day as an arrow using geom_arrow() from the metR package (it can be seen in the aforementioned application).

Buy Me A Coffee

Dr. Dominic Royé
Dr. Dominic Royé
Researcher and responsible for data science

Related