downloadHandler: Set reactiveValues inside content function - r

Is it possible to set reactiveValues inside the content part of the downloadHandler? I tried it and don't understand the behavior.
A simple example could be a counter showing how often the download button has been clicked:
library(shiny)
ui <- fluidPage(
downloadButton("downloadData", "Download"),
textOutput("nDownloads"),
actionButton("trig", "get number")
)
server <- function(input, output) {
# Our dataset
data <- mtcars
r.nDownloads <- reactiveValues(n=0)
output$nDownloads <- renderText({
input$trig
paste("number of downloads:", r.nDownloads$n)
})
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
},
content = function(file) {
r.nDownloads$n <- r.nDownloads$n + 1
write.csv(data, file)
}
)
}
shinyApp(ui, server)
If the download button is clicked, the textOutput is grayed out, but not updated. I added an action button as a trigger to force the renderText to be updated. Surprisingly (at least to me) that works: the correct number is shown.
So, somehow the reactiveValue is changed by the downloadHandler, but its dependencies are only invalidated, not updated.
Of course, the proper way to do it would be making the "data"-object reactive and doing the counting there. But I'm curious how the described behavior can be explained.
EDIT:
OK, now I get really confused: I tried what I mentioned above: making "data" reactive and doing the counting there. This could not be simple counting of downloads anymore, because the data-reactive gets only recalculated if it's invalid.
Here is an example with an additional input for the invalidation of "data":
library(shiny)
ui <- fluidPage(
numericInput("nRows", label = "nRows", min=1, max=32, value=15),
downloadButton("downloadData", "Download"),
textOutput("nDownloads")
)
server <- function(input, output) {
r.nDownloads <- reactiveValues(n=0)
# Our dataset
data <- reactive({
isolate({
r.nDownloads$n <- r.nDownloads$n + 1
})
mtcars[1:input$nRows,]
})
output$nDownloads <- renderText({
paste("number of downloads:", r.nDownloads$n)
})
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
},
content = function(file) {
write.csv(data(), file)
}
)
}
shinyApp(ui, server)
But still, I see a similar behavior: Clicking the download button grays the text out, changing "nRows" makes the expected number of downloads (which is now downloads after a change in nRows ;-)) to show up.
Now it gets an actual problem for me: In my real app, a rather complex Excel file can be downloaded. While preparing and formatting the Excel file there can occur events that should lead to some reaction of the app. That's why the download should trigger something. The alternative I can see is, to prepare the Excel file before the user clicks on download (what I would like to avoid, because this can take a few seconds depending on the complexity of the file/formatting).
Am I missing something obvious? If not, I'd appreciate any ideas, how the download event can trigger something in the rest of the app.

The solution is to remove the isolation of the reactiveValues as this prevents the counter from being updated until the numericInput is triggered. This is because data() is dependent on input$nrows.
library(shiny)
ui <- fluidPage(
numericInput("nRows", label = "nRows", min = 1, max = 32, value = 15),
downloadButton("downloadData", "Download"),
textOutput("nDownloads")
)
server <- function(input, output) {
r.nDownloads <- reactiveValues(n = 0)
# Our dataset
data <- reactive({
r.nDownloads$n <- r.nDownloads$n + 1
mtcars[1:input$nRows,]
})
output$nDownloads <- renderText({
paste("number of downloads:", r.nDownloads$n)
})
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(data(), file)
}
)
}
shinyApp(ui, server)
With regards to the deeper problem, it would be inefficient to constantly prepare a complex Excel file if there is no guarantee that the user would download the file. What you can try to do is:
Keep your data in a reactive method (e.g. data()).
Write a method to prep your data for downloading (e.g. prepExcel(data)) which returns your prepped data.
Pass (1) and (2) into the content of the downloadHandler() like this: write_xx(prepExcel(data())) or pipe the data into the write_xx function like this data() %>% prepExcel() %>% write_xx() where xx is the method used to output your final file e.g. write_xlsx or write_csv etc.
I hope this helps.

Related

Read json file continuously

I want to read a json file continuously, e.g. every 1000 ms.
One option my be reactiveFileReader
reactiveFileReader(intervalMillis, session, filePath, readFunc, ...)
described here.
This function seems only working with csv files and not for json files:
file_data <- reactiveFileReader(intervalMillis = 1000, NULL, filePath = json_path, readFunc = read.json)
observe({
View(file_data())
})
Error in View : object read.json not found
With reactivePoll like here:
getJsonData <- reactivePoll(1000, session,
checkFunc = function() {
if (file.exists(path))
file.info(path)$mtime[1]
else
""
},
valueFunc = function() {
read_json(path)
}
I get nearly what I want, but this function is not working in my context. How do I force the program to read the file every second and not only when the content of the file is changing?
Are there other possibilities I not have thought about yet?
In your first way, you wrote read.json instead of read_json.
With your second way, you could replace file.info(path)$mtime[1] with runif(1, 0, 1e6). You would be very unlucky if runif returns the same number two consecutive times.
Finally, a third way could be:
server <- function(input, output, session){
autoInvalidate <- reactiveTimer(1000)
getJsonData <- reactive({
autoInvalidate()
read_json("path/to/file.json")
})
}
Here is a reprex on how to use reactiveFileReader with a json file.
I used a future to detach the writing process from the shiny session - you can simply replace this with your json input.
library(shiny)
library(jsonlite)
library(datasets)
library(promises)
library(future)
plan(multisession(workers = 2))
ui <- fluidPage(
uiOutput("printResult")
)
server <- function(input, output, session) {
json_path <- tempfile(fileext = ".json")
write_json(NULL, json_path)
# async file writing process
future({
for(i in seq_len(nrow(iris))){
Sys.sleep(1)
write_json(iris[i,], json_path)
}
})
file_data <- reactiveFileReader(intervalMillis = 1000, NULL, filePath = json_path, readFunc = read_json)
output$printResult <- renderUI({
req(file_data())
})
}
shinyApp(ui, server)

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()
})
}

How to allow the user to input form multiple times before downloading file?

I would like a user to be able to answer the form multiple times before downloading the file, with each subsequent form added into the excel file.
I've considered a loop with the user able to input how many times through the loop, but I would prefer the user to be able to calculate multiple times, but only need to download the file once. I am not sure where to begin with this. I've also considered shiny modules, but I am not sure that would be the most effective way to complete this. I've included a simplified version:
library(shiny)
library(lubridate)
library(openxlsx)
ui <- fluidPage(
textInput("name","Name"),
dateInput("date","Birthdate"),
textInput("title","Title"),
fileInput("excelfile","Excel File"),
actionButton("calculate","Calculate"),
downloadButton("download","Download")
)
server <- function(input, output) {
createcolumns<-observeEvent(input$calculate,{
age<-year(Sys.Date())-year(input$date)
df<-data.frame("Name"=input$name,"Age"=age,"Title"=input$title)
wb<-loadWorkbook(input$excelfile$datapath)
writeData(wb,"Sheet1",df)
saveWorkbook(wb,input$excelfile$datapath,overwrite = TRUE)
})
output$download<-downloadHandler(
file = function(){
filename<-strsplit(input$excelfile$name,"\\.")
filename<-filename[[1]][1]
filename<-paste0(filename,"_",Sys.Date())
paste(filename,"xlsx",sep=".")
},
content = function(file){
file.rename(input$excelfile$datapath,file)
},
contentType = "application/xlsx"
)
}
# Run the app ----
shinyApp(ui = ui, server = server)
Ideally, the user could input multiple people at once visit, then once everyone was entered, download the completed excel file.
I was able to do this by adding a few two variables in the server function (globaldf, x) and moving the bulk of the work into an if statement that checks if the calculate button has increased since the last time.
library(shiny)
library(lubridate)
library(openxlsx)
ui <- fluidPage(
fluidRow(
column(6,
textInput("name","Name",value = 1),
dateInput("date","Birthdate"),
textInput("title","Title"),
fileInput("excelfile","Excel File"),
actionButton("calculate","Calculate"),
downloadButton("download","Download")
),
column(6,
h1("Output"),
tableOutput("data")
)
)
)
server <- function(input, output) {
globaldf<-data.frame("Name"=NULL,"Age"=NULL,"Title"=NULL)
x<-0
createcolumns<-reactive({
req(input$name,input$date,input$title,input$excelfile,input$calculate)
y<-input$calculate
if(x<y){
age<-year(Sys.Date())-year(input$date)
df<-data.frame("Name"=input$name,"Age"=age,"Title"=input$title)
globaldf<<-rbind(globaldf,df)
wb<-loadWorkbook(input$excelfile$datapath)
writeData(wb,"Sheet1",globaldf)
saveWorkbook(wb,input$excelfile$datapath,overwrite = TRUE)
x<<-y
globaldf
} else {return()}
})
output$data<-renderTable({
outputtable<-data.frame(createcolumns())
outputtable
})
output$download<-downloadHandler(
file = function(){
filename<-strsplit(input$excelfile$name,"\\.")
filename<-filename[[1]][1]
filename<-paste0(filename,"_",Sys.Date())
paste(filename,"xlsx",sep=".")
},
content = function(file){
file.rename(input$excelfile$datapath,file)
},
contentType = "application/xlsx"
)
}

R Shiny - Dynamic download link in datatable

I want to add a download link in each row of a datatable in shiny.
So far I have
server <- function(input, output) {
v<-eventReactive(input$button,{
temp<-data.frame(TBL.name=paste("Data ",1:10))
temp<-cbind(
temp,
#Dynamically create the download and action links
Attachments=sapply(seq(nrow(temp)),function(i){as.character(downloadLink(paste0("downloadData_",i),label = "Download Attachments"))})
)
})
# Table of selected dataset ----
output$table <- renderDataTable({
v()
}, escape = F)}
ui <- fluidPage(
sidebarPanel(
actionButton("button", "eventReactive")
),
mainPanel(
dataTableOutput("table")
)
)
I have the download links in the table for each row. Now I want to add a different file location for each row. For example, each download link will result in a download of a different zip-folder. Can I use downloadHandler for this?
I do not believe you can embed downloadButtons/downloadLinks directly in a datatable. However, you can create hidden downloadLinks that get triggered by links embedded in your table. This produces the same end result. To do so you must:
Dynamically generate downloadLinks/downloadButtons.
Use css to set their visibility to hidden.
Embed normal links/buttons in the table
Set the onClick field of these links to trigger the corresponding hidden downloadLink.
Here is code from an example using the mtcars dataset.
library(tidyverse)
library(shiny)
ui <- fluidPage(
tags$head(
tags$style(HTML("
.hiddenLink {
visibility: hidden;
}
"))
),
dataTableOutput("cars_table"),
uiOutput("hidden_downloads")
)
server <- function(input, output, session) {
data <- mtcars
lapply(1:nrow(data), function(i) {
output[[paste0("downloadData", i)]] <- downloadHandler(
filename = function() {
paste("data-", i, ".csv", sep="")
},
content = function(file) {
write.csv(data, file)
}
)
})
output$hidden_downloads <- renderUI(
lapply(1:nrow(data), function(i) {
downloadLink(paste0("downloadData", i), "download", class = "hiddenLink")
}
)
)
output$cars_table <- renderDataTable({
data %>%
mutate(link = lapply(1:n(),
function(i)
paste0('<a href="#" onClick=document.getElementById("downloadData',i, '").click() >Download</a>')
))
}, escape = F)
}
shinyApp(ui, server)
Since each downloadLink label must correspond to a name in output, I don't think there is a way to create an arbitrary set of downloads using the standard Shiny download* functions.
I solved this using DT and javascript. DT allows javascript to be associated with a datatable. The javascript can then tell Shiny to send a file to the client and the client can force the data to be downloaded.
I created a minimal example gist. Run in RStudio with:
runGist('b77ec1dc0031f2838f9dae08436efd35')
Safari is not supporting .click() anymore since v12.0. Hence, I adapted the hidden link solution from abanker with the dataTable/actionButton described by P Bucher, and the .click() workaround described here. Here is the final code:
library(shiny)
library(shinyjs)
library(DT)
# Random dataset
pName <- paste0("File", c(1:20))
shinyApp(
ui <- fluidPage( useShinyjs(),
DT::dataTableOutput("data"),
uiOutput("hidden_downloads") ),
server <- function(input, output) {
# Two clicks are necessary to make the download button to work
# Workaround: duplicating the first click
# 'fClicks' will track whether click is the first one
fClicks <- reactiveValues()
for(i in seq_len(length(pName)))
fClicks[[paste0("firstClick_",i)]] <- F
# Creating hidden Links
output$hidden_downloads <- renderUI(
lapply(seq_len(length(pName)), function(i) downloadLink(paste0("dButton_",i), label="")))
# Creating Download handlers (one for each button)
lapply(seq_len(length(pName)), function(i) {
output[[paste0("dButton_",i)]] <- downloadHandler(
filename = function() paste0("file_", i, ".csv"),
content = function(file) write.csv(c(1,2), file))
})
# Function to generate the Action buttons (or actionLink)
makeButtons <- function(len) {
inputs <- character(len)
for (i in seq_len(len)) inputs[i] <- as.character(
actionButton(inputId = paste0("aButton_", i),
label = "Download",
onclick = 'Shiny.onInputChange(\"selected_button\", this.id, {priority: \"event\"})'))
inputs
}
# Creating table with Action buttons
df <- reactiveValues(data=data.frame(Name=pName,
Actions=makeButtons(length(pName)),
row.names=seq_len(length(pName))))
output$data <- DT::renderDataTable(df$data, server=F, escape=F, selection='none')
# Triggered by the action button
observeEvent(input$selected_button, {
i <- as.numeric(strsplit(input$selected_button, "_")[[1]][2])
shinyjs::runjs(paste0("document.getElementById('aButton_",i,"').addEventListener('click',function(){",
"setTimeout(function(){document.getElementById('dButton_",i,"').click();},0)});"))
# Duplicating the first click
if(!fClicks[[paste0("firstClick_",i)]])
{
click(paste0('aButton_', i))
fClicks[[paste0("firstClick_",i)]] <- T
}
})
}
)

download count in shinyapps.io

I have set up a shiny app that knitr's markdown PDF's based on selceted data. The generated report needs to have some sort of unique ID to be referenced in meetings.
I understand that I can use the session ID but I need a way to count the downloads per session.
Is there a way of counting events outside sessions or counting the number of session ID's since first deployment?
I needed to do that once, so I had some code lying around. It basically kept track of everything in a csv that I appended to. Here I built it into a shiny test platform.
it uses the session$token for the id (maybe there is something better)
it uses write.table and read.table because they behave better with the append option.
it increments the count by calling out to writetolog in the shiny download handler but you can also increment the count manually with an extra button (which is only for test purposes obviously)
It has two output, one is a summary of the log, the other is a dump of what is in the log. These are for debugging as the downloadHandler can be a bit "challenging" at times when interacting with all this reactivity.
Here is that code modified to be an example like about what you need:
library(shiny)
logfname <- "log.csv"
writetolog <- function(newcount,newsessionid,operation){
time <- format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")
df <- data.frame(time=time,count=newcount,sessionid=newsessionid,operation=operation)
doappend <- file.exists(logfname)
if (doappend){
write.table(df,logfname,append=T,quote=F,col.names=F,sep=",",row.names=F)
} else {
write.table(df,logfname,append=F,quote=F,sep=",",row.names=F)
}
}
getcounts <- function(){
if (!file.exists(logfname)){
return(list(count=0,sessioncount=0))
}
df <- read.table(logfname,header=T,sep=",")
nr <- nrow(df)
rlst <- list(count=sum(df$count),sessioncount=length(unique(df$sessionid)),
lastop=df$operation[nr],lasttime=df$time[nr])
return(rlst)
}
ui <- fluidPage(
titlePanel("Keep a download log"),
sidebarLayout(
sidebarPanel(
actionButton("inccount","Increment Count"),
actionButton("getcount","Refresh Summary"),
actionButton("showlog","Show Log"),
downloadButton("dodownload", "Save to .csv")
),
mainPanel(
h2("Summary of Download Log"),
verbatimTextOutput("showcount"),
h2("Dump of Download Log"),
verbatimTextOutput("loglog")
)
)
)
server <- function(input, output,session) {
observeEvent(input$inccount,{
print("writetolog")
writetolog(1,session$token,"inc count")
})
output$showcount <- renderPrint({
input$getcount
rv <- getcounts()
time <- format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")
print(sprintf("%s - count:%d sessioncount:%d",time,rv$count,rv$sessioncount))
})
output$loglog <- renderPrint({
input$showlog
if (!file.exists(logfname)) return(NULL)
ldf <- read.csv(logfname)
print(ldf)
})
output$dodownload<-downloadHandler(
filename = function() {
paste(input$table_name, '.csv', sep='')
},
content = function(file) {
write.csv(mtcars, file)
writetolog(1,session$token,"save file")
}
)
}
shinyApp(ui = ui, server = server)
Screen shot:

Resources