4 Dynamic interaction
Next we make things dynamic, which means we enter the world of Shiny2. There is a nice overview of how flexdashboard interacts with Shiny. The jump here is that we go from being able to hover over and drag around our images, to allowing users to interact with the data. To do that we need three ingredients:
- Input / user-interface
- Output / back-end
- runtime: shiny in the YAML
Generally you will have to iterate between creating the input and output to get the result you want. A good place to start is usually by imagining one way that you want users to be able to interface with the data. This can take lots of forms. You may want to filter the data based on different attributes, lets users decide different grouping or variables, or alter color schemes.
4.1 Filtering by attributes
First, we are going to focus on interactivity that let’s users filter the data, meaning, reduce it to the variables that they are interested in. For this example, we’ll focus on just the baseline plot we’re working with – the relationship between bill length and bill depth – but focus on filtering the data by several different variables, such as species, island, and year. So we’ll keep our base plot with only 2 dimensions, but alter the figure based on those three variables. As an end-result, users will be able to make 64 different plots based on all combinations of species, islands, and years.
4.1.1 Input
The input focuses on the user-interface (UI) side – what the user toggles in order to change their view. There are different kinds of inputs that can give you drop-down menus, sliders, text box entries, etc. 3
Input function | Input type |
---|---|
selectInput | A box with choices to select from |
sliderInput | A slider bar |
radioButtons | A set of radio buttons |
textInput | A field to enter text |
numericInput | A field to enter numbers |
checkboxInput | A single check box |
dateInput | A calendar to aid date selection |
dateRangeInput | A pair of calendars for selecting a date range |
fileInput | A file upload control wizard |
So these are functions that we can put into an R chunk, and then fill out the arguments accordingly. You can look at the arguments like this:
Generally, the arguments are the inputID
, or the name that will be assigned to a column in a new data frame called input. Then the label
, which is what users will see next to the input, choices
, which is what users will see with their input selection, and selected
can be set to have a default value selected from your list of choices
. There are others, but we’ll stick with these for now.
So we will add this code chunk into our sidebar column, and first make an input selector for the species of penguins. We provide the choices of “All”, or the unique names of all the penguins in the data frame, and select “All” as the default. Note that we also need to load in the shiny
library because the input functions are from Shiny.
---
title: 'Palmer Penguin Dashboard'
output:
flexdashboard::flex_dashboard:
---
```{r, echo = F, warning = F, message = F}
library(tidyverse)
library(palmerpenguins)
library(plotly)
library(shiny)
```
## Column {.sidebar}
```{r}
selectInput("species", "Species",
choices=c("All",
unique(as.character(penguins$species))),
selected="All", multiple=F)
```
## Column
### Relationship between penguin bill length and depth
```{r}
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(size = 3, color = "#51127C") +
theme_minimal() +
labs(x = "Length (mm)", y = "Depth (mm)")
```
If we add in the selectInput
function and Knit, we can see that we now have a selector on the sidebar. As a user, you can switch it around. BUT it doesn’t work yet because we need to connect it to the back end.
4.1.2 Output
To connect to the output figure to the input, we need to do 2 things:
1. Prepare the data
2. Wrap our plot in the right render
function
Preparing the data is probably the way of thinking that is most important to creating interactive data. The idea is to create a reactive function that filters the data reactive to the input.
So a first note is about what actually happens when we use the input functions. These functions create a data frame called input (under the hood), with a column for each input selected, named after the inputID
. Essentially, in our example already we’ve created this:
## species
## 1 All
## 2 Adelie
## 3 Gentoo
## 4 Chinstrap
With that in mind, we need to use that input data frame to identify our “filtered data”. We can call this whatever we want, but I think based on how we’re setting up this page, we can call it “filtered data.”
Anyway, the idea is that you are writing a function that will create a new data frame every time the selector inputs are changed. That new data frame will represent the conditions set by your inputs. This is the hardest step because it forces you to think about the conditionality and shape of your data.
In our example, we will call our function filteredData
(though we could call this anything), and we will create it using the reactive
function. Inside this reactive function, using curly brackets, we will define the conditions based on which we filter our data. You can use any conditional you want: if...else
, ifelse()
, case_when()
. Here I use if...else
because I think it is easier to read.
```{r}
filteredData <- reactive({
if(input$species == "All"){
penguins
} else {
penguins %>% filter(species == input$species)
}
})
```
So now we have this reactive function called filteredData()
.
The second part of this, wrapping our figure in the correct function, is easier. We just change two things: 1. We need to replace our static data with the reactive data function, which we called filteredData
. And then 2. We need to wrap the whole plot in a function called renderPlot()
.
4.1.3 runtime: shiny
The third and final ingredient to turn your dashboard into something that is dynamically interactive is to integrate Shiny. We do this by adding one line to our YAML and making sure we load the shiny
package into R.
In the end, your Rmd file should look like this:
---
title: 'Palmer Penguin Dashboard'
runtime: shiny
output:
flexdashboard::flex_dashboard:
---
```{r, echo = F, warning = F, message = F}
library(tidyverse)
library(palmerpenguins)
library(plotly)
library(shiny)
```
## Column {.sidebar}
```{r}
selectInput("species", "Species",
choices=c("All",
unique(as.character(penguins$species))),
selected="All", multiple=F)
```
```{r}
filteredData <- reactive({
if(input$species == "All"){
penguins
} else {
penguins %>% filter(species == input$species)
}
})
```
## Column
### Relationship between penguin bill length and depth
```{r}
renderPlot({
ggplot(filteredData(), aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(size = 3, color = "#51127C") +
theme_minimal() +
labs(x = "Length (mm)", y = "Depth (mm)")
})
```
If you make this final change and click save, you’ll notice that the Knit button disappears and is replaced with a Run document
button. This is your Rmd becoming shinier. If you click run, take a look. You should now have a reactive page!
Now, to make sure we understand the logic of aligning inputs and outputs, let’s try adding a few more variables to the mix. We initially wanted users to be able to make one of 64 plots, based on species, island, and year. So let’s add those other two selectors, and we can use different types just to mix it up.
```{r}
selectInput("species", "Species",
choices=c("All",
unique(as.character(penguins$species))),
selected="All", multiple=F)
radioButtons("island", "Island",
choices=c("All",
unique(as.character(penguins$island))),
selected="All")
sliderInput("year", "Year",
min = min(penguins$year),
max = max(penguins$year),
value = median(penguins$year),
step = 1)
```
Now, whenever we add more inputs, we have to update our data filtering function to react to them. Now that we have several inputs we need several conditions. I like to create intermediate versions of the filtered data, which I signify by appening the letter of the input in each condition.
```{r}
filteredData <- reactive({
if(input$species == "All"){
penguins_s <- penguins
} else {
penguins_s <- penguins %>% filter(species == input$species)
}
if(input$island == "All"){
penguins_si <- penguins_s
} else {
penguins_si <- penguins_s %>% filter(island == input$island)
}
penguins_siy <- filter(penguins_si, year == input$year)
return(penguins_siy)
})
```
Now because all of this is changing the data that feeds into the figure, but not any parameters of the figure itself, we don’t need to change the output portion. Everything that get’s added is in the input/UI side and the backend of filtering the data.
4.2 Selecting by different columns
Now, let’s think of another user scenario. Let’s say we didn’t want to filter/reduce the data, but instead, we wanted to color the points of the plot by different variables. In this scenario, we don’t need to change the shape of the data at all, just make one of the arguments in the figure creation reactive to the inputs.
So, let’s think about this first on the input side. Let’s create a drop down of variables that we want to color the nodes by.
```{r}
selectInput("fill_var", "Color points by...",
choices=c("None",
"species",
"island",
"year"),
selected="None", multiple=F)
```
Now, think about what our under-the-food input data from looks like. It will only have one column: input$fill_var. This input value is going to specify if we want to color by a given variable, and if so, which column. So instead of making the data filtering conditional on our input, we can make the plot conditional on the input.
Here we can specify, if the fill variable input is selected to “None”, just make the plot with no fill. But otherwise, inser input$fill_var as the fill.
```{r}
renderPlot({
if(input$fill_var == "None"){
# Make the same plot as before
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(size = 3) +
theme_minimal() +
labs(x = "Length (mm)", y = "Depth (mm)")
} else {
# Add in the input, make sure to evaluate the parsed text so that R
# thinks of it as a variable and not just text
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm,
color = eval(parse(text = input$fill_var)))) +
geom_point(size = 3) +
theme_minimal() +
labs(x = "Length (mm)", y = "Depth (mm)", color = "") +
scale_color_viridis_d(end = .8)
}
})
```
BUT, when we try this we get a frustrating result. It is evaluating input$fill_var as text, not as a column name in the data frame. How do we get around this? We have to be very specific with R, telling it to evaluate the text as an object. We don’t get too much into this, but we will do it using these two functions:
eval(parse(text = input$fill_var))
So when we add that in, we get what we want:
```{r}
renderPlot({
if(input$fill_var == "None"){
# Make the same plot as before
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point() +
theme_minimal() +
labs(x = "Length (mm)", y = "Depth (mm)")
} else {
# Add in the input, make sure to evaluate the parsed text so that R
# thinks of it as a variable and not just text
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm,
color = eval(parse(text = input$fill_var)))) +
geom_point() +
theme_minimal() +
labs(x = "Length (mm)", y = "Depth (mm)", color = "")
}
})
```
Note there are several different ways to play around with the reactive()
function that could perhaps be used here, so I am demonstrating just one way to work with this.