For part of a Shiny application I am building, I need to have the user select a directory. The directory path is stored in a reactive variable. The directory can either be selected by the user from a file window or the path can be manually entered by textInput. I have figured out how to do this, but I don't understand why the solution I have works! A minimal example of the working app:
library(shiny)
ui <- fluidPage(
actionButton("button1", "First Button"),
textInput("inText", "Input Text"),
actionButton("button2", "Second Button"),
textOutput("outText"),
textOutput("outFiles")
)
server <- function(input, output) {
values <- reactiveValues(inDir = NULL)
observeEvent(input$button1, {values$inDir <- tcltk::tk_choose.dir()})
observeEvent(input$button2, {values$inDir <- input$inText})
inPath <- eventReactive(values$inDir, {values$inDir})
output$outText <- renderText(inPath())
fileList <- reactive(list.files(path=inPath()))
output$outFiles <- renderPrint(fileList())
}
shinyApp(ui, server)
The first thing I tried was to just use eventReactive and assign the two sources of input to the reactive variable:
server <- function(input, output) {
inPath <- eventReactive(input$button1, {tcltk::tk_choose.dir()})
inPath <- eventReactive(input$button2, {input$inText})
output$outText <- renderText(inPath())
fileList <- reactive(list.files(path=inPath()))
output$outFiles <- renderPrint(fileList())
}
The effect of this as far as I can tell is that only one of the buttons does anything. What I don't really understand is why this doesn't work. What I thought would happen is that the first button pushed would create inPath and then subsequent pushes would update the value and trigger updates to dependent values (here output$outText). What exactly is happening here then?
The second thing I tried, which was almost there, was based off of this answer:
server <- function(input, output) {
values <- reactiveValues(inDir = NULL)
observeEvent(input$button1, {values$inDir <- tcltk::tk_choose.dir()})
observeEvent(input$button2, {values$inDir <- input$inText})
inPath <- reactive({if(is.null(values$inDir)) return()
values$inDir})
output$outText <- renderText(inPath())
fileList <- reactive(list.files(path=inPath()))
output$outFiles <- renderPrint(fileList())
}
This works correctly except that it shows an "Error: invalid 'path' argument" message for list.files. I think this may mean that fileList is being evaluated with inPath = NULL. Why does this happen when I use reactive instead of eventReactive?
Thanks!
You could get rid of the inPath reactive and just use values$inDir instead.
With req() you'll wait until values are available. Otherwise you'll get the same error (invalid 'path' argument).
The reactive triggers right away, while the eventReactive will wait until the given event occurs and the eventReactive is called.
And if(is.null(values$inDir)) return() won't work correctly, as it will return NULL if values$inDir is NULL, which is then passed to list.files. And list.files(NULL) gives the error: invalid 'path' argument.
Replace it with req(values$inDir) and you won't get that error.
And your example with 2 inPath - eventReactive's won't work, as the first one will be overwritten by the second one, so input$button1 won't trigger anything.
library(shiny)
ui <- fluidPage(
actionButton("button1", "First Button"),
textInput("inText", "Input Text"),
actionButton("button2", "Second Button"),
textOutput("outText"),
textOutput("outFiles")
)
server <- function(input, output) {
values <- reactiveValues(inDir = NULL)
observeEvent(input$button1, {values$inDir <- tcltk::tk_choose.dir()})
observeEvent(input$button2, {values$inDir <- input$inText})
output$outText <- renderText(values$inDir)
fileList <- reactive({
req(values$inDir);
list.files(path=values$inDir)
})
output$outFiles <- renderPrint(fileList())
}
shinyApp(ui, server)
You could also use an eventReactive for button1 and an observeEvent for button2, but note that you need an extra observe({ inPath() }) to make it work. I prefer the above solution, as it is more clear what's happening and also less code.
server <- function(input, output) {
values <- reactiveValues(inDir = NULL)
inPath = eventReactive(input$button1, {values$inDir <- tcltk::tk_choose.dir()})
observe({
inPath()
})
observeEvent(input$button2, {values$inDir <- input$inText})
output$outText <- renderText(values$inDir)
fileList <- reactive({
req(values$inDir);
list.files(path=values$inDir)
})
output$outFiles <- renderPrint(fileList())
}
And to illustrate why if(is.null(values$inDir)) return() won't work, consider the following function:
test <- function() { if (TRUE) return() }
test()
Although the if-condition evaluates to TRUE, there is still gonna be a return value (in this case NULL), which will be passed on to the following functions and in your case list.files, which will cause the error.
Related
I want to save the value from username input if it doesn't exist in data frame, and render text if it already exists (for reprex purpose).
Rendering text part works perfectly, but I don't know how to save it and use it later.
I want to save the value permanently, not only on current session
I've got this error:
Warning: Error in <-: invalid (NULL) left side of assignment
library(shiny)
ui <- fluidPage(
textInput("username", "username"),
actionButton("save", "Save!"),
textOutput("confirmation")
)
server <- function(input, output, session) {
df <- reactive(data.frame(column1 = "user1"))
exist <- reactive(input$username %in% df()$column1)
observeEvent(input$save, {
if (exist() == TRUE) {
output$confirmation <- renderText("Username already exists!")
} else {
df() <- rbind(df(), input$username) # <-- THIS dosn't work
}
})
}
shinyApp(ui, server)
EDIT:
Thanks to #I_O answer, I figured out this solution
reactiveVal() keep the changes in current session.
write_csv() and read_csv() part, allows app to use previously saved usernames.
saved_df <- read_csv("C:\\Users\\Przemo\\Documents\\R\\leaRn\\Shiny\\Moodtracker\\testers\\test_safe.csv")
ui <- fluidPage(
textInput("username", "username"),
actionButton("save", "Save!"),
textOutput("confirmation")
)
server <- function(input, output, session) {
df <- reactiveVal(saved_df)
exist <- reactive(input$username %in% df()$column1)
observeEvent(input$save, {
if (exist() == TRUE) {
output$confirmation <- renderText("Username already exists!")
} else {
output$confirmation <- renderText("")
df(rbind(df(), input$username))
write_csv(df(), "C:\\Users\\Przemo\\Documents\\R\\leaRn\\Shiny\\Moodtracker\\testers\\test_safe.csv")
}
})
}
shinyApp(ui, server)
I am trying to access the data frame created in one render function into another render function.
There are two server outputs, lvi and Category, in lvi I have created Data1 data frame and Category I have created Data2 dataframe. I want to select Data2 where Data1 ID is matching.
I am following the below steps to achieve my objective but I get error "Object Data1 not found".
My UI is
ui <- fluidPage(
# App title ----
titlePanel("Phase1"),
fluidPage(
column(4,
# Input: Select a file ----
fileInput("file1", "Import file1")
)
),
fluidPage(
column(4,
# Input: Select a file ----
fileInput("file2", "Import File2")
)
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Data file ----
dataTableOutput("lvi"),
dataTableOutput("category")
)
)
My server code is
server <- function(input, output) {
output$lvi <- renderDataTable({
req(input$file1)
Data1 <- as.data.frame(read_excel(input$file1$datapath, sheet = "Sheet1"))
})
output$category <- renderDataTable({
req(input$file2)
Data2 <- as.data.frame(read_excel(input$file2$datapath, sheet = "Sheet1"))
Data2 <- Data2[,c(2,8)]
Data2 <- Data2[Data1$ID == "ID001",]
})
}
shinyApp(ui, server)
Once a reactive block is done executing, all elements within it go away, like a function. The only thing that survives is what is "returned" from that block, which is typically either the last expression in the block (or, when in a real function, something in return(...)). If you think of reactive (and observe) blocks as "functions", you may realize that the only thing that something outside of the function knows of what goes on inside the function is if the function explicitly returns it somehow.
With that in mind, the way you get to a frame inside one render/reactive block is to not calculate it inside that reactive block: instead, create that frame in its own data-reactive block and use it in both the render and the other render.
Try this (untested):
server <- function(input, output) {
Data1_rx <- eventReactive(input$file1, {
req(input$file1, file.exists(input$file1$datapath))
as.dataframe(read_excel(input$file1$datapath, sheet = "Sheet1"))
})
output$lvi <- renderDataTable({ req(Data1_rx()) })
output$category <- renderDataTable({
req(input$file2, file.exists(input$file2$datapath),
Data1_rx(), "ID" %in% names(Data1_rx()))
Data2 <- as.data.frame(read_excel(input$file2$datapath, sheet = "Sheet1"))
Data2 <- Data2[,c(2,8)]
Data2 <- Data2[Data1_rx()$ID == "ID001",]
})
}
shinyApp(ui, server)
But since we're already going down the road of "better design" and "best practices", let's break data2 out and the data2-filtered frame as well ... you may not be using it separately now, but it's often better to separate "loading/generate frames" from "rendering into something beautiful". That way, if you need to know something about the data you loaded, you don't have to (a) reload it elsewhere, inefficient; or (b) try to rip into the internals of the shiny DataTable object and get it manually. (Both are really bad ideas.)
So a slightly better solution might start with:
server <- function(input, output) {
Data1_rx <- eventReactive(input$file1, {
req(input$file1, file.exists(input$file1$datapath))
as.dataframe(read_excel(input$file1$datapath, sheet = "Sheet1"))
})
Data2_rx <- eventReactive(input$file2, {
req(input$file2, file.exists(input$file2$datapath))
dat <- as.dataframe(read_excel(input$file2$datapath, sheet = "Sheet1"))
dat[,c(2,8)]
})
Data12_rx <- reactive({
req(Data1_rx(), Data2_rx())
Data2_rx()[ Data1_rx()$ID == "ID001", ]
})
output$lvi <- renderDataTable({ req(Data1_rx()); })
output$category <- renderDataTable({ req(Data12_rx()); })
}
shinyApp(ui, server)
While this code is a little longer, it also groups "data loading/munging" together, and "render data into something beautiful" together. And if you need to look at early data or filtered data, it's all right there.
(Side note: one performance hit you might see from this is that you now have more copies of data floating around. As long you are not dealing with "large" data, this isn't a huge deal.)
I've created a dynamic fileInput in shiny using lapply. When I want to read the file, I've also used lapply in an observer.
The problem of using lapply here is, it is triggered every time I upload a new file and thus, reads all files again and again if a new file is uploaded.
Here I provide a Hello World app. The lapply function depends on an input paramter which I abtracted from for simplicity.
library(shiny)
ui <- fluidPage(
titlePanel("Hello World"),
sidebarLayout(
sidebarPanel(),
mainPanel(
lapply(1:2, function(i) {
fileInput(
paste0("file", i),
label = NULL,
multiple = F,
accept = c(
"text/csv",
"text/comma-separated-values,text/plain",
".csv"
),
buttonLabel = paste("File", i)
)
}),
verbatimTextOutput("list")
)
)
)
server <- function(input, output) {
r <- reactiveValues()
observe({
lapply(1:2, function(i) {
file <- input[[paste0("file",i)]]
if(is.null(file)) return()
isolate({
r$file[[paste(i)]] <- readr::read_csv2(file = file$datapath)
})
})
})
output$list <- renderPrint(reactiveValuesToList(r))
}
shinyApp(ui = ui, server = server)
How to replace the loop or add a requirement to lapply?
While I started down the road of cache-invalidation in the comments, I think something else may work better for you since you have a fixed number of fileInput fields: swap the lapply and observe lines in your code (plus a couple of other tweaks).
server <- function(input, output) {
lapply(paste0("file", 1:2), function(nm) {
observeEvent(input[[ nm ]], {
req(input[[nm]], file.exists(input[[nm]]$datapath))
readr::read_csv2(file = input[[nm]]$datapath)
})
})
}
Explanation:
I'm creating a list of reactive blocks instead of a reactive block operating on a list. This means "file1" won't react to "file2".
I short-cutted the definition of the input names by putting paste0(...) in the data of the lapply instead of in the function, though it'd be just as easy to do
lapply(1:2, function(i) {
nm <- paste0("file", i)
# ...
})
It's important to have nm defined outside of the observeEvent, and it has to do with delayed evaluation and namespace search order. I fell prey to this a few years ago and was straightened out by Joe Cheng: you can't use a for loop, it must be some environment-preserving operation like this.
N.B.: this is a stub of code, and it is far from complete: having an observe or observeEvent read the data and then discard it is wrong ... it's missing something. Ideally, this should really be a reactive or eventReactive block, or the processed data should be stored in a reactiveValues or reactiveVal. For example:
server <- function(input, output) {
mydata <- lapply(paste0("file", 1:2), function(nm) {
observeEvent(input[[ nm ]], {
req(input[[nm]], file.exists(input[[nm]]$datapath))
readr::read_csv2(file = input[[nm]]$datapath)
})
})
observe({
# the following are identical, the latter more declarative
mydata[[1]]
mydata[["file1"]]
})
}
(And another note about defensive programming: you cannot control perfectly how readr::read_csv2 reacts to that file ... it may error out for some reason. One further step would be to wrap it in tryCatch(..., error = function(e) { errfun(e); NULL; }) where errfun(e) does something meaningful with the error message (logs it and/or gives it to the user in a modal popup) and then returns NULL so that reactive blocks downstream can use req(mydata[[1]]) and will not try to process the NULL.
server <- function(input, output) {
mydata <- lapply(paste0("file", 1:2), function(nm) {
observeEvent(input[[ nm ]], {
req(input[[nm]])
file <- input[[nm]]
tryCatch(
readr::read_csv2(file = input[[nm]]$datapath),
error = function(e) { errfun(e); NULL; })
})
})
observe({
# the following are identical, the latter more declarative
mydata[[1]]
mydata[["file1"]]
})
}
I have a problem with my code. I have 2 input files which I want to read with click of button and a numeric input which contains a filter value for the output of the table being created from the 2 files (after manipulating the data). The whole process (read files + create table + filter) right now is executed every time the user click the button. I want to do only the filter action if the input files doesn't change, because the process takes long time.
After the first click I want to do only the filtering command when the numeric input changes, unless the input files is also changed by the user.
The following code reproduces my problem:
library(shiny)
library(data.table)
server <- function(input, output, session) {
output$table1 <- renderDataTable({
input$gobtn
isolate({
infile1 <<- input$f1
infile2 <<- input$f2
if (is.null(infile1) || is.null(infile1)) {
return (NULL)
}
else {
calc()
}
})
})
calc <- function() {
inf1 <<- fread(infile1$datapath)
inf2 <<- fread(infile2$datapath)
# do some process with files data.....
my_table <- as.data.table(rbind(inf1, inf2))
setnames(my_table, c('name', 'rank'))
result <- my_table[rank > input$rank]
return(result)
}
}
ui <- basicPage(
fileInput("f1", "f1"),
fileInput("f2", "f2"),
numericInput("rank", "show rank only above :", value = 6),
actionButton("gobtn", "show"),
dataTableOutput('table1')
)
shinyApp(ui = ui, server = server)
The way to use reactivity is to break things into parts, so that you only need to update what is necessary. The first step in your pipeline is reading and processing the files. This seems like a good reactive: if they don't change, nothing happens, but when they change, everything that needs to be recalculated is recalculated. The next step is filtering, when the filter variable changes we want to refilter the data. Then we can just put that in the output.
server <- function(input, output, session) {
processedData <- reactive({
req(input$f1,input$f2)
inf1 <- fread(input$f1$datapath)
inf2 <- fread(input$f2$datapath)
# do some process with files data.....
my_table <- as.data.table(rbind(inf1, inf2))
setnames(my_table, c('name', 'rank'))
my_table
}
filteredData <- reactive({
req(input$rank)
processedData()[processedData()$rank > input$rank]
})
output$table1 <- renderDataTable({
input$gobtn
isolate({
filteredData()
})
})
}
I've observed some strange behaviour of rhandsontable in a shiny app. In this simple example I am assigning a data.frame to a reactiveValues element if some event happens. The data is then shown in a rhandsontable. But when I change some entry of the table the function hot_to_r fails with: Error in seq.default: argument 'length.out' must be of length 1
Strangely the error only happens if I use iris, but not when I use iris[1:50, ], which should be identical. Does somebody have an idea, how to fix this?
(There is another error when values$data is still NULL before the actionButton is clicked. I'm aware of this, but this is not relevant for the question.)
library(shiny)
ui <- fluidPage(
actionButton("click", "click"),
rHandsontableOutput("table")
)
server <- function(input, output, session) {
values <- reactiveValues(data = NULL)
observeEvent(input$click, {
values$data <- iris # with iris[1:50, ] no error appears
})
output$table <- renderRHandsontable({
rhandsontable(t(values$data))
})
observe({
if (!is.null(input$table$changes$changes)) {
table_data <- hot_to_r(input$table)
print(table_data)
}
})
}
shinyApp(ui, server)
#BigDataScientist is on to something, colnames(t(iris)) is NULL, whereas colnames(t(iris[1:50,])) is not. That is a mystery to me, but preventing that nullness should resolve your problem. Using something in the call to rhandsontable should do the trick. Using
rhandsontable(data.frame(t(values$data)))
worked for me.