Update UI prior to ggvis plot in R Shiny - r

Background: I'm building a dashboard that interfaces with a MySQL database. The user specifies a coarse filter to pull data from the database and clicks "Submit", the data are plotted with ggvis, then the user is able to play with fine filters to affect what subset of data are plotted. These fine filters depend on the data pulled from the database, therefore I generate them from the data using uiOutput/renderUI.
Problem: My challenge is that I want the UI to be updated based on the data before the plot is updated. Otherwise the fine filters from the old dataset are applied to the new data, which results in an error when plotting.
Example: The following example roughly reproduces the problem using mtcars. To get the error, select 4 cylinders, click "Submit", then select 6 cylinders and click "Submit" again. In this case, when the 4 cylinder fine filter is applied to the 6 cylinder dataset only a single point is returned, which causes an error when trying to apply a smoother in ggvis. Not the same error as I'm getting, but close enough.
library(shiny)
library(dplyr)
library(ggvis)
ui <- fluidPage(
headerPanel("Example"),
sidebarPanel(
h2("Course Filter:"),
selectInput("cyl_input", "Cylinders", c(4, 6)),
actionButton("submit", "Submit"),
conditionalPanel(condition = "input.submit > 0",
h2("Fine Filter: "),
uiOutput("mpg_input")
)
),
mainPanel(
ggvisOutput("mtcars_plot")
)
)
server <- function(input, output) {
mycars <- eventReactive(input$submit, {
filter(mtcars, cyl == input$cyl_input)
})
output$mpg_input <- renderUI({
mpg_range <- range(mycars()$mpg)
sliderInput("mpg_input", "MPG: ",
min = mpg_range[1], max = mpg_range[2],
value = mpg_range,
step = 0.1)
})
observe({
if (!is.null(input$mpg_input)) {
mycars() %>%
filter(mpg >= input$mpg_input[1],
mpg <= input$mpg_input[2]) %>%
ggvis(~mpg, ~wt) %>%
layer_points() %>%
layer_smooths() %>%
bind_shiny("mtcars_plot")
}
})
}
shinyApp(ui = ui, server = server)

After many hours of messing around, I've found a very hacky workaround. I'm not very satisfied with it, so am hoping someone can offer an improvement.
To summarize, my realization was that the renderUI call was being executed when it was supposed to be, i.e. prior to the plot being generated. However, renderUI doesn't directly change the slider in the UI, rather it sends a message to the browser telling it to update the slider. Such messages are only executed once all observers have been run. In particular, this happens after the observer wrapping the call to ggvis is run. So, the sequence seems to be
Message sent to browser to update slider.
Plot generated based on values in slider, which are still the old values.
Browser updates slider. Sadly too late :(
So, to work around this I decided to create a new reactive variable storing the range of MPG values. Immediately after the coarse filter has been applied, and before the slider is updated in the browser, this variable references the new data frame directly. Afterwards, when playing with the slider directly, this reactive variable references the slider. This just requires setting a flag specifying whether to reference the data frame or the slider, then flipping the flag in a sensible location.
Here's the code:
library(shiny)
library(dplyr)
library(ggvis)
ui <- fluidPage(
headerPanel("Example"),
sidebarPanel(
h2("Course Filter:"),
selectInput("cyl_input", "Cylinders", c(4, 6)),
actionButton("submit", "Submit"),
conditionalPanel(condition = "input.submit > 0",
h2("Fine Filter: "),
uiOutput("mpg_input")
)
),
mainPanel(
ggvisOutput("mtcars_plot")
)
)
server <- function(input, output) {
# create variable to keep track of whether data was just updated
fresh_data <- TRUE
mycars <- eventReactive(input$submit, {
# data have just been refreshed
fresh_data <<- TRUE
filter(mtcars, cyl == input$cyl_input)
})
output$mpg_input <- renderUI({
mpgs <- range(mycars()$mpg)
sliderInput("mpg_input", "MPG: ",
min = mpgs[1], max = mpgs[2],
value = mpgs,
step = 0.1)
})
# make filtering criterion a reactive expression
# required because web page inputs not updated until after everything else
mpg_range <- reactive({
# these next two lines are required though them seem to do nothing
# from what I can tell they ensure that mpg_range depends reactively on
# these variables. Apparently, the reference to these variables in the
# if statement is not enough.
input$mpg_input
mycars()
# if new data have just been pulled reference data frame directly
if (fresh_data) {
mpgs <- range(mycars()$mpg)
# otherwise reference web inputs
} else if (!is.null(input$mpg_input)) {
mpgs <- input$mpg_input
} else {
mpgs <- NULL
}
return(mpgs)
})
observe({
if (!is.null(mpg_range())) {
mycars() %>%
filter(mpg >= mpg_range()[1],
mpg <= mpg_range()[2]) %>%
ggvis(~mpg, ~wt) %>%
layer_points() %>%
layer_smooths() %>%
bind_shiny("mtcars_plot")
}
# ui now updated, data no longer fresh
fresh_data <<- FALSE
})
}
shinyApp(ui = ui, server = server)

Related

Making an Action Button re-read file and recompute in Shiny

I've written a script to calculate glicko ratings and produce odds and historic plots for sport teams. A separate script is responsible for querying the SQL server that holds historic data and extracting the relevant info to make a local tsv file of the info I need for the rating calculation. To make it more user-friendly, I've put the functions into a simple shiny app.
My problem is that I would like to put in a button that automatically executes the code of the second script that adds recent matches to the data file, so the ratings can be updated.
I've proved a simplified example of my code, showing that I'm handling the bulk of the data wrangling and preparation of the ratings object, from where I can get probabilities, before defining the UI. I tried a simple example of modifying the teams_list with my action button, but this did not recalculate the list of teams available to enter in selectInput(); because of how observeEvent() handles the code with isolate() to avoid recalculations, I'm guessing. So simply duplicating all the code that loads data and prepares the ratings object will not do unless it makes all the rest of the code re-evaluate its input.
I considered moving all of that into the action button and deleting it from the start of the script, but that would mean that there is no data at all until the action button would be pressed and that is not desirable either. I don't want to query the database more often than is necessary, so it is a must to be able to run the app from the existing data rather than querying it every time the app is launched.
Does anyone have a suggestion for how this could be accomplished?
### Toy example
## Prep: This input data normally exists before app is run.
library(tidyverse)
tibble(team1 = c("Name1", "Name2", "Name3", "Name2"),
team2 = c("Name2", "Name1", "Name1", "Name3"),
team1Won = c(T, T, F, T)) %>%
write_tsv("example_match_file.tsv")
## Here the app code starts.
# Loading data and calculating team ratings
match_df <- read_tsv("example_match_file.tsv")
rating_calculation <- function(match_data = match_df) {
match_data %>%
group_by(team1) %>%
summarize(matchesWon = sum(team1Won)) %>%
arrange(desc(matchesWon))
}
rating_df <- rating_calculation(match_df)
team_list <- rating_df$team1
odds_calculation <- function(team1, team2, ratingObject = rating_df) {
#Real calculation omitted for brevity
p <- runif(1)
}
## Define Shiny UI
library(shiny)
ui <- fluidPage(
titlePanel("Odds"),
sidebarLayout(
sidebarPanel(
selectInput(inputId = "team1",
label = "Team 1",
choices = team_list),
selectInput(inputId = "team2",
label = "Team 2",
choices = team_list),
#actionButton("update", "Update match data")
),
mainPanel(
tableOutput("odds")
)
)
)
## Define Shiny server logic
server <- function(input, output) {
#Generate Odds
output$odds <- renderTable({
p <- odds_calculation(rating_df, input$team1, input$team2)
tibble(Team = c(input$team1, input$team2), Win = c(p, 1-p)) %>%
mutate(Odds = (1/Win))
})
### Make Action Button update database, re-read example_match_file.tsv and rerun all calculations.
# datasetInput <- observeEvent(input$update, {
# ???
# })
}
# Run the application
shinyApp(ui = ui, server = server)
If I'm reading this right you'd like to spare your query limit by providing a local set of data to your shiny application. But if a user requests an update you'd like to trigger a query to be used in calculations.
I cannot recommend enough that you make full use of reactivity in Shiny. It is fairly rare to use an object from the global environment, especially when you intend for user inputs to manipulate those objects. You should have your base data ( in your case the tsv) load into the global environment, and then call that information into your application via a reactive dataframe. I built the below minimal example using mpg subset to the first 5 rows to simulate the .tsv on your local machine. mpg subset to 10 rows is to simulate the results of a query to a database. These two data sets get called via an if else statement dependent on an actionbutton.
library(tidyverse)
library(shiny)
# using partial mpg data to simulate un-updated data
mpg <- ggplot2::mpg[1:5,]
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
actionButton("update", "Update Data"),
uiOutput('selectOpts')
),
mainPanel(
h2("This is our base data layer"),
verbatimTextOutput('print_interval1'),
h2("This is our output data"),
verbatimTextOutput('print_interval2')
)
)
)
server <- function(input, output) {
# The core of shiny is the reactivity. It's the workhorse of interactive apps.
# If possible, a data calculation should always happen in a reactive context.
working_data <- reactive({
# actionbuttons increment a value by 1, starting with 0. If input < 0 the
# user has not interacted yet. If incremented again, the reactive context
# will invalidate and re-calculate the working_data() object
if (input$update < 1) {
base_dat <-
mpg %>%
mutate(ratio = cty/hwy)
} else {
base_dat <-
ggplot2::mpg[1:10,] %>% # calling from namespace to simulate a query. Full data
mutate(ratio = cty/hwy)
}
# return our base data. Can be called with `working_data()`
data.frame(base_dat)
})
output$print_interval1 <- renderPrint({
working_data()
})
output$selectOpts <- renderUI({
# using the reactive data inside renderUI we can be flexible in our options
# this lets us adapt the UI to reactive data.
radioButtons('model',
"Select Model",
sort(unique(working_data()$model)))
})
# You can also chain reactive objects.
output_data <- reactive({
working_data() %>%
group_by(model) %>%
filter(model == input$model) %>%
summarise(m.ratio = mean(ratio))
})
output$print_interval2 <- renderPrint({
output_data() %>%
data.table()
})
}
shinyApp(ui = ui, server = server)
I also recommend looking into this post about database syncing for setting up triggers and using reactive objects as your applications get more complex. I hope that's enough to get you on the right track for both your initial question about updating data, and your comments about having your inputs react to updated data.

R Shiny: How to subset and then sort a data frame?

I am new to Shiny. I was trying to subset a data frame and the data frame, but encountered an error message:
"Can't access reactive value 'xx' outside of reactive consumer."
Could anybody tell me why?
The design idea is to (1) let the users to select the subgroup that they'd like to look into, which I tried to accomplish using the reactiveValues() command but failed, and then (2), an delayed action, which is within that subgroup, sort the data based on a key variable. Below are the codes, and I appreciate your help:
library(shiny)
library(tidyverse)
data(iris)
ui <- fluidPage(
navbarPage(
title = "Test",
tabsetPanel(
tabPanel(
"Tab 3, subset and then sort",
sidebarLayout(
sidebarPanel(
selectInput("xx", "species:", choices = unique(iris$Species), selected = "setosa"),
actionButton("click", "sort")
),
mainPanel(
tableOutput("table3")
)
)
)
)
)
)
server <- function(input, output) {
rv <- reactiveValues(
#### This line caused a problem whenever I added %>% dplyr::filter ####
df3 = iris %>% dplyr::filter(Species == !!input$xx)
)
observeEvent(input$click, {
rv$df3 <- rv$df3[order(rv$df3$Sepal.Length), ]
})
output$table3 <- renderTable({
rv$df3
})
}
# Run the application
app <- shinyApp(ui = ui, server = server)
runApp(app)
reactiveValues should be used like a list of values that are updated/evaluated within reactive/observe blocks. It's being used incorrectly here, I think you should be using reactive or eventReactive.
Double-bang !! is relevant for NSE (non-standard evaluation) within rlang (and much of the tidyverse), but that's not what you're doing here. In your case, input$xx is character, in which case you can simply compare to it directly, ala Species == input$xx.
Sometimes, depending on the startup of an app, the reactive is triggered before the input has a valid value, instead it'll be NULL. This causes an error and glitches in the shiny interface, and can be avoided by the use if req.
Unfortunately, you can't resort a reactive data block outside of it.
Here's one alternative:
server <- function(input, output) {
rv_unsorted <- reactive({
req(input$xx)
dplyr::filter(iris, Species == input$xx)
})
rv_sorted <- reactive({
req(input$click)
dplyr::arrange(isolate(rv_unsorted()), Sepal.Length)
})
output$table3 <- renderTable({
rv_sorted()
})
}
Another method, which is less efficient (more greedy, less lazy),
server <- function(input, output) {
rv <- reactiveVal(iris)
observeEvent(input$xx, {
rv( dplyr::filter(iris, Species == input$xx) )
})
observeEvent(input$click, {
rv( dplyr::arrange(rv(), Sepal.Length) )
})
output$table3 <- renderTable({
rv()
})
}
This may seem more straight-forward logically, but it will do more work than will technically be necessary. (observe blocks are greedy, firing as quickly as possible, even if their work is not used/noticed. reactive blocks are lazy in that they will never fire unless something uses/needs them.)
Edit: I corrected the previous behavior, which was:
Load iris, have all species present, store in rv().
Immediately filter, showing just setosa, store in rv().
Display in the table.
Change selector to a new species.
Filter the contents of rv() so that only the new species are in the frame. Unfortunately, since the contents of rv() were just setosa, this next filtering removed all rows.
The means that the current observe-sequence (as greedy and inefficient as it may be) must start with a fresh frame at some point, so I changed the input$xx observe block to always start from iris.

Reactive data table in Shiny with optional filtering

I have a double problem creating a simple Shiny app. I know this topic was covered in several other questions, but having studied them, I havent found a question that would fully cover my problems.
I want to render mtcars table (full 32 rows as a default option) and then I want to apply a reactive filter on the number of gears with multiple selection allowed.
The following code doesnt work in a number of ways:
The mtcars table is not rendered unless filter on gears is set. Instead -I got an error message in the app: "Result must have length 32, not 0"
Choosing multiple gears levels does not add up correctly. When I select each gear number separately, the table is rendered correctly, but, lets say, when I combine to show gear number 3 and 4, I got only 10 rows return, which is incorrect.
I appreciate your help in advance.
ui <- fluidPage(
sidebarLayout(
mainPanel(
selectInput("pickvalue", label = "Gears", mtcars$gear,
selected = NULL, multiple = T)),
tableOutput("tableOut")
)
)
server <- function(input, output, session){
gears <- reactive({
mtcars %>% filter(gear == input$pickvalue) %>% select(-gear)
})
output$tableOut<- renderTable({gears()
})
}
shinyApp(ui = ui, server=server)
The key to your problems are
Create an if statement when your input is NULL (i.e. when you first open the app) that will just spit out the dataset
When selecting multiple inputs, you need to use %in% operator
See code below:
ui <- fluidPage(
sidebarLayout(
mainPanel(
selectInput("pickvalue", label = "Gears", unique(mtcars$gear),
selected = NULL, multiple = T)),
tableOutput("tableOut")
)
)
server <- function(input, output, session){
gears <- reactive({
dat <- mtcars
if (!is.null(input$pickvalue)){dat <- dat %>% filter(gear %in% input$pickvalue)}
dat <- dat %>% select(-gear)
return(dat)
})
output$tableOut<- renderTable({gears()})
}
shinyApp(ui = ui, server=server)

How to update slider range using text input in Shiny? (currently gives back Error: Result must have length 10, not 0)

I am creating a shiny app to analyze data in a database. I have set up a slider bar to select a range of values and also have two input boxes to adjust the range on the sliders.
In my simplified code below, the slider works fine, but when you try to update the slider by inputting numbers, I get the following error:
Error: Result must have length 10, not 0
The slider itself still works fine and does what it should in tandem with the selectInput, but as soon as you try to input a number into min or max and hit update, it gives back this error.
Looking online it seems this might be a problem with dplyr/filter(), but I couldn't really find any solutions for my problem and I'm not really sure if that's actually the problem here.
Below is some simplified code with some dummy data. For the slider, I am using the code found here to update the values: R shiny - Combine the slider bar with a text input to make the slider bar more user-friendly
library(shiny)
library(ggplot2)
library(readxl)
library(DT)
library(dplyr)
#Fake Data
MSGRAIN <- data.frame("Year" = c(2018,2018,2018,2017,2016,2010,2010,2000,2000,2000),
"SiteNameNew" = c('A','B','B','B','C','C','C','C','D','D'),
"RiverMile" = 550:559)
ui <- fluidPage(
# Selection Bar
fluidRow(
#Select by River Mile (Manual Input)
column(5,
controlledSliderUI('RiverMile')
),
#Select By Site
column(5,
selectInput("SiteNameNew",
"Site Name:",
c("All", unique(as.character(MSGRAIN$SiteNameNew))
)
)
),
column(6,h4(textOutput('test'))
),
#Create a new row for the table
DT::dataTableOutput("table")
)
)
server <- function(input, output, session) {
range <- callModule(controlledSlider, "RiverMile", 550, 559, c(550,559)
)
range$max <- 559
# Current Year
cyear <- as.numeric(format(Sys.Date(), "%Y"))
# Output to show if selected area has been tested in the last 5 years
output$test <- renderText({
data <- MSGRAIN %>%
filter(SiteNameNew == input$SiteNameNew,
RiverMile >= range$min,
RiverMile <= range$max
)
if (max(data$Year) >= cyear-5){
"This site has been tested in the last 5 years."
} else if (max(data$Year) <= cyear-5){
"This site has not been tested in the last 5 years."
} else {
"Cannot Determine"
}
})
output$table <- DT::renderDataTable(DT::datatable({
data <- MSGRAIN
# Sorts data based on Site Name selected
if (input$SiteNameNew !="All"){
data <- data[data$SiteNameNew == input$SiteNameNew,]
}
# Sorts data based on River Mile selected
if (range !="All"){
data <- data[data$RiverMile >= range$min & data$RiverMile <= range$max,]
}
# Show Data Table
data
})
)
}
# Run the application
shinyApp(ui = ui, server = server)
I think it might be a combination of the code from the link + my code causing the issue, but I am new to shiny and not really sure where things are going wrong. I only have a pretty general understanding of how the observeEvent code is working to update my slider.

R Shiny: Switching datasets based on user input

I am working on a shiny app where users can upload their own data and get some plots and statistics back. However, I also want to include an example dataset that gets used instead if the user presses a specific button. Importantly, the plots should be reactive so that users get updated plots whenever they click on the "use example data instead" button or upload a new file. I tried to recreate my current approach of overwriting the data object as best as I could here, but simply defining the data object twice doesn't overwrite the data in the way I hoped it would. Any suggestions are appreciated.
library(shiny)
# UI
ui <- fluidPage(
# Application title
titlePanel("Reproducible Example"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
fileInput("Upload", "Upload your own Data"),
actionButton("Example", "Use Example Data instead")
),
# Show a plot of the generated distribution
mainPanel(
plotOutput("hist")
)
)
)
# Server Logic
server <- function(input, output) {
data <- eventReactive(input$Upload,{input$Upload})
data <- eventReactive(input$Example, {faithful$eruptions})
output$hist <- renderPlot({hist(data())})
}
# Run the application
shinyApp(ui = ui, server = server)
You can use a reactiveVal like this:
server <- function(input, output) {
my_data <- reactiveVal()
observeEvent(input$Upload, {
tmp <- read.csv(input$Upload$datapath)
## do whatever is needed to parse the data
my_data(tmp)
})
observeEvent(input$Example, {
my_data(faithful)
})
output$hist <- renderPlot({
dat <- as.data.frame(req(my_data()))
dat <- dat[, sapply(dat, is.numeric), drop = FALSE]
validate(need(NCOL(dat) > 1, "No numeric columns found in provided data"))
hist(dat[,1])
})
}
Depending on upload or button click, you store your data in my_data which is a reactive value. Whenever this value changes, the renderPlot function fires and uses the correct data.
You can use a reactive value to access whether the user has chosen to use an example dataset or use their own dataset. The user can choose to switch between the active dataset using an input from your UI.
Here's the official explanation on reactive values from RStudio: link
This would go in your ui.R:
radioButtons("sample_or_real",
label = h4("User data or sample data?"),
choices = list(
"Sample Data" = "sample",
"Upload from user data" = "user",
),
selected = "user"
)
This would go in your server.R:
data_active <- reactive({
# if user switches to internal data, switch in-app data
observeEvent(input$sample_or_real_button, {
if(input$sample_or_real == "sample"){
data_internal <- sample_data_object
} else {
data_internal <- uploaded_data_object
}
})
Note, that when using a reactive value in your server.R file, it must have parentheses () at the end of the object name. So, you call the data_internal object as data_internal().

Resources