14 Interactive dashboards with flexdashboard and Shiny

Dashboards allow to communicate large amounts of information visually and quickly, and are essential tools to support data-driven decision making. In Chapter 12 we introduced the R package flexdashboard (Iannone, Allaire, and Borges 2020) which can be used to create dashboards that contain several related data visualizations. We also showed an example on how to build a dashboard to visualize global air pollution by means of a map, a table and a histogram.

In some situations, we may want to build dashboards that enable users to change options and see the updated results immediately. For example, we may want to build a dashboard showing results in a map and a table, and include a slider that the user can modify to filter the map areas and the table rows that contain the values in the range of values specified in the slider. We can add this functionality in a dashboard by combining flexdashboard with Shiny. Briefly, this is done by adding runtime: shiny to the YAML header of the R Markdown document, and then adding inputs that the user can modify (e.g., sliders, checkboxes), and outputs (e.g., maps, tables, plots) and reactive expressions that dynamically drive the components within the dashboard. Note that standard flexdashboards are stand-alone documents that can be easily shared with others. However, by adding Shiny to flexdashboard we create interactive documents that need to be deployed to a server to be shared broadly. Further details about how to use Shiny with flexdashboard can be seen in the flexdashboard website.

We can also create dashboards with Shiny by using the shinydashboard package (Chang and Borges Ribeiro 2021). This package provides a number of color themes that make it easy to create dashboards with an attractive appearance. Information about shinydashboard can be seen on the shinydashboard website.

14.1 An interactive dashboard to visualize global air pollution

Here we create an interactive dashboard with flexdashboard and Shiny by modifying the dashboard we created in Chapter 12 showing global air pollution. This dashboard has a slider with the PM\(_{2.5}\) values that the user can modify to filter the countries that he or she wants to inspect. When the slider is changed, the dashboard visualizations update and show the data corresponding to the countries that have PM\(_{2.5}\) values in the range of values specified in the slider. A snapshot of the interactive dashboard created is shown in Figure 14.1.

Snapshot of the interactive dashboard to visualize air pollution data.

FIGURE 14.1: Snapshot of the interactive dashboard to visualize air pollution data.

To build this dashboard, we add runtime: shiny to the YAML header.

---
title: "Air pollution, PM2.5 mean annual exposure
  (micrograms per cubic meter), 2016.
  Source: World Bank https://data.worldbank.org"
output: flexdashboard::flex_dashboard
runtime: shiny
---

Then, we add a column on the left-hand side of the dashboard where we add the slider to filter the countries that are shown in the visualizations. In this column we add the .{sidebar} attribute to indicate that the column appears on the left and has a special background color. The default width of a {.sidebar} column is 250 pixels. Here we modify the width of this column and the other two columns of the dashboard as follows. We use {.sidebar data-width=200} for the sidebar column, {data-width=500} for the column that contains the map, and {data-width=300} for the column that contains the table and the histogram.

Column {.sidebar data-width=200}
-------------------------------------

Then, we add the slider using the sliderInput() function with inputId equal to "rangevalues" and label (text that appears next to the slider) equal to "PM2.5 values:". Then we calculate the variables minvalue and maxvalue as the minimum and maximum integers of the PM\(_{2.5}\) values in the data. After that, we indicate that the slider minimum and maximum values (min and max) are equal to the the minimum and maximum values of the PM\(_{2.5}\) values in the data (minvalue and maxvalue). Finally, we set value = c(minvalue, maxvalue) so initially the slider values are in the range minvalue to maxvalue.

Column {.sidebar data-width=200}
-------------------------------------

```{r}
minvalue <- floor(min(map$PM2.5, na.rm = TRUE))
maxvalue <- ceiling(max(map$PM2.5, na.rm = TRUE))

sliderInput("rangevalues", label = "PM2.5 values:",
            min = minvalue, max = maxvalue,
            value = c(minvalue, maxvalue))

```

Then we modify the code that creates the map, the table and the histogram so they show the data corresponding to the countries with PM\(_{2.5}\) values in the range of values selected in the slider. First we calculate a vector rowsinrangeslider with the indices of the rows of the map that are in the range of values specified in the slider. That is, between input$rangevalues[1] and input$rangevalues[2]. Then we use a reactive expression to create the object mapFiltered that is equal to the subset of rows of map corresponding to rowsinrangeslider.

```{r}
mapFiltered <- reactive({
rowsinrangeslider <- which(map$PM2.5 >= input$rangevalues[1] &
                           map$PM2.5 <= input$rangevalues[2])
map[rowsinrangeslider, ]})
```

After that, we create the visualizations using mapFiltered() instead of map, and using render*() functions to be able to access the mapFiltered() object calculated in the reactive expression. Specifically, we enclose the map with renderLeaflet({}), the table with renderDT({}), and the histogram with renderPlot({}) so they are interactive.

Finally, to avoid the error that appears when the leaflet map is rendered with mapFiltered() that does not contain any country, we check the number of rows before rendering the map. If the number of rows of mapFiltered() is equal to 0 the execution stops returning NULL.

```{r}
if(nrow(mapFiltered()) == 0){
  return(NULL)
}
```

The complete code to build this interactive dashboard to global air pollution is shown below.

---
title: "Air pollution, PM2.5 mean annual exposure
  (micrograms per cubic meter), 2016.
  Source: World Bank https://data.worldbank.org"
output: flexdashboard::flex_dashboard
runtime: shiny
---


```{r}
library(rnaturalearth)
library(wbstats)
library(leaflet)
library(DT)
library(ggplot2)

map <- ne_countries()
names(map)[names(map) == "iso_a3"] <- "ISO3"
names(map)[names(map) == "name"] <- "NAME"

d <- wb(indicator = "EN.ATM.PM25.MC.M3",
        startdate = 2016, enddate = 2016)

map$PM2.5 <- d[match(map$ISO3, d$iso3), "value"]
```


Column {.sidebar data-width=200}
-------------------------------------

```{r}
minvalue <- floor(min(map$PM2.5, na.rm = TRUE))
maxvalue <- ceiling(max(map$PM2.5, na.rm = TRUE))

sliderInput("rangevalues",
  label = "PM2.5 values:",
  min = minvalue, max = maxvalue,
  value = c(minvalue, maxvalue)
)
           
```


  
  
Column {data-width=500}
-------------------------------------

### Map


```{r}
pal <- colorBin(
  palette = "viridis", domain = map$PM2.5,
  bins = seq(0, max(map$PM2.5, na.rm = TRUE) + 10, by = 10)
)


map$labels <- paste0(
  "<strong> Country: </strong> ",
  map$NAME, "<br/> ",
  "<strong> PM2.5: </strong> ",
  map$PM2.5, "<br/> "
) %>%
  lapply(htmltools::HTML)


mapFiltered <- reactive({
  rowsinrangeslider <- which(map$PM2.5 >= input$rangevalues[1] &
    map$PM2.5 <= input$rangevalues[2])
  map[rowsinrangeslider, ]
})

renderLeaflet({
  if (nrow(mapFiltered()) == 0) {
    return(NULL)
  }

  leaflet(mapFiltered()) %>%
    addTiles() %>%
    setView(lng = 0, lat = 30, zoom = 2) %>%
    addPolygons(
      fillColor = ~ pal(PM2.5),
      color = "white",
      fillOpacity = 0.7,
      label = ~labels,
      highlight = highlightOptions(
        color = "black",
        bringToFront = TRUE
      )
    ) %>%
    leaflet::addLegend(
      pal = pal, values = ~PM2.5,
      opacity = 0.7, title = "PM2.5"
    )
})
```
   

Column {data-width=300}
-------------------------------------

### Table


```{r}
renderDT({
  DT::datatable(mapFiltered()@data[, c("ISO3", "NAME", "PM2.5")],
    rownames = FALSE, options = list(pageLength = 10)
  )
})
```   

### Histogram


```{r}
renderPlot({
  ggplot(data = mapFiltered()@data, aes(x = PM2.5)) +
    geom_histogram()
})
```