Reputation: 403
How would I build a reactive UI that responds to a reactiveFileReader with varying data inputs?
I'm interested in integrating a reactiveFileReader into an app that graphs groups within the data and shows selected points group by group.
Challenges:
CRASHES/FAILS WHEN I
POTENTIAL SOLUTIONS:
reactive()
and req()
isolate()
the dependent UI so it only changes the # of groups the first time a file is loaded. I included mock data (and its generation), a UI, the broken server, and a working server that doesn't have the reactive file reader.
UPDATES
The only thing left is to have the renderUI
'group' not reset the moment the file is reread. Normally it's a good thing, but here I don't want that.
Packages
library(tidyr); library(dplyr); library(ggplot2); library(readr); library(stringr)
library(shiny)
#library(DT)
Mock Data
a1 <- structure(list(Group = c("alpha_1", "alpha_1", "alpha_2", "alpha_2", "alpha_3", "alpha_3"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(1, 1.1, 4, 4.1, 6.8, 7), y = c(2.1, 2, 7.3, 7, 10, 9.7)), .Names = c("Group", "Sample", "x", "y"), row.names = c(NA,-6L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")),Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
a2 <- structure(list(Group = c("alpha_6", "alpha_6", "alpha_7", "alpha_7", "alpha_9", "alpha_9", "alpha_10", "alpha_10"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(3,3.2, 5, 5.1, 1, 1.1, 5, 5.1), y = c(8.1, 7, 3, 4, 14, 15, 4,3)), .Names = c("Group", "Sample", "x", "y"), row.names = c(NA, -8L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")), Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
b2 <- structure(list(Group = c("beta_3", "beta_3", "beta_4", "beta_4", "beta_6", "beta_6"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(3, 3.2, 5, 5.1, 1, 1.1), y = c(8.1, 7, 3, 4, 14, 15)),.Names = c("Group", "Sample", "x", "y"), row.names = c(NA, -8L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")), Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
b3 <- structure(list(Group = c("beta_3", "beta_3", "beta_4", "beta_4", "beta_6", "beta_6"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(3, 3.2, 5, 5.1, 1, 1.1), y = c(8.1, 7, 3, 4, 14, 15)), .Names = c("Group", "Sample", "x", "y"), row.names = c(NA, -8L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")), Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
# Data export to simulate the problem
lz_write <- function(input) {
write_csv(input, paste0(substitute(input), ".csv"))
}
lz_write(a1); lz_write(a2); lz_write(b2); lz_write(b3) # Messed up function for lapply...
# rm(list = ls()) # Clean the environment
UI
ui <- fluidPage(
titlePanel("Minimal Example"),
fluidRow(
column(width = 2, class = "well",
# File selection
HTML(paste("Which file?")),
# Prefix:
selectInput(inputId = "p",
label = "Prefix:",
choices = c("a", "b", "c"),
selected = "a"),
# Suffix:
numericInput(inputId = "s",
label = "Suffix:",
min = 1,
max = 3,
value = 1,
step = 1)),
column(width = 10,
plotOutput(outputId = "scatterplot",
dblclick = "plot_dblclick", # Might not be necessary, but it's not more work to include but more work to exclude
brush = brushOpts(id = "plot_brush", resetOnNew = TRUE)))
),
fluidRow(
column(width = 3,
br(),
uiOutput(outputId = "group_n")),
column(width = 9,
fixedRow(
column(width = 3,
HTML(paste0("Arg 1"))),
column(width = 3,
HTML(paste0("Arg 2"))),
column(width = 3,
uiOutput(outputId = "num_2"))
)
)
),
fluidRow(
br(), br(), br(), #Lets add some gaps or spacing
DT::dataTableOutput(outputId = "Table")) # Summary table
) # Not sure if actually necessary for this example
Broken Server It's only problem right now is that the UI resets the moment the file is re-read...
server_broken <- function(input, output, session) { # Broken version
#Larger subset: A Reactive Expression # May be used later...
args <- reactive({
list(input$p, input$s) #which file do we wish to input. This was our tag
})
# Reactive File-reader Subset
path <- reactive({
paste0(input$p, input$s, ".csv")
}) # Reactive Filename, kinda like our args...
filereader <- function(input) { # The function we pass into a reactive filereader.
suppressWarnings(read_csv(input, col_types = cols(
Group = col_character(),
Sample = col_character(),
x = col_double(),
y = col_double())
))
}
##BROKEN REACTIVE FILE READER HERE##
data_1 <- reactiveValues() # The function we use for livestream data
observe({
if(file.exists(path()) == TRUE) {
fileReaderData <- reactiveFileReader(500, session, path(), filereader)
} else {
message("This file does not exist")
## OR DO I DO SOMETHING ELSE HERE??##
}
data_1$df <- reactive({
## STOPS APP CRASHING, BUT NO LONGER REFRESHES CONSTANTLY ##
req(fileReaderData())
fileReaderData()
})
}) # Honestly don't understand still
data <- reactive(data_1$df()) # Pulling things out just so the rest of our code can stay the same.
## END OF BROKEN FILE READER##
## Reactive UI HERE##
data_m <- reactive({
req(data())
args()
tmp <- isolate(select(data(), Group))
tmp %>% distinct()
}) # number of groups
output$num_2 <- renderUI({
req(data())
numericInput(inputId = "n",
label = "Group:",
min = 1,
max = length(data_m()$Group),
value = 1
)
}) #This is our 'reactive' numeric input for groups. This caps the max of our function based on the number of groups there are per file
n <- reactive(input$n) #which marker number we are dealing with.
## End of reactive UI##
data_n <- reactive({
req(data()); req(data_m())
dt <- filter(data(), Group == data_m()[[1]][input$n])
})
# Create scatterplot object the plotOutput function is expecting ----
ranges <- reactiveValues(x = NULL, y = NULL)
output$scatterplot <- renderPlot({
validate(need(data(), "The specified file does not exist. Please try another"))
p <- as.numeric(input$p)
plot <- ggplot(data_n(), aes(x, y)) +
labs(title = paste0("Group ", data_n()$Group[1])) +
labs(x = "X vals", y = "Y vals") +
geom_point() + theme_bw() # I already have customized aesthetics. Removed for minimalism
plot + coord_cartesian(xlim = ranges$x, ylim = ranges$y, expand = TRUE) # So we see all points more readily. messes up the zoom but oh well
})
# When a double-click happens, check if there's a brush on the plot.
# If so, zoom to the brush bounds; if not, reset the zoom.
observeEvent(input$plot_dblclick, {
brush <- input$plot_brush
if (!is.null(brush)) {
ranges$x <- c(brush$xmin, brush$xmax)
ranges$y <- c(brush$ymin, brush$ymax)
} else {
ranges$x <- NULL
ranges$y <- NULL
}
})
#Creating text ----
output$group_n <- renderText({
req(data())
paste0("There are ", length(data_m()$Group), " groups in this file.",
tags$br("This is Group: ", data_m()$Group[n()])
)
})
#Building a table for you to visibly see points. You may need to update the DT to the github version ----
output$Table <- DT::renderDataTable({
req(data())
brushedPoints(data_n(), brush = input$plot_brush) %>%
select(Sample)
})
}
Functional Server
It has been removed since the broken one at least doesn't crash, and the problem is apparent. See previous edits for the original.
Sources consulted
isolate
and observers
Session Info
UPDATES
Placing a reactive within an Observe()
stopped the app from crashing, AND it does update the files (forgot to delete some stuff). All that's left is saving the dependent UI somewhere...
Upvotes: 3
Views: 1849
Reputation: 403
In short, problems were due to not properly understanding the logic of observers
, missing a ()
after a reactive, and not calling req
to stop certain parts from re-executing (see HERE).
Specific line-by-line updates can be found by looking for ##CHANGE:
below... The most important changes (in no significant order) are:
isolate()
for the renderUI
req()
in the renderUI
to slow it down and not run until there is an update in the # of groups, but calling args()
to make it dependent upon the file selectionrenderUI
Updated Server
server_fixed <- function(input, output, session) {
#Larger subset: A Reactive Expression # May be used later...
args <- reactive({
list(input$p, input$s) #which file do we wish to input. This was our tag
})
# Reactive File-reader Subset
path <- reactive({
paste0(input$p, input$s, ".csv")
}) # Reactive Filename, kinda like our args...
filereader <- function(input) { # The function we pass into a reactive filereader.
suppressWarnings(read_csv(input, col_types = cols(
Group = col_character(),
Sample = col_character(),
x = col_double(),
y = col_double())
))
}
data_1 <- reactiveValues() # The function we use for livestream data
observe({
if(file.exists(path()) == TRUE) {
fileReaderData <- reactiveFileReader(500, session, path(), filereader)
} else {
message("This file does not exist")
}
data_1$df <- reactive({
# if(exists(fileReaderData())) {
# fileReaderData()
# } # Crashed from the beginning
req(fileReaderData())
fileReaderData()
})
})
data <- reactive(data_1$df()) ##CHANGE: FORGOT THE ()##
# Group setting...
data_m <- reactive({
req(data())
args()
tmp <- isolate(select(data(), Group))
tmp %>% distinct()
}) #number of markers, keeping only the marker name
data_m_length <- reactive({ ##CHANGE: TOOK OUT OF output$num_2##
##CHANGE: ADDED AN ISOLATE to fix the # of groups per file ##
isolate(length(data_m()$Group))
})
output$num_2 <- renderUI({
req(data_m_length()) ## CHANGE: ONLY EXECUTE ONCE WE HAVE OUR isolated data_m_length##
args() ## CHANGE: DEPENDENT UPON changing files##
isolate(
numericInput(inputId = "n",
label = "Group:",
min = 1,
max = data_m_length(),
value = 1 # THIS SHOULD BE CACHED!
)) ##CHANGE: ADDED IT IN ISOLATE when testing. NOT SURE IF STILL NEEDED##
}) #This is our 'reactive' numeric input for groups. This caps the max of our function based on the number of groups there are per file
n <- reactive(input$n) #which marker number we are dealing with.
data_n <- reactive({
req(data()); req(data_m())
dt <- filter(data(), Group == data_m()[[1]][n()])
})
# Create scatterplot object the plotOutput function is expecting ----
ranges <- reactiveValues(x = NULL, y = NULL)
output$scatterplot <- renderPlot({
validate(need(data(), "The specified file does not exist. Please try another"))
p <- as.numeric(input$p)
plot <- ggplot(data_n(), aes(x, y)) +
labs(title = paste0("Group ", data_n()$Group[1])) +
labs(x = "X vals", y = "Y vals") +
geom_point() + theme_bw() # I already have customized aesthetics. Removed for minimalism
plot + coord_cartesian(xlim = ranges$x, ylim = ranges$y, expand = TRUE) # So we see all points more readily. messes up the zoom but oh well
})
# When a double-click happens, check if there's a brush on the plot.
# If so, zoom to the brush bounds; if not, reset the zoom.
observeEvent(input$plot_dblclick, {
brush <- input$plot_brush
if (!is.null(brush)) {
ranges$x <- c(brush$xmin, brush$xmax)
ranges$y <- c(brush$ymin, brush$ymax)
} else {
ranges$x <- NULL
ranges$y <- NULL
}
})
#Creating text ----
output$group_n <- renderText({
req(data())
paste0("There are ", length(data_m()$Group), " groups in this file.",
tags$br("This is Group: ", data_m()$Group[n()])
)
})
#Building a table for you to visibly see points. You may need to update the DT to the github version ----
output$Table <- DT::renderDataTable({
req(data())
brushedPoints(data_n(), brush = input$plot_brush) %>%
select(Sample)
})
}
All that's left is to use suppressError
and validate
appropriately.
Upvotes: 3