This is a guest post from VP Nagraj, a data scientist embedded within UVA’s Health Sciences Library, who runs our Data Analysis Support Hub (DASH) service.
Last weekend I was fortunate enough to be able to participate in the first ever Shiny Developer Conference hosted by RStudio at Stanford University. I’ve built a handful of apps, and have taught an introductory workshop on Shiny. In spite of that, almost all of the presentations de-mystified at least one aspect of the how, why or so what of the framework. Here’s a recap of what resonated with me, as well as some code and links out to my attempts to put what I digested into practice.
tl;dr
- reactivity is a beast
- javascript isn’t cheating
- there are already a ton of shiny features … and more on the way
reactivity
For me, understanding reactivity has been one of the biggest challenges to using Shiny … or at least to using Shiny well. But after > 3 hours of an extensive (and really clear) presentation by Joe Cheng, I think I’m finally starting to see what I’ve been missing. Here’s something in particular that stuck out to me:
output$plot = renderPlot()
is not an imperative to the browser to do a what … it’s a recipe for how the browser should do something.
Shiny ‘render’ functions (e.g.
renderPlot()
, renderText()
, etc) inherently depend on reactivity. What the point above emphasizes is that assignments to a reactive expression are not the same as assignments made in “regular” R programming. Reactive outputs depend on inputs, and subsequently change as those inputs are manipulated.
If you want to watch how those changes happen in your own app, try adding
options(shiny.reactlog=TRUE)
to the top of your server script. When you run the app in a browser and press COMMAND + F3
(or CTRL + F3
on Windows) you’ll see a force directed network that outlines the connections between inputs and outputs.
Another way to implement reactivity is with the
For my apps, one of the pitfalls has been re-running the same code multiple times. That’s a perfect use-case for reactivity outside of the render functions.
reactive()
function.For my apps, one of the pitfalls has been re-running the same code multiple times. That’s a perfect use-case for reactivity outside of the render functions.
Here’s a trivial example:
library(shiny)
ui = fluidPage(
numericInput("threshold", "mpg threshold", value = 20),
plotOutput("size"),
textOutput("names")
)
server = function(input, output) {
output$size = renderPlot({
dat = subset(mtcars, mpg > input$threshold)
hist(dat$wt)
})
output$names = renderText({
dat = subset(mtcars, mpg > input$threshold)
rownames(dat)
})
}
shinyApp(ui = ui, server = server)
The code above works … but it’s redundant. There’s no need to calculate the “dat” object separately in each render function.
The code below does the same thing but stores “dat” in a reactive that is only calculated once.
library(shiny)
ui = fluidPage(
numericInput("threshold", "mpg threshold", value = 20),
plotOutput("size"),
textOutput("names")
)
server = function(input, output) {
dat = reactive({
subset(mtcars, mpg > input$threshold)
})
output$size = renderPlot({
hist(dat()$wt)
})
output$names = renderText({
rownames(dat())
})
}
shinyApp(ui = ui, server = server)
javascript
For whatever reason I’ve been stuck on the idea that using JavaScript inside a Shiny app would be “cheating”. But Shiny is actually well equipped for extensions with JavaScript libraries. Several of the speakers leaned in on this idea. Yihui Xie presented on the DT package, which is an interface to use features like client-side filtering from the DataTables library. And Dean Attali demonstrated shinyjs, a package that makes it really easy to incorporate JavaScript operations.
# https://apps.bioconnector.virginia.edu/game
library(shiny)
library(shinyjs)
shinyApp(
ui = fluidPage(
titlePanel(actionButton("start", "start the game")),
useShinyjs(),
hidden(actionButton("restart", "restart the game")),
tags$h3(hidden(textOutput("game_over")))
),
server = function(input, output) {
output$game_over =
renderText({
"game over, man ... game over"
})
observeEvent(input$start, {
show("game_over", anim = TRUE, animType = "fade")
hide("start")
show("restart")
})
observeEvent(input$restart, {
hide("game_over")
hide("restart")
show("start")
})
}
)
everything else
brushing
Adding a brush argument to
plotOutput()
let’s you click and drag to select a points on a plot. You can use this for “zooming in” on something like a time series plot. Here’s the code for an app I wrote based on data from the babynames package - in this case the brush let’s you zoom to see name frequency over specific range of years.# http://apps.bioconnector.virginia.edu/names/
library(shiny)
library(ggplot2)
library(ggthemes)
library(babynames)
library(scales)
options(scipen=999)
ui = fluidPage(titlePanel(title = "names (1880-2012)"),
textInput("name", "enter a name"),
actionButton("go", "search"),
plotOutput("plot1", brush = "plot_brush"),
plotOutput("plot2"),
htmlOutput("info")
)
server = function(input, output) {
dat = eventReactive(input$go, {
subset(babynames, tolower(name) == tolower(input$name))
})
output$plot1 = renderPlot({
ggplot(dat(), aes(year, prop, col=sex)) +
geom_line() +
xlim(1880,2012) +
theme_minimal() +
# format labels with percent function from scales package
scale_y_continuous(labels = percent) +
labs(list(title ="% of individuals born with name by year and gender",
x = "\n click-and-drag over the plot to 'zoom'",
y = ""))
})
output$plot2 = renderPlot({
# need latest version of shiny to use req() function
req(input$plot_brush)
brushed = brushedPoints(dat(), input$plot_brush)
ggplot(brushed, aes(year, prop, col=sex)) +
geom_line() +
theme_minimal() +
# format labels with percent function from scales package
scale_y_continuous(labels = percent) +
labs(list(title ="% of individuals born with name by year and gender",
x = "",
y = ""))
})
output$info = renderText({
"data source: social security administration names from babynames package
"
})
}
shinyApp(ui, server)
gadgets
A relatively easy way to leverage Shiny reactivity for visual inspection and interaction with data within RStudio. The main difference here is that you’re using an abbreviated (or ‘mini’) ui. The advantage of this workflow is that you can include it in your script to make your analysis interactive. I modified the example in the documentation and wrote a basic brushing gadget that removes outliers:
library(shiny)
library(miniUI)
library(ggplot2)
outlier_rm = function(data, xvar, yvar) {
ui = miniPage(
gadgetTitleBar("Drag to select points"),
miniContentPanel(
# The brush="brush" argument means we can listen for
# brush events on the plot using input$brush.
plotOutput("plot", height = "100%", brush = "brush")
)
)
server = function(input, output, session) {
# Render the plot
output$plot = renderPlot({
# Plot the data with x/y vars indicated by the caller.
ggplot(data, aes_string(xvar, yvar)) + geom_point()
})
# Handle the Done button being pressed.
observeEvent(input$done, {
# create id for data
data$id = 1:nrow(data)
# Return the brushed points. See ?shiny::brushedPoints.
p = brushedPoints(data, input$brush)
# create vector of ids that match brushed points and data
g = which(p$id %in% data$id)
# return a subset of the original data without brushed points
stopApp(data[-g,])
})
}
runGadget(ui, server)
}
# run to open plot viewer
# click and drag to brush
# press done return a subset of the original data without brushed points
library(gapminder)
outlier_rm(gapminder, "lifeExp", "gdpPercap")
# you can also use the same method above but pass the output into a dplyr pipe syntax
# without the selection what is the mean life expectancy by country?
library(dplyr)
outlier_rm(gapminder, "lifeExp", "gdpPercap") %>%
group_by(country) %>%
summarise(mean(lifeExp))
req()
This solves the issue of requiring an input - I’m definitely going to use this so I don’t have to do the
return(NULL)
work around:# no need to do do this any more
#
# inFile = input$file1
#
# if (is.null(inFile))
# return(NULL)
# use req() instead
req(input$file1)
profvis
Super helpful method for digging into the call stack of your R code to see how you might optimize it.
One or two seconds of processing can make a big difference, particularly for a Shiny app …
rstudio connect
Jeff Allen from RStudio gave a talk on deployment options for Shiny applications and mentioned this product, which is a “coming soon” platform for hosting apps alongside RMarkdown documents and plots. It’s not available as a full release yet, but there is a beta version for testing.