= bslib::bs_theme(bootswatch = "superhero") theme
6 simple Shiny things I have learned from creating a somewhat small app
A couple of weeks back, I wanted to explain to my student what I mean when I talk about the “variance of the sample variance”. I know this term sounds quite confusing. It contains the word “variance” at least one too many times.
But I was not sure whether my subsequent explanation really came through. Thus, I decided to let my students explore the notion on their own through a Shiny app.
Honestly, I thought this would be quite easy to build. I’ve already learned the basics of Shiny and the app was supposed to be simple. However, I soon came to realize that I would need to level up my Shiny skills.
As is usual with coding, I did this mostly by strolling through the web in order to find helpful code snippets. Most of the time, I consulted Hadley Wickham’s Mastering Shiny book. Still, I ended up searching for a lot of random Shiny stuff on the web.
So, here is a compilation of loosely connected troubles I solved during my Shiny learning process. May this summary serve you well.
Use a theme for simple customization
Let’s start with something super easy. If you wish to customize the appearance of you app, you can set the theme
argument of fluidPage()
to either a CSS-file that contains the necessary configuration (this is the hard way) or use a theme from bslib::bs_theme()
.
The latter approach comes with a lot of named pre-implemented themes and is easily implemented by bootswatch = "name"
. In my app, I have simply added
For other themes, have a look at RStudio’s Shiny themes page.
Check out this super simple example that I have adapted from the default “new Shiny app” output (you will actually have to copy and run this in an R script on your own).
library(shiny)
library(tidyverse)
<- fluidPage(
ui # Theme added here
theme = bslib::bs_theme(bootswatch = "superhero"),
titlePanel("Old Faithful Geyser Data"),
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30)
),
mainPanel(
plotOutput("distPlot")
)
)
)
<- function(input, output) {
server $distPlot <- renderPlot({
output<- faithful[, 2]
x <- seq(min(x), max(x), length.out = input$bins + 1)
bins hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
}
shinyApp(ui = ui, server = server)
During the course of this text, we will extend this small example bit by bit. But, I want to avoid copy-and-pasting code each time we change something. Thus, I will only describe the changes to the previous version instead of pasting the whole code. Nevertheless, I will provide links after each example so that each script can be downloaded at will. The current example can be found here.
Isolate slider from reactivity
As is currently intended, our app’s histogram changes whenever the slider is moved. Sometimes, though, this is not what we wish to do. Instead, we may want to delay the rendering of the plot until a button is clicked.
This can be achieved through a simple isolate()
command which, well, isolates whatever is in between the function’s parentheses from changes on the UI. Here, let us put input$bins
into the isolate()
function and check what happens when we move the slider (full code here), i.e. we changed
<- seq(min(x), max(x), length.out = isolate(input$bins) + 1) bins
Excellent! Nothing happens when we move the slider. Dumb and useless but excellent anyway.
Observe that we could have also put the whole renderPlot()
function call into isolate()
. This app would work in the sense that we created valid code but then the reactivity of the slider is still active. The isolate()
documentation hints at this with “…if you assign a variable inside the isolate(), its value will be visible outside of the isolate()
”.
Use eventReactive() as an alternative for updating values
Honestly, this part I learned just 5 minutes ago while I was writing the last section of this blog post. When I looked into the documentation of observeEvent()
, I noticed that there is also a function eventReactive()
which may be better suited for our current use case. It allows us to avoid manually isolating input$bins
.
This new function works similar to observeEvent()
but it creates a reactive variable instead. This, we can use for rendering. Check this out
<- eventReactive(
plot $draw_button, {
input<- faithful[, 2]
x <- seq(min(x), max(x), length.out = input$bins + 1)
bins hist(x, breaks = bins, col = 'darkgray', border = 'white')
}
)
$distPlot <- renderPlot({plot()}) output
Notice how we do not use isolate()
anymore and use the plot
variable like a reactive in renderPlot()
, i.e. we have to “call” its value with ()
.
However, be aware that eventReactive()
creates a reactive variable such that you cannot change, say, multiple plots at once. Nevertheless, eventReactive()
can be a great way to tie a plot to an event. So, I guess it dependes on your use case and personal preference if you want to use eventReactive()
rather than observeEvent()
. Anyway, this version’s code can be copied from here.
Use reactiveVal() to manually change values on click
Another neat function is reactiveVal()
which helps you to construct for instance counters that increase on the click of a button. We can initialize a reactive value by writing
<- reactiveVal(value = 0) counter
within the server function. This way, our counter is set to zero and we can update it and set it to, say, one by calling counter(value = 1)
. The current value of the counter can be accessed through counter()
.
Clearly, we can tie the update of a reactive value to an event that we observe through observeEvent()
. For instance, we could count how often the draw button is clicked by changing our previous observeEvent(input$draw_button, ...)
. Here, we would change this particular line of code to
observeEvent(
$draw_button, {
input<- counter()
tmp counter(tmp + 1)
$distPlot <- renderPlot({
output<- faithful[, 2]
x <- seq(min(x), max(x), length.out = isolate(input$bins) + 1)
bins hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
} )
Finally, we can show this information on our UI for demo purposes by adding a textOutput("demonstration_text")
to our UI and setting
$demonstration_text <- renderText(paste(
output"You have clicked the draw button",
counter(),
"times. Congrats!"
))
The complete app can be found here.
Use tabsetPanel and unique plot names
Often, you do not want to display all information at once. In my particular case, I wanted to show only one out of two plots based on the user’s chosen estimator (sample mean or sample variance). A great way to achieve that is to use tabsetPanel()
in the UI.
Ordinarily, you can create a UI this way by setting
mainPanel(
tabsetPanel(
tabPanel("Plot", plotOutput("plot")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Table", tableOutput("table"))
) )
This was an example taken straight out of the documentation of tabsetPanel()
. What you will get if you start an app containing a UI like this is a panel with three tabs (each one corresponding to a plot, text or table output). Unsurprisingly, the user can click on the tabs to switch between the views.
However, we can also add an id
to this and set type
to hidden
, like so
mainPanel(
tabsetPanel(
id = "my_tabs",
type = "hidden",
tabPanel("Plot", plotOutput("plot")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Table", tableOutput("table"))
) )
Then, the user does not have the option to change between views by clicking on tabs. Now, the view has to change based on other interactions of the user with the UI.
That’s the part we have to code within the server function. And this is where the id
argument comes into play. It allows us to address the tabs via updateTabsetPanel()
.
Here, let us take our previous example and display the same information on a different panel. At the end, we will have two panels with exactly the same information in each tab. I know. This is not particularly exciting or meaningful but it serves our current purpose well.
Naively, we might implement our user-interface like so
mainPanel(
tabsetPanel(
id = "my_tabs",
type = "hidden",
tabPanel("panel1", {
# UI commands from before here
}),tabPanel("panel2", {
# UI commands from before here
}),
) )
However, we will have to be careful! If we simply copy-and-paste our UI from before, then we won’t have unique identifiers to address e.g. the draw button or the plot output.
This is a serious NO-NO (all caps for dramatic effect) and the app won’t work properly. Instead, let us write a function that draws the UI for us but creates it with different identifiers like this
<- function(unique_part) {
create_UI sidebarLayout(
sidebarPanel(
# unique label here by adding unique_part to bins
sliderInput(paste("bins", unique_part, sep = "_"),
"Number of bins:",
min = 1,
max = 50,
value = 30),
actionButton(paste("draw_button", unique_part, sep = "_"), "Reevaluate!", width = "100%"),
actionButton(paste("change_view", unique_part, sep = "_"), "Change view", width = "100%")
),
mainPanel(
textOutput(paste("demonstration_text", unique_part, sep = "_")), # Counter text added
textOutput(paste("countEvaluations", unique_part, sep = "_")),
plotOutput(paste("distPlot", unique_part, sep = "_"))
)
) }
Also, notice that I have created another button called “Change view” within the UI. Further, this button’s name is so mind-baffling that I won’t even try to elaborate what it will do. Finally, using create_UI
, we can set up the UI like so
mainPanel(
tabsetPanel(
id = "my_tabs",
selected = "panel1",
type = "hidden",
tabPanel("panel1", create_UI("panel1")),
tabPanel("panel2", create_UI("panel2")),
) )
This will address everything within the UI in a unique manner. Of course, such a functional approach only works well if the two panels look sufficiently similar such that it makes sense to design them through a single function. In my “variance of estimators” app, this was the case because the tabs for the sample mean and sample variance were quite similar in their structure.
Now that we have covered how the UI needs to be set up, let me show you how to change the view from one panel to the next. Shockingly, let us link this to a click on the “change view” button(s) like so
observeEvent(
$change_view_panel1,
inputupdateTabsetPanel(inputId = "my_tabs", selected = "panel2")
)observeEvent(
$change_view_panel2,
inputupdateTabsetPanel(inputId = "my_tabs", selected = "panel1")
)
Also, note that the previous code
observeEvent(
$draw_button, {
input<- counter()
tmp counter(tmp + 1)
$distPlot <- renderPlot({
output<- faithful[, 2]
x <- seq(min(x), max(x), length.out = isolate(input$bins) + 1)
bins hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
} )
won’t work anymore because the old identifiers like draw_button
etc. need to be updated to draw_button_panel1
or draw_button_panel2
. Clearly, this could potentially require some code duplication to implement the server-side logic for both tabs. But since we feel particularly clever today1, let us write another function that avoids a lot of code duplication.
<- function(panel, counter, input, output) {
render_my_plot <- counter() # save current value of counter
tmp counter(tmp + 1) # update counter
# Create identifier names
<- paste("bins", panel, sep = "_")
bins_name <- paste("distPlot", panel, sep = "_")
distplot_name <- paste("demonstration_text", panel, sep = "_")
demonstration_text
# Render Plot
<- renderPlot({
output[[distplot_name]] <- faithful[, 2]
x <- seq(min(x), max(x), length.out = isolate(pluck(input, bins_name)) + 1)
bins hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
# Render counter text
<- renderText(paste(
output[[demonstration_text]] "You have clicked the draw button",
counter(),
"times. Congrats!"
)) }
Notice a few things here:
- Our function needs to know the objects
counter
,input
andoutput
to work. - Also we need to switch to double-bracket notation for assigning new variables like
distPlot_panel1
tooutput
. Obviously, we couldn’t use$
for assignment anymore but single-bracket notation likeoutput[var_name]
is for some reason forbidden in Shiny. At least, that’s what an error message will kindly tell you when you dare to use only one bracket.
All in all, our server-side logic looks like this now
<- function(input, output) {
server # Counter initialization
<- reactiveVal(value = 0)
counter <- reactiveVal(value = 0)
counter2
# Plot Rendering
observeEvent(
$draw_button_panel1, {
inputrender_my_plot("panel1", counter, input, output)
}
)observeEvent(
$draw_button_panel2, {
inputrender_my_plot("panel2", counter2, input, output)
}
)
# Panel Switching
observeEvent(
$change_view_panel1,
inputupdateTabsetPanel(inputId = "my_tabs", selected = "panel2")
)observeEvent(
$change_view_panel2,
inputupdateTabsetPanel(inputId = "my_tabs", selected = "panel1")
) }
The complete app can be found here.
Closing
Alright, I hope this helps you to build your own small Shiny app. In my particular case, I had to use another cool function from the shinyjs
package . It helped me to update the text on the UI such that it appears in red for a second (so that the user notices what changes).
I have the feeling that shinyjs
has way more in store for us. But this post is already quite long. So, let me save that (exciting) story for another time. Hope you will be there next time.
Footnotes
And with that I really mean today. When I built my Shiny app, I actually used code duplication. But in hindsight, I feel somewhat embarrassed to leave it as it is for this blog post. Thus, I figured out how to make it work with a function.
Second Update (September 2022): All of this can be done with Shiny modules. Took me a while to learn that. Now I’m embarrassed for my original “smart” approach. I guess you never stop learning something new.↩︎