Download Handler with reactive datatable (R Shiny) - r

I have simplified a lot the shiny app I'm trying to build, but, in the idea, I have two functions :
choose_input <- function(n1,n2,n3){
x1 <<- n1+n2
x2 <<- n2+n3
x3 <<- (n1*n2)/n3
}
createmydata <- function(n){
c1 <- c(1:n)
c2 <- c1+(x2*x3)
c3 <- c2+x1
df <- data.frame("column1"=c1,"column2"=c2,"column3"=c3)
return(df)
}
You'll tell me that I can do simply one function with these two because they are very simple, but in my app there are a lot of lines and I have to separate the two. Anyway, here is my simulated code :
ui <- fluidPage(
numericInput("n1",label="Choose the first parameter",min=0,max=100,value=3),
numericInput("n2",label="Choose the second parameter",min=0,max=100,value=4),
numericInput("n3",label="Choose the third parameter",min=0,max=100,value=5),
numericInput("n",label="Choose dataframe length",min=1,max=10000,value=100),
radioButtons("filetype", "File type:",
choices = c("csv", "tsv")),
downloadButton('downloadData', 'Download'),
tableOutput("data")
)
server <- function(input,output){
RE <- reactive({
choose_input(input$n1,input$n2,input$n3)
createmydata(input$n)
})
output$data <- renderTable({
RE()
})
output$downloadData <- downloadHandler(
filename = function() {
paste(name, input$filetype, sep = ".")
},
content = function(file) {
sep <- switch(input$filetype, "csv" = ",", "tsv" = "\t")
write.table(RE(), file, sep = sep,
row.names = FALSE)
}
)
}
shinyApp(ui = ui, server=server)
As you can see, I'd like to download the output table to a csv or excel file... I let you try the code and then try to click on the download button, it does not work...

Debugging
When I run the code up above and attempted to download the data set, I received the following warning and error message in the Console Pane within RStudio.
Warning: Error in paste: object 'name' not found
Stack trace (innermost first):
1: runApp
Error : object 'name' not found
This led me to examine the paste() function used within the filename argument in shiny::downloadHandler(). In your code, you use the object name without ever assigning it a value.
I replaced name with the text "customTable" within the filename argument inside of downloadHandler().
output$downloadData <- downloadHandler(
filename = function() {
paste( "customTable", input$filetype, sep = ".")
},
content = function(file) {
sep <- switch(input$filetype, "csv" = ",", "tsv" = "\t")
write.table(RE(), file, sep = sep,
row.names = FALSE)
}
)
Downloading Data in Browser
After running the app.R script, I clicked on the Open in Browser button to view the Shiny app in a new tab on Chrome. Once there, I was successfully able to download both a .csv and .tsv file after hitting the download button.
Note: I'm looking for a better reason as to why this action needs to occur, but for now, I came across this relevant SO post Shiny app: downloadHandler does not produce a file.

Related

Two observe events in shiny

In shiny, I am trying to have two observe events at once. One of them is a URL passed parameter. The other is that what I press a "Run" button, I output a csv.
The first part works fine and can be verified by adding /?param=some_text. However when I press the "Run" button no csv is created. What am I missing here? I feel like I have actionButton setup up correctly and that observeEvent is the right command to put to the csv?
library(shiny)
ui <- fluidPage(
textOutput("param"),
actionButton(inputId = "button", label="Run")
)
server <- function(input, output, session) {
observe({
query <- parseQueryString(session$clientData$url_search)
if (!is.null(query[['param']])) {
output$param <- renderText({
query[['param']]
})
} else {
output$param <- renderText({"unset"})
}
})
observeEvent(input$button, {
write.csv(iris, file = "temp.csv", row.names = TRUE)
})
}
shinyApp(ui, server)
I think the issue is with the filename which you're writing.
Listening on http://127.0.0.1:4380 Warning in file(file,
ifelse(append, "a", "w")) : cannot open file
'2022-11-25_10:32:31_temp.csv': Invalid argument Warning: Error in
file: cannot open the connection
You can use gsub to parse the filename so its compatible or change it to something else:
observeEvent(input$button, {
myfile <- paste0(Sys.time(),"_temp.csv")
myfile <- gsub(" ","_",myfile)
myfile <- gsub("-","_",myfile)
myfile <- gsub(":","",myfile)
write.csv(iris, file = myfile, row.names = TRUE)
})

How to download multiple files from R/Shiny app?

There are a number of different Q/A's regarding this topic on SO, but none that I have been able to find that fit my use-case. I am also very surprised that RStudio / the Shiny developers themselves have not come out with some documentation on how to do this. Regardless, take this example application:
library(shiny)
library(glue)
library(tidyverse)
# Define UI for application
ui <- fluidPage(
# Application title
titlePanel("Test Multi-File Download"),
p("I hope this works!"),
downloadButton(
outputId = "download_btn",
label = "Download",
icon = icon("file-download")
)
)
# Define server logic
server <- function(input, output) {
#datasets stored in reactiveValues list
to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
blahblah <- iris
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".csv", sep = "")
},
content = function(file){
#works
#readr::write_csv(blahblah, file)
#Attempt 1
# #create some temp directory
# temp_directory <- tempdir()
# browser()
# reactiveValuesToList(to_download) %>%
# #x is data, y is name
# imap(function(x,y){
# #browser()
# #check if data is not null
# if(!is.null(x)){
# #create file name based on name of dataset
# file_name <- glue("{y}_data.csv")
# #write file to temp directory
# readr::write_csv(x, file_name)
# }
# })
# zip::zip(
# zipfile = file,
# files = ls(temp_directory),
# root = temp_directory
# )
}
)
}
# Run the application
shinyApp(ui = ui, server = server)
I have some datasets that are stored in a reactiveValues list, and I would like the user to be able to download them all. Ideally, I'd like for them just to be able to download multiple files all at once, rather than having to zip them up, and then download a .zip file. Another option I would be okay with is to add each dataset to an Excel sheet, then download the multi-sheet Excel file.
My general thought process (on the former) is as follows:
Download button gets pressed
create some temporary directory
write (the not NULL) datasets contained in to_download reactiveValues list to this directory
zip the temp directory and download
I feel like I am very close, however I have not been able to successfully get this work yet. Any ideas?
Edit 1: I am aware of the proposed answer here, but would like to avoid using setwd() because I believe it is bad practice to mess with working directories from within a Shiny application.
A few things edited and it's working:
using dir instead of ls inside the zip::zip call to show the contents of the temp directory (ls lists R environment rather than directory contents)
as a further suggestion: making a new, unique folder inside tempdir() to ensure only relevant files are added.
library(shiny)
library(glue)
library(tidyverse)
# Define UI for application
ui <- fluidPage(
# Application title
titlePanel("Test Multi-File Download"),
p("I hope this works!"),
downloadButton(
outputId = "download_btn",
label = "Download",
icon = icon("file-download")
)
)
# Define server logic
server <- function(input, output) {
#datasets stored in reactiveValues list
to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
blahblah <- iris
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".zip", sep = "")
},
content = function(file){
temp_directory <- file.path(tempdir(), as.integer(Sys.time()))
dir.create(temp_directory)
reactiveValuesToList(to_download) %>%
imap(function(x,y){
if(!is.null(x)){
file_name <- glue("{y}_data.csv")
readr::write_csv(x, file.path(temp_directory, file_name))
}
})
zip::zip(
zipfile = file,
files = dir(temp_directory),
root = temp_directory
)
},
contentType = "application/zip"
)
}
shinyApp(ui = ui, server = server)
In my own Shiny app I had used a multi-worksheet approach as you suggested above. An alternative setup which works to produce a multi-sheet xlsx workbook using openxlsx could be:
...
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".xlsx", sep = "")
},
content = function(file){
wb <- createWorkbook()
reactiveValuesToList(to_download) %>%
imap(function(x,y){
if(!is.null(x)){
addWorksheet(wb, sheetName = y)
writeData(wb, x, sheet = y)
}
})
saveWorkbook(wb, file = file)
},
contentType = "file/xlsx"
)
...
Created on 2021-12-16 by the reprex package (v2.0.1)

Optimizing Performance - Large File Input in Shiny

I have a function (clawCheck) defined in the file CheckClawback.R which takes three data frames as arguments. In my Shiny app, the user uploads three files which are then read into memory and used as the ClawCheck arguments. In order to save time, I want R to start reading a file into memory as soon as it is uploaded, and not only after the "GO" button is pressed, so that once the button is pressed, the arguments for ClawCheck are already in memory and ready to use.
I'm thinking that I have to use eventReactive expressions within the renderTable statement, since I don't want the files to be re-read every time a user changes some input. To avoid further complication, I assume the input is filled in in order, i.e first "account", then "commpaid", then "termriders". When I run the app and the first input file has been uploaded, there is no progress bar appearing which indicates that my code is not working correctly. Here is my (reduced) code:
library('shiny')
source("CheckClawback.R")
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
fileInput("account", "Account File (.csv)"),
fileInput("commpaid", "CommPaid File (.txt)"),
fileInput("termriders", "TermRiders File (.txt)"),
actionButton("do", "GO!")),
mainPanel(
tableOutput("out_table"))
)
)
server <- function(input, output) {
func <- eventReactive(input$do, {
req(acc)
req(comm)
req(term)
datat <<- clawCheck(acc, comm, term)
})
output$out_table <- renderTable({
eventReactive(input$account, {
withProgress(message = "Preparing Account Data...Please Wait", {
acc <<- read.csv(input$account$datapath, header = TRUE, sep = ",")
})
})
eventReactive(input$commpaid, {
withProgress(message = "Preparing CommPaid Data...Please Wait", {
comm <<- read.table(input$commpaid$datapath, header = TRUE, sep = "\t")
})
})
eventReactive(input$termriders, {
withProgress(message = "Preparing TermRiders Data...Please Wait", {
term <<- read.table(input$termriders$datapath, header = TRUE, sep = "\t")
})
})
withProgress(func(), message = "Loading Output...Please Wait")
datat
})
}
shinyApp(ui = ui, server = server)
Ideally, after a file is uploaded, a progress bar should appear, indicating that it is being processed. If, during this process, a second file is uploaded, a second progress bar should appear, indicating that the second file is being processed etc. Once the actual function call happens, I want the input files to be ready to go.
I am very thankful for any help!
Your use of <<- and withProgress() is wrong. Also, using eventReactive() inside a render*() is wrong. I suggest going through RStudio Shiny tutorials to get help on understanding how reactivity works. Also look at showNotification() instead of withProgress(). For now, here's what you probably need -
server <- function(input, output, session) {
acc <- reactive({
validate(need(input$account), "acc not uploaded")
# use showNotification(); use same approach for other files
read.csv(input$account$datapath, header = TRUE, sep = ",")
# use removeNotification() to remove mesg after file is uploaded
})
comm <- reactive({
validate(need(input$commpaid), "comm not uploaded")
read.table(input$commpaid$datapath, header = TRUE, sep = "\t")
})
term <- reactive({
validate(need(input$termriders), "term not uploaded")
read.table(input$termriders$datapath, header = TRUE, sep = "\t")
})
func <- eventReactive(input$do, {
clawCheck(acc(), comm(), term())
})
output$out_table <- renderTable({
func()
})
}

Shiny Download File based on File Path

I have a file which i generate in shiny
The user clicks a button and the file should download. However nothing happens
The function export_report generates the excel file and saves it to a location. The function then passes back the file location to the download handler so it will download the file. The problem seems to be that it isnt being returned correctly. I have tested the function (export_report) outside of shiny and it returns everything perfectly so I'm clearly doing something wrong from the shiny perspective.
The file itself is created where it is supposed to be on the server because i can download it within RStudio and see it in the file explorer. Can anyone help
# UI Section
downloadButton("downloadRpt", "Download Report")
# Server Section
output$downloadRpt <- downloadHandler(
filename = function() {
mydf <- report()
dateRange <- input$dates_report
selection <- input$selection
myfile <- export_report (mydf, selection, dateRange)
},
content = function(file) {
file.copy(myfile, file)
}
)
I have seen other examples R Shiny: Download existing file which is what my code is based on
EDIT 1: Adding the export_report function with some fake data to run it
export_report <- function(mydf,selection,dateRange) {
# Template for where the template excel file is stored
myoutputTemplate <- '/home/shiny_tutorials/Save to Database/templates/output_template.xlsx'
start_date <- dateRange[1]
end_date <- dateRange[2]
date_range <- paste(start_date ,end_date, sep = " - " )
# Load workbook the template workbook
wb <- loadWorkbook(myoutputTemplate)
# write to the workbook the data frame
writeWorksheet(wb, mydf, sheet="Details",
startRow=8, startCol=2,
header=FALSE)
# add the the customer the user selected
writeWorksheet(wb, selection, sheet="Details",
startRow=3, startCol=3,
header=FALSE)
# date
writeWorksheet(wb, date_range, sheet="Details",
startRow=5, startCol=3,
header=FALSE)
# Create The file Name
filename <- paste(selection, Sys.Date(), sep = " - ") %>%
paste(.,"xlsx", sep = ".")
# removes the % sign and extra qoutes
filename <- gsub (pattern = '\'|%','', x = filename)
# output directory
myoutput <- paste('/home/shiny_tutorials/Save to Database/output/',
filename, sep = '')
# Save workbook
saveWorkbook(wb, myoutput)
# Return File Path
myoutput
}
To call the function you can use the data below
dateRange <- c("2011-09-23","2016-09-23")
selection = "COMPANY_A"
mydf <- iris
myfile <- export_report(mydf,selection,dateRange)
EDIT 2 I have now managed to get an error out of it. When i cat(myfile) in the filename = function() { section of the code i get the error after the correct file path has been returned
Warning in rep(yes, length.out = length(ans)) :
'x' is NULL so the result will be NULL
Warning: Error in ifelse: replacement has length zero
Stack trace (innermost first):
1: runApp
Error : replacement has length zero
This error is basically because my file path does not get passed to the segment myfile so
if someone can tell me how to get the filepath generated by my function to the server section of the code below, that should fix my problem
content = function(file) {
file.copy(myfile, file)
}
Thank you to everyone who commented and clarified my thinking a bit on how the download handler works.
In the end, i created a new function which split up the export function above
The new function i used is called generate_file() which simply returns the file name
generate_file_name <- function(selection) {
# Create The file Name
filename <- paste(selection, Sys.Date(), sep = " - ") %>%
paste(.,"xlsx", sep = ".")
# removes the % sign and extra qoutes
filename <- gsub (pattern = '\'|%','', x = filename)
# output directory
myoutput <- paste('/home/shiny_tutorials/Save to Database/output/',
filename, sep = '')
# Return File Path
myoutput
}
Then in the server side
output$downloadRpt <- downloadHandler(
filename = function() {
selection <- input$company
generate_file_name(selection)
},
content = function(file) {
mydf <- report()
dateRange <- input$dates_report
selection <- input$company
export_report(mydf,selection,dateRange)
myfile <- generate_file_name(selection)
file.copy(myfile, file)
}
)
This then finds the newly created file and exports it for the user
I just checked your problem with this example code and it worked:
output$downloadData <- downloadHandler(
filename = function() {
data <- mtcars
myfile <- "data.csv"
write.csv(data, myfile)
myfile
},
content = function(file) {
print(file) //prints: [...]\\Local\\Temp\\RtmpEBYDXT\\fileab8c003878.csv
file.copy(file, file)
}
)
myfile is the filename of the downloaded file. You cannot use it in file.copy as input, this variable is out of scope. It seems that R creates a temp file name (see the print()).
Try to use the filename function to define your path or a custom file name, and the write.csv in the content part. Example code:
output$downloadData <- downloadHandler(
filename = function() {
paste(<user_name or date for eg>, ".csv", sep="")
},
content = function(file) {
write.csv(data, file)
}
)
I noticed in your comment above, you have asked how the application would generate the correct file when used by multiple users. For this part, you need to use the session.
So if your business logic functions were to come from an R file called foo.R, the server code should look something like:
shinyServer(func = function(input, output, session) {
source("./foo.R", local=TRUE)
......
And this would separate out the session for each user, thereby generating files specific to each, when downloading. Hope this gives you some pointers.

Download multiple csv files in a zipped folder in Shiny

Can someone please point out how I can make this download zip function work in server.R? When I run this, I get the following error:
[1] "/var/folders/00/1dk1r000h01000cxqpysvccm005p87/T//Rtmps3T6Ua"
Warning in write.csv(datasetInput()$rock, file = "rock.csv", sep = ",") :
attempt to set 'sep' ignored
Warning in write.csv(datasetInput()$pressure, file = "pressure.csv", sep = ",") : attempt to set 'sep' ignored
Warning in write.csv(datasetInput()$cars, file = "cars.csv", sep = ",") :
attempt to set 'sep' ignored
[1] "rock.csv" "pressure.csv" "cars.csv"
adding: rock.csv (deflated 54%)
adding: pressure.csv (deflated 42%)
adding: cars.csv (deflated 57%)
Error opening file: 2
Error reading: 9
library(shiny)
# server.R
server <- function(input, output) {
datasetInput <- reactive({
return(list(rock=rock, pressure=pressure, cars=cars))
})
output$downloadData <- downloadHandler(
filename = 'pdfs.zip',
content = function(fname) {
tmpdir <- tempdir()
setwd(tempdir())
print(tempdir())
fs <- c("rock.csv", "pressure.csv", "cars.csv")
write.csv(datasetInput()$rock, file = "rock.csv", sep =",")
write.csv(datasetInput()$pressure, file = "pressure.csv", sep =",")
write.csv(datasetInput()$cars, file = "cars.csv", sep =",")
print (fs)
zip(zipfile=fname, files=fs)
},
contentType = "application/zip"
)
}
# ui.R
ui <- shinyUI(fluidPage(
titlePanel('Downloading Data'),
sidebarLayout(
sidebarPanel(
downloadButton('downloadData', 'Download')
),
mainPanel()
)
)
)
shinyApp(ui = ui, server = server)
Solution: Include if(file.exists(paste0(fname, ".zip"))) {file.rename(paste0(fname, ".zip"), fname)} after zip() call.
The top solution still wasn't working for me. I was working in RStudio Server on a Linux server. The problem was that RStudio couldn't automatically locate the path to the zip executable. I had to manually specify it. In the command line, which zip revealed to me /usr/bin/zip.
So, I just had to set the R_ZIPCMD environment variable at the top of my code.
Sys.setenv(R_ZIPCMD="/usr/bin/zip")
Source: The help file for zip() mentions R_ZIPCMD.

Resources