7.1 Dashboards

Shiny is a way to create web documents that can receive inputs from a user that can trigger further R code and dynamically change what’s shown on the screen. In some sense, you’ve already experienced interactivity in individual widgets, like leaflet maps that let you zoom in and out, or plotly plots that change their look when you hover over them. But consider those self-contained widgets of interactivity, whereas Shiny completely opens up the box to allow customizable inputs and outputs across your entire document (imagine some action on the map affecting a visual on a nearby chart, which would be impossible with just leaflet and plotly alone).

For our purposes, we’ll demonstrate not the most raw form of Shiny, which you can pick up on your own by following online tutorials, but instead a package called flexdashboard which provides an extra user-friendly layer on top of Shiny. It has its own detailed tutorial, but we’ll walk through the basics to take an analysis from this curriculum and publish it as a web app.

Unlike standard Shiny documents which are written as .R files, flexdashboard documents are built using .Rmd files, which we’re most familiar with. In Section 1.3, we described the YAML header, between two --- lines. For flexdashboard, instead of output: html_document, you’ll want to set output: flexdashboard::flex_dashboard (you can remove the standard author, date, and editor options). Once you’ve made these changes and save, then the next time you open this .Rmd document, you’ll notice that the “Knit” button is replaced with a “Run Document” button, which will let you preview your dashboard locally in a pop-up window.

flexdashboard standardizes certain design decisions to make your job easier. First, there’s a top navigation bar you can add buttons to by adding “level 1 markdown” headers like (==================) as shown here. Whatever you write in place of “Page 1” will be the name of the button on the top navigation bar. As for each individual page, then you generally design a layout of panes on a grid of columns and rows using “level 2 markdown” headers like (------------------) and ### headers as you’re familiar with; these options, implemented column-wise or row-wise, are shown here. Having set up the layout for your panes across one or more pages, and in a grid structure on each page, then the code chunks themselves create the actual outputs in each pane.

Generally, you would want to do as much pre-processing as possible in separate “processing” .Rmd scripts, so that all you’re doing in the dashboard is displaying outputs like leaflet maps and plotly charts (and, in more sophisticated dashboards, whatever interactive processing you would like the user to be able to trigger). Just like you usually have a first chunk where you load your libraries, you can start with a “global” chunk just below your YAML header, where you load libraries as well as readRDS() any files you’ve pre-processed. In the curly brackets that are always at the top of the chunk, after the three backticks and after the r, write global, include = F. In our demonstration, we’ll include the following within our “global” chunk, bringing back the PG&E data we used in Chapter 1 (pge_data_raw.rds is the result of rbind()ing the CSVs for electricity consumption from 2017-2020):

library(flexdashboard)
library(tidyverse)
library(leaflet)
library(sf)
library(plotly)

pge_data_raw <- readRDS("pge_data_raw.rds")

bay_zips <- readRDS("bay_zips.rds")

pge_data <-
  pge_data_raw %>% 
  filter(
    CUSTOMERCLASS %in% c(
      "Elec- Commercial",
      "Elec- Residential"
    )
  ) %>% 
  group_by(
    MONTH, 
    YEAR, 
    CUSTOMERCLASS
  ) %>% 
  summarize(
    TOTALKBTU = sum(TOTALKBTU, na.rm = T)
  ) %>% 
  mutate(
    DATE = 
      paste(
        YEAR,
        MONTH, 
        "01",
        sep="-"
      ) %>% as.Date()
  )

Then, to show a plot and a map side-by-side, we simply have to create two Column headers and include one chunk within each. Outside of chunks, you can still write text commentary, which in a flexdashboard will show up as rendered text in the appropriate locations.

Here’s the code to output a plotly object in the first column:

chart <- pge_data %>% 
  filter(
    CUSTOMERCLASS %in% c(
      "Elec- Residential"
    ),
    YEAR == 2020
  ) %>% 
  ggplot(
    aes(
      x = MONTH,
      y = TOTALKBTU/1e9
    )
  ) +
  geom_line(
    aes(
      color = YEAR %>% factor()
    )
  ) +
  scale_x_discrete(
    limits = c(
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec"
    )
  ) +
  labs(
    x = "",
    y = "Total kBTUs (billions)",
    title = "Residential Energy Consumption in the Bay Area, 2020",
    color = "Year"
  ) + 
  theme(legend.position = "none")

chart %>% 
  ggplotly() %>% 
  config(displayModeBar = F)

And here’s the code for the leaflet map in the second column:

pge_20_res_elec <-
  pge_data_raw %>% 
  filter(
    CUSTOMERCLASS == "Elec- Residential",
    YEAR == 2020
  ) %>% 
  mutate(
    ZIPCODE = ZIPCODE %>% as.character()
  ) %>% 
  group_by(ZIPCODE) %>% 
  summarize(
    TOTALKBTU = sum(TOTALKBTU, na.rm = T)
  ) %>% 
  right_join(
    bay_zips %>% select(GEOID10),
    by = c("ZIPCODE" = "GEOID10")
  ) %>% 
  st_as_sf() %>% 
  st_transform(4326)

res_pal <- colorNumeric(
  palette = "Reds",
  domain = 
    pge_20_res_elec$TOTALKBTU
)

leaflet() %>% 
  addProviderTiles(provider = providers$CartoDB.Positron) %>% 
  addPolygons(
    data = pge_20_res_elec,
    fillColor = ~res_pal(TOTALKBTU),
    color = "white",
    opacity = 0.5,
    fillOpacity = 0.5,
    weight = 1,
    label = ~paste0(
      round(TOTALKBTU), 
      " kBTU total in ",
      ZIPCODE
    ),
    highlightOptions = highlightOptions(
      weight = 2,
      opacity = 1
    )
  ) %>% 
  addLegend(
    data = pge_20_res_elec,
    pal = res_pal,
    values = ~TOTALKBTU,
    title = "Total Residential<br>Electricity (kBTU), 2020"
  )

The full code for this simple dashboard can be seen here, with all the formatting that we’ve just explained. Since this is a dashboard with no actual Shiny interactivity, it can be knit similarly to the knitted HTML documents you’ve created before, but you’ll need to save the .Rmd file, then write the following command in your console because the RStudio interface no longer gives you a Knit button option:

rmarkdown::render(
  input = "dashboard_demo1.Rmd", 
  output_format = "flexdashboard::flex_dashboard", 
  output_file = "dashboard_demo1.html"
)

This HTML can then be published on your personal GitHub page the same way. Our demo can be seen here.

Going from this simplest of dashboards to one that has Shiny interactivity requires publishing with a service like shinyapps.io, with which you can sign up for a free account and publish a set number of personal dashboards. We can add interactivity to the previous dashboard, specifically a dropdown menu for selecting the specific year of data that will be displayed in the plot and the map.

First, add runtime: shiny to the YAML section of your document.

Next, for flexdashboards, the standard location for placing inputs is in a “sidebar”, which is a first column given the extra parameter {.sidebar} (as shown here). The shiny package has a variety of functions that specifically create interactive input widgets, like dropdown menus (the primary set are listed here). You can learn how to use each one through practice. For our example, we use selectInput() as follows:

selectInput(
  inputId = "year", 
  label = "Year:",
  choices = c(2017,2018,2019,2020), 
  selected = 2020
)

The location you place this is where a dropdown menu will show up in the live dashboard. Using inputId=, you have generated an object located automatically at input$year, where year was your choice. In a Shiny app, you can imagine an empty list called input that is waiting to hold as many input variables as you would like it to be able to collect. When the user selects, say, “2020” (which happens to be the default selection because of selected=), then in real-time, the application stores “2020” in input$year. When the user changes the selection to “2019”, then input$year is updated to store “2019”. So in other parts of the script, we can refer to input$year, and it will be able to “observe” the change and re-execute entire sections of code with the new value of input$year.

The following is the resulting updated structure of the plotly column code. We now use two chunks. The first is simply:

plotlyOutput("plot")

Because out plot is now “dynamic”, we also need to store the plot itself in a more dynamic way. This function specifically pairs with plotly to create dynamic plotly objects, and you have instantiated the slot output$plot. Again, imagine an empty list called output that mirrors the input list, and is waiting to hold as many outputs as you would like to be able to dynamically display. After this “frontend” chunk”, we now need a “backend” chunk that will be prepared to update the value of output$plot. We establish the “backend” nature of the chunk by writing context = “server” where we would typically write echo = F. The chunk itself looks like this:

observeEvent(input$year, {
  
  chart <- pge_data %>% 
    filter(
      CUSTOMERCLASS %in% c(
        "Elec- Residential"
      ),
      YEAR == input$year
    ) %>% 
    ggplot(
      aes(
        x = MONTH,
        y = TOTALKBTU/1e9
      )
    ) +
    geom_line(
      aes(
        color = YEAR %>% factor()
      )
    ) +
    scale_x_discrete(
      limits = c(
        "Jan",
        "Feb",
        "Mar",
        "Apr",
        "May",
        "Jun",
        "Jul",
        "Aug",
        "Sep",
        "Oct",
        "Nov",
        "Dec"
      )
    ) +
    labs(
      x = "",
      y = "Total kBTUs (billions)",
      title = paste0("Residential Energy Consumption in the Bay Area, ", input$year),
      color = "Year"
    ) + 
    theme(legend.position = "none")
  
  output$plot <- renderPlotly({
    chart %>% 
      ggplotly() %>% 
      config(displayModeBar = F)
  })
  
})

There are some slight cosmetic changes to properly label the dynamically changing plot, but mostly the only change is that the previous code has been placed inside of an observeEvent() function. This takes as its first parameter the “signal” to listen for, which we provide as input$year. So whenever input$year changes, this code will run, and will use input$year. The second parameter is the set of code we want to run. Where we previously hard-coded “2020”, now we use input$year. And at the end, we pass the output leaflet object into a function called renderPlotly(), which prepares the plot in the necessary format to be given to output$plot. These are some of many techniques to pick up over time when specifically creating Shiny applications.

Making the map dynamic is structurally similar. First we provide the “frontend” chunk:

leafletOutput("map")

And then the “backend” chunk (with context = “server” in the first bracket):

observeEvent(input$year, {
  
  pge_res_elec <-
    pge_data_raw %>% 
    filter(
      CUSTOMERCLASS == "Elec- Residential",
      YEAR == input$year
    ) %>% 
    mutate(
      ZIPCODE = ZIPCODE %>% as.character()
    ) %>% 
    group_by(ZIPCODE) %>% 
    summarize(
      TOTALKBTU = sum(TOTALKBTU, na.rm = T)
    ) %>% 
    right_join(
      bay_zips %>% select(GEOID10),
      by = c("ZIPCODE" = "GEOID10")
    ) %>% 
    st_as_sf() %>% 
    st_transform(4326)
  
  res_pal <- colorNumeric(
    palette = "Reds",
    domain = 
      pge_res_elec$TOTALKBTU
  )
  
  output$map <- renderLeaflet({
    leaflet() %>% 
      addProviderTiles(provider = providers$CartoDB.Positron) %>% 
      addPolygons(
        data = pge_res_elec,
        fillColor = ~res_pal(TOTALKBTU),
        color = "white",
        opacity = 0.5,
        fillOpacity = 0.5,
        weight = 1,
        label = ~paste0(
          round(TOTALKBTU), 
          " kBTU total in ",
          ZIPCODE
        ),
        highlightOptions = highlightOptions(
          weight = 2,
          opacity = 1
        )
      ) %>% 
      addLegend(
        data = pge_res_elec,
        pal = res_pal,
        values = ~TOTALKBTU,
        title = paste0("Total Residential<br>Electricity (kBTU), ", input$year)
      )
  })
  
})

Notice leafletOutput() and renderLeaflet() as the analogous Shiny functions to make the map dynamic. Essentially, when a year is picked from the dropdown menu, both the plot and the map are completely regenerated. There are more advanced ways to keep plots and maps “partially” existent, and only swap out individual lines or individual shapes. And beyond having just one interactive input, you can have many in the sidebar, all triggering different types of dynamic changes effectively simultaneously, and furthermore, you can trigger events through direct selections on the map which can cause changes in the plot, and vice versa.

The full code for this second dashboard can be seen here, with all the necessary formatting. You should be able to preview this dashboard as a pop-up in RStudio, with all interactivity. To publish, you need to have created your Shinyapps.io account and configured a token. Then you will find a “Publish” option on the same toolbar as the Knit button, which will let you publish a live web app your Shinyapps.io account at a specified URL. This is fundamentally different from Knitting because this app needs a server to be able to store those input and output dynamic objects. Once you start building more elaborate apps, then be prepared to pay some kind of server cost with Shinyapps.io or another provider to be able to “host” these apps online. Our live hosted dashboard can be viewed here.