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 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.
Below is code for a
masterpiece that that does some
hide()
and
show()
:
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.
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() +
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({
req(input$plot_brush)
brushed = brushedPoints(dat(), input$plot_brush)
ggplot(brushed, aes(year, prop, col=sex)) +
geom_line() +
theme_minimal() +
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(
plotOutput("plot", height = "100%", brush = "brush")
)
)
server = function(input, output, session) {
output$plot = renderPlot({
ggplot(data, aes_string(xvar, yvar)) + geom_point()
})
observeEvent(input$done, {
data$id = 1:nrow(data)
p = brushedPoints(data, input$brush)
g = which(p$id %in% data$id)
stopApp(data[-g,])
})
}
runGadget(ui, server)
}
library(gapminder)
outlier_rm(gapminder, "lifeExp", "gdpPercap")
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:
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.