I have an RShiny application where I'm displaying multiple data-frames across different tab-setted panels. I want to be able to write data-frames appearing across each panel to one csv file per panel. I am currently trying to render this using Shiny's downloadHandler option.
Did a little bit of research and found out I could use the sink() function to divert R output to a file.
Here's my attempt at trying to incorporate sink() into one such button on the server.R side
output$downloadDemographic <- downloadHandler(
filename = function() {
paste('Demographics', '.csv', sep='')
},
content = function(file) {
sink("Demographics.csv")
cat('Population Demographics')
write.csv(Population)
cat('Geography Demographics')
write.csv(Geography)
sink()
}
)
where Population and Geography are two data-frames with the same number of columns but different names.
The sink function automatically writes the above dataframes to a csv file 'Demographics.csv' into my working directory but the downloadHandler option on Shiny prompts the user to save the csv file on the fly. Kind of contradicting what I am trying to achieve here. I'm probably missing something or making an obvious mistake, I don't know.
Desired output - https://i.stack.imgur.com/yddrE.jpg
I could render multiple download buttons, write each of the data-frames to multiple csv files but some of my tab setted elements have as many as 8 data-frames and this would make the UI messy.
or
I could coerce multiple data-frames into a single one before writing to a csv file but they are all unequal sized dataframes and formatting them into the desired output would be a huge pain.
Any thoughts on how I could establish this any other way?
Thanks!
Other options:
Write an Excel workbook with one sheet per
dataframe
Zip together multiple csv
files
Here's a sample of both options using four dataframes from the R Datasets Package.
library(shiny)
library(xlsx)
shinyApp(
ui = fluidPage(
downloadButton("downloadExcelSheet", "Download Excel Workbook with Multiple Sheets"),
downloadButton("downloadZippedCSV", "Download zipped csv files")
),
server = function(input, output) {
#### Write an Excel workbook with one sheet per dataframe ####
output$downloadExcelSheet <- downloadHandler(
filename = function() {
"excelWorkbook.xlsx"
},
content = function(file) {
# write workbook and first sheet
write.xlsx(mtcars, file, sheetName = "mtcars", append = FALSE)
# add other sheets for each dataframe
listOtherFiles <- list(iris = iris,
airquality = airquality,
sleep = sleep)
for(i in 1:length(listOtherFiles)) {
write.xlsx(listOtherFiles[i], file,
sheetName = names(listOtherFiles)[i], append = TRUE)
}
}
)
#### Zip together multiple csv files ####
output$downloadZippedCSV <- downloadHandler(
filename = function() {
"zippedCSV.zip"
},
content = function(file) {
# go to temp dir to avoid permission issues
owd <- setwd(tempdir())
on.exit(setwd(owd))
# create list of dataframes and NULL value to store fileNames
listDataFrames <- list(mtcars = mtcars,
iris = iris,
airquality = airquality,
sleep = sleep)
allFileNames <- NULL
# loop through each dataframe
for(i in 1:length(listDataFrames)) {
# write each dataframe as csv and save fileName
fileName <- paste0(names(listDataFrames)[i], ".csv")
write.csv(listDataFrames[1], fileName)
allFileNames <- c(fileName, allFileNames)
}
# write the zip file
zip(file, allFileNames)
}
)
}
)
Related
I am new to Shiny, apologies if this is obvious and has been asked numerous times, but I've been stuck on this for days.
I've been modifying a dashboard to process analytical chemistry data i.e. it reads in multiple csv files, processes the data (smooths etc.) with various sliders and functions in Shiny, but does not save/download the processed data/output, which I've been trying to do. I don't seem to be able to access the "output" or processed data e.g. as a list of matrices, which I then write out as new .csv files. (I get "object of type 'closure' is not subsettable")
I am competent in R, and have script which works well, but making this change to Shiny is proving problematic. How do I access the output data of detectedPeaks or baselineCorrectedSpectra to write to csv (or zip up the mutilple csv files)?
Thank you.
#Just part of the relevant code - a long script
#server
baselineCorrectedSpectra <- reactive({
if (is.null(input$bc)) {
method <- "SNIP"
hws <- 100
} else {
method <- input$bc
hws <- input$bcHws
}
return(lapply(smoothedSpectra(), function(y) {
bl <- estimateBaseline(y, method=method, hws)
intensity(y) <- intensity(y)-bl[, 2]
return(y)
}))
})
detectedPeaks <- reactive({
return(detectPeaks(baselineCorrectedSpectra(), method=input$pdNoise,
halfWindowSize=input$pdHws, SNR=input$pdSNR))
})
datasetInput <- reactive({
switch(input$dtset,
"peaks" = detectedPeaks(),
"centroided" = baselineCorrectedSpectra())
})
output$DownloadZip <- downloadHandler(
filename = function(){
paste0("Results",".zip")
},
content = function(con){
files <- c()
tmpdir <- tempdir()
setwd(tempdir())
for (i in 1:length(s)){
x<-as.matrix(datasetInput[[i]]) #This doesn't work,how do I access this data?
y<-metaData(s[[i]])
f<-(paste("processed", y, sep="_" ))
if(input$downloadType == ".csv")
write.csv(x,f)
else write.table(x,f)
files<-c(x,files)
}
zip(zipfile=con, files=files)
},
contentType = "application/zip"
)
I added () to datasetInput
Can you give this a try:
x<-as.matrix(datasetInput([[i]]))
The way to do it is below:
x<-as.matrix(datasetInput()[[i]])
Im trying to save two data frames in one xlsx file, each one in a different sheet. The thing is that each data frame is a reactive object that depends on the same action button to generate them. The code in the download is like this:
output$downloadtable <- downloadHandler(
filename = function(){
paste("file.csv")
},
content = function(file){
write.xlsx(dataframe1(), file, row.names = FALSE, sheetName = "Hoja1")
write.xlsx(dataframe2(), file, row.names = FALSE, append = TRUE, sheetName = "Hoja2")
}
)
The problem is that I only get in my excel file the "Hoja2" sheet with the second data frame and not the first one... Somebody knows what might be wrong with this?
library(openxlsx)
filename = function() {
"mydata.xlsx"
},
content = function(file) {
g= openxlsx::createWorkbook()
openxlsx::addWorksheet(wb,"Hoja1")
openxlsx::writeData(wb,"Hoja1",dataframe1())
openxlsx::addWorksheet(wb,"Hoja2")
openxlsx::writeData(wb,"Hoja2",dataframe2())
openxlsx::saveWorkbook(g,file)
}
I am still relatively new at working in R shiny and I am trying to load several excel files into an R-shiny app. Part of the problem is that I need to be able to pull several files from a dropbox folder without specifying what the data file is called. So I need to be able to tell R to read in all the files from a dropbox folder. Also the files I am working with are in .xlsx format and I will need to read them into R as such.
I tried to do this first by using a folder on my computer desktop. I managed to get it to work using my local directory with the code below:
library(readxl)
library(tidyverse)
files <- list.files(path = "~/Desktop/data", pattern = "*.xlsx", full.names = TRUE) #read files from folder on desktop
df <- sapply(files, read_excel, simplify = FALSE) %>% #read files from the path, and bind them together
bind_rows()
I tried to adjust the code above to work with the drop_dir function in rdrop2. The code I tried is below:
library(rdrop2)
library(tidyverse)
library(readxl)
token <- drop_auth()
files <- drop_dir("!dropbox_folder", dtoken = token) #List all files in Dropbox folder MPD_03_Test
f <- files$path_display #list directory to dropbox
df <- sapply(f, read_excel, simplify = FALSE) %>% #runs the read function for all the files that are pulled
bind_rows() # .id="id creates a unique ID for each row and then binds them all together based on the ID.
When I run it the code is not loading the data files from the dropbox into R. When I run the dropbox code it just creates an empty object. Any help on where to go to figure this out will be greatly appreciated! Also I intend to use this as how I read data into and R-shiny app if that helps frame any suggestions you may have about how to approach my problem.
Thank You!
#MrGumble is correct in his comments. The files need to downloaded before being read. The drop_dir() function lists file paths on dropbox server and we can only read in data saved locally to our machine. If you have .csv files then this can be down in 1 step with the drop_read_csv() function. But since you have excel files these need to first to be downloaded explicitly with drop_download() and then read in with read_excel().
library(rdrop2)
library(tidyverse)
library(readxl)
#install.packages("xlsx")
library(xlsx)
token <- drop_auth()
#make a few excel file with iris dataset, save locally, and upload to dropbox root
iris_filenames <- paste0("iris", 1:3, ".xlsx")
walk(iris_filenames, ~write.xlsx(iris, file = .x, row.names = FALSE))
walk(iris_filenames, drop_upload)
#list all files on dropbox root and filter for only iris ones
iris_files_on_dropbox <- drop_dir(dtoken = token) %>%
filter(str_detect(name, 'iris'))
#make new filenames so we can see that the download worked correctly
#you could do overwrite = TRUE and not pass through new filenames
#see ?drop_download for all options
new_iris_filenames <- paste0("iris", 1:3, "-from-dropbox.xlsx")
#download the files first
walk2(iris_files_on_dropbox$name, new_iris_filenames, ~drop_download(path = .x, local_path = .y))
#then read them all in
df <- bind_rows(map(new_iris_filenames, read_xlsx))
Additionally, we can create our own custom function to do the download and reading in 1 step just as drop_read_csv() does by altering the source code for this function. All we need to do is change the read...() function from read.csv to read_excel and the reference to the dtoken default get_drop_token() to rdrop2:::get_drop_token() which is an un-exported function from the rdrop2 package so we need the three ':::'.
#source for drop_read_csv we can rewrite for excel files
# drop_read_csv <- function(file, dest = tempdir(), dtoken = get_dropbox_token(), ...) {
# localfile = paste0(dest, "/", basename(file))
# drop_download(file, localfile, overwrite = TRUE, dtoken = dtoken)
# utils::read.csv(localfile, ...)
# }
drop_read_excel <- function(file, dest = tempdir(), dtoken = rdrop2:::get_dropbox_token(), ...) {
localfile = paste0(dest, "/", basename(file))
drop_download(file, localfile, overwrite = TRUE, dtoken = dtoken)
readxl::read_excel(localfile, ...)
}
df2 <- bind_rows(map(iris_files_on_dropbox$name, drop_read_excel))
To work in a shiny app we first need to save the drop_auth token so we can authenticate while using the shiny app. Save this into your shiny app directory.
saveRDS(token, file = "token.rds")
Now here is a shiny app. When the 'go' button is clicked the iris excel files are downloaded and shown in the UI. We need to call drop_auth() in the global environment or global.R along with the custom drop_read_excel() function to use it.
library(shiny)
library(rdrop2)
library(tidyverse)
#saveRDS(token, file = "token.rds") into shiny app directory
#authenticate in global.R or outside of ui/server
drop_auth(rdstoken = "token.rds")
drop_read_excel <- function(file, dest = tempdir(), dtoken = rdrop2:::get_dropbox_token(), ...) {
localfile = paste0(dest, "/", basename(file))
drop_download(file, localfile, overwrite = TRUE, dtoken = dtoken)
readxl::read_excel(localfile, ...)
}
ui <- fluidPage(
actionButton("go", "go"),
tableOutput("table")
)
server <- function(input, output, session) {
df <- eventReactive(input$go, {
withProgress(message = 'Downloading from dropbox',
detail = 'This may take a while...', value = 0.5, {
iris_files_on_dropbox <- drop_dir() %>%
filter(str_detect(name, 'iris'))
setProgress(value = 0.75)
df <- bind_rows(map(iris_files_on_dropbox$name, drop_read_excel))
setProgress(value = 1)
})
return(df)
})
output$table <- renderTable({
df()
})
}
shinyApp(ui, server)
Context: I have an app transforming data according to user's choices. It creates a few tables and plots in the process.
Objective: to save some objects created in the process into one new folder with one click on a button.
Previous researches: the code below saves objects using downloadHandler() and some functions as presented here. It does not seems to allow multiple objects to be passed into downloadHandler(). I am aware it is possible to stack these objects in a list and then save it but if possible I would like to avoid doing it and instead get multiple files (like .txt or .png, ...)
Here is a reproductible example with very little data using datasets included in R (mtcars and iris).
library(shiny)
ui <- fluidPage(
downloadButton("save", "Save") # one click on this button to save df1 AND df2 tables in a new folder
)
server <- function(input, output) {
# my real app does multiple changes on datasets based on user choices
df1 = mtcars[1:10,]
df2 = iris[1:10,]
# Now I want to save df1 and df2 objects with 1 click on the "Save" button
output$save = downloadHandler(
filename = function(){ paste("example", ".txt", sep = " ") },
content = function(file) { write.table(df1, file) }
)
}
# Run the application
shinyApp(ui = ui, server = server)
Many thanks for your help and suggestions!
As noted in the comments of the linked post, it's not typically a good idea to change the working directory (and unnecessary in this case). While inconsequential with a small number of files, the paste0 call to create the path doesn't need to be in the for loop as it is vectorized. This also eliminates the need to dynamically grow the fs vector (also generally a bad practice). Lastly, my zip utility wasn't on my path which caused the utils::zip to fail (you can specify the path in the function call, otherwise it checks for the environment variable R_ZIPCMD and defaults to 'zip' assuming it to be on the path).
I generally agree with the accepted answer, but here's an alternative solution using the zip::zipr function instead (also walk instead of the for loop)
library(shiny)
library(purrr)
library(zip)
ui <- fluidPage(
downloadButton("save", "Save") # one click on this button to save df1 AND df2 tables in a new folder
)
server <- function(input, output) {
# my real app does multiple changes on datasets based on user choices
df1 <- mtcars[1:10,]
df2 <- iris[1:10,]
# need to names these as user won't be able to specify
fileNames <- paste0("sample_", 1:2, ".txt")
output$save = downloadHandler(
filename = function(){ paste0("example", ".zip") },
content = function(file) {
newTmpDir <- tempfile()
if(dir.create(newTmpDir)){
# write data files
walk2(list(df1, df2), fileNames,
~write.table(.x, file.path(newTmpDir, .y))
)
# create archive file
zipr(file, files = list.files(newTmpDir, full.names = TRUE))
}
},
contentType = "application/zip"
)
}
I want to use Shiny within RMarkdown for users to upload data (xlsx file).
Then I want to pass all the worksheets as R data frames (w/o reactivity) to run rest of the RMarkdown file.
I mainly want to convert them into data frames so I can use reticulate to run Python code as well.
I've tried this, and it doesn't seem to quite work:
library(dplyr)
library(miniUI)
library(shiny)
library(XLConnect)
launch_shiny <- function() {
ui <- miniPage(
gadgetTitleBar("Input Data"),
miniContentPanel(
fileInput(inputId = "my.file", label = NULL, multiple = FALSE)
)
)
server <- function(input, output, session) {
wb <- reactive({
new.file <- input$my.file
loadWorkbook(
filename = new.file$datapath,
create = FALSE,
password = NULL
)
})
observeEvent(input$done, {
stopApp(c(wb()))
})
}
runGadget(ui, server)
}
test <- launch_shiny()
df1 <- readWorksheet(object = test, sheet = "sheet1")
df2 <- readWorksheet(object = test, sheet = "sheet2")
It throws this error:
Error in (function (classes, fdef, mtable) :
unable to find an inherited method for function ‘readWorksheet’ for signature ‘"list", "character"’
I can return one sheet at a time using stopApp(readWorksheet(object = wb(), sheet = "sheet1")), but I can't seem to return an entire workbook or multiple data frames at the same time.
I don't really want to read in xlsx, save each sheet as csv in working directory, then read those files in again.
Would anyone have a good suggestion on how to get around this?
The documentation of fileInput() states in the details:
datapath
The path to a temp file that contains the data that was
uploaded. This file may be deleted if the user performs another upload
operation.
Meaning that the datapath given in the input variable is a temporary file that is no longer accessible after you close the App, which is what the function readWorksheet will try to do.
So you'll have to read the sheets in the server and return the dataframes somehow.
I did that by defining a second reactive value which is basically a list of dataframes returned by applying lapply on all the sheets in wb, in this case test will be this list of data frames.
There might be other ways (more efficient, or suits your purpose better) to do this, but here it is:
library(dplyr)
library(miniUI)
library(shiny)
library(XLConnect)
launch_shiny <- function() {
ui <- miniPage(
gadgetTitleBar("Input Data"),
miniContentPanel(
fileInput(inputId = "my.file", label = NULL,
multiple = FALSE)
)
)
server <- function(input, output, session) {
wb <- reactive({
new.file <- input$my.file
loadWorkbook(
filename = new.file$datapath,
create = FALSE,
password = NULL
)
})
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
df_lst <- reactive({
# read all sheets into a list
lapply(getSheets(wb()),
function(sheet){
readWorksheet(object = wb(),
sheet = sheet)
})
})
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
observeEvent(input$done, {
# get the list of dfs from the app
stopApp(c(df_lst()))
})
}
runGadget(ui, server)
}
test <- launch_shiny()