## Data creation
##
## Create a large Excel spreadsheet within a Shiny app
##
library(shiny)
library(openxlsx)
## Create a dummy matrix
<- function(input, output, session) {
server
<- reactiveValues(wb = NULL)
mydata <- reactiveValues(text = 'Waiting')
status
## Disable download button
observe({
$sendCustomMessage('disableButton', 'butDownload')
session
})
## Generate Excel output
observeEvent(input$butCreate, {
$sendCustomMessage('disableButton', 'butDownload')
session$sendCustomMessage('disableButton', 'butCreate')
session
## Included for comparison - the status text will not update until after the spreadsheet is built
$text <- 'Building'
status
<- matrix(rexp(input$numRows * input$numCols, rate = 0.1), ncol = input$numCols)
m
<- createWorkbook()
wb addWorksheet(wb, 'sheet1')
writeData(wb, 'sheet1', m)
$wb <<- wb
mydata$sendCustomMessage('enableButton', 'butDownload')
session$sendCustomMessage('enableButton', 'butCreate')
session$text <- 'Completed'
status
})
$butDownload <- downloadHandler(
outputfilename = function() {
'output.xlsx'
},content = function(file) {
showNotification('Writing Excel File')
saveWorkbook(mydata$wb, file, overwrite = TRUE)
}
)
$uiStatus <- renderUI(
outputh4(paste0('STATUS: ', status$text), style="color:red;")
)
$plt <- renderPlot({
outputhist(faithful$eruptions, probability = TRUE, breaks = as.numeric(input$n_breaks),
xlab = 'Duration (minutes)', main = 'Geyser eruption duration')
<- density(faithful$eruptions, adjust = input$bw_adjust)
dens lines(dens, col = 'blue')
})
}
<- fluidPage(
ui singleton(tags$head(HTML('
<script type="text/javascript">
$(document).ready(function() {
// Enable button
Shiny.addCustomMessageHandler("enableButton", function(id) {
$("#" + id).removeAttr("disabled");
});
// Disable button
Shiny.addCustomMessageHandler("disableButton", function(id) {
$("#" + id).attr("disabled", "true");
});
})
</script>
')
)),fluidRow(
column(4,
wellPanel(
fluidRow(
column(6, numericInput('numRows', 'Number of Rows', value = 100000, min = 1000, step = 1000)),
column(6, numericInput('numCols', 'Number of Columns', value = 50, min = 10, step = 10))
)
),fluidRow(
column(11, offset = 1,
actionButton('butCreate', 'Create Data', class = 'btn action-button btn-success'),
downloadButton('butDownload', 'Download Data', class = 'btn btn-warning'),
br(),
uiOutput('uiStatus')
)
)
),column(8,
wellPanel(
fluidRow(
column(4, selectInput('n_breaks', label = 'Number of bins:', choices = c(10, 20, 35, 50), selected = 20)),
column(4, sliderInput('bw_adjust', label = 'Bandwidth adjustment:', min = 0.2, max = 2, value = 1, step = 0.2))
)
),plotOutput('plt')
)
)
)
shinyApp(ui = ui, server = server)
R is a very powerful language but was designed to run single-threaded. There are libraries that will run parallel code but there is no inherent support to run a background process and monitor for completion in the same way that you can in, for example, C++.
One way to overcome this is to launch a second instance of R and run the process in the background. If the background process generates a log file it can be monitored within Shiny using the reactivePoll or reactiveFileInput functions. Once completed subsequent action can be taken. The advantage of this method is that the second process is run under a second instance of R and does not interfere with the user interface of the Shiny app.
By way of example, there are two apps below. The first attempts to run the process within an observer which ties up the UI – the UI will only respond to changes once the calculation is completed. The second launches a second instance and the UI is not affected – the UI is fully responsive during the calculation.
Running as a Single Process
The gist for the code can be found at https://gist.github.com/harveyl888/fa6ff9823b9c5a5fff11c946d8e7c9f5
Running as an Asynchronous Process
The gist for the code can be found at https://gist.github.com/harveyl888/bf05d902b10c138a02acd5c9c65fc5da
## Data creation
##
## Create a large Excel spreadsheet as a asynchronous process
##
library(shiny)
## Temporary files to store log, script, rds data and excel output
<- tempfile()
logfile <- tempfile()
scriptfile <- tempfile()
datafile <- tempfile()
excelfile
<- function(input, output, session) {
server
<- reactiveValues(text = 'Waiting')
status
## Disable download button
observe({
$sendCustomMessage('disableButton', 'butDownload')
session
})
# reactivePoll - look for changes in log file every second
<- reactivePoll(1000, session,
logData checkFunc = function() {
if (file.exists(logfile))
file.info(logfile)$mtime[1]
else
''
},valueFunc = function() {
if (file.exists(logfile))
readLines(logfile)
else
''
}
)
## React to an update in the logfile
observe({
if (grepl('COMPLETED', logData())) {
$sendCustomMessage('enableButton', 'butDownload')
session$sendCustomMessage('enableButton', 'butCreate')
session$text <- 'Completed'
status
}
})
## Generate Excel output
## Once button is pressed create an R Script and run as a second process
## to avoid tying up Shiny
observeEvent(input$butCreate, {
$sendCustomMessage('disableButton', 'butDownload')
session$sendCustomMessage('disableButton', 'butCreate')
session$text <- 'Building'
status
<- matrix(rexp(input$numRows * input$numCols, rate = 0.1), ncol = input$numCols)
m
## Write data to an rds file
saveRDS(m, file = datafile)
## Create script file
<- c('library(openxlsx)',
vfile paste0('m <- readRDS(\"', datafile, '\")'),
'wb <- createWorkbook()',
'addWorksheet(wb, \"sheet1\")',
'writeData(wb, \"sheet1\", m)',
paste0('saveWorkbook(wb, \"', excelfile, '\", overwrite = TRUE)'),
paste0('fileConn <- file(\"', logfile, '\")'),
'writeLines(\"COMPLETED\", fileConn)',
'close(fileConn)'
)
## Save script file
<- file(scriptfile)
fileConn writeLines(vfile, fileConn)
close(fileConn)
## Execute script file
system(paste0(Sys.getenv('R_HOME'), '/bin/Rscript ', scriptfile), wait = FALSE)
})
$butDownload <- downloadHandler(
output<- function() {
filename 'excel-out.xlsx'
},<- function(file) {
content file.copy(excelfile, file)
}
)
$uiStatus <- renderUI(
outputh4(paste0('STATUS: ', status$text), style="color:red;")
)
$plt <- renderPlot({
outputhist(faithful$eruptions, probability = TRUE, breaks = as.numeric(input$n_breaks),
xlab = 'Duration (minutes)', main = 'Geyser eruption duration')
<- density(faithful$eruptions, adjust = input$bw_adjust)
dens lines(dens, col = 'blue')
})
}
<- fluidPage(
ui singleton(tags$head(HTML('
<script type="text/javascript">
$(document).ready(function() {
// Enable button
Shiny.addCustomMessageHandler("enableButton", function(id) {
$("#" + id).removeAttr("disabled");
});
// Disable button
Shiny.addCustomMessageHandler("disableButton", function(id) {
$("#" + id).attr("disabled", "true");
});
})
</script>
')
)),fluidRow(
column(4,
wellPanel(
fluidRow(
column(6, numericInput('numRows', 'Number of Rows', value = 100000, min = 1000, step = 1000)),
column(6, numericInput('numCols', 'Number of Columns', value = 50, min = 10, step = 10))
)
),fluidRow(
column(11, offset = 1,
actionButton('butCreate', 'Create Data', class = 'btn action-button btn-success'),
downloadButton('butDownload', 'Download Data', class = 'btn btn-warning'),
br(),
uiOutput('uiStatus')
)
)
),column(8,
wellPanel(
fluidRow(
column(4, selectInput('n_breaks', label = 'Number of bins:', choices = c(10, 20, 35, 50), selected = 20)),
column(4, sliderInput('bw_adjust', label = 'Bandwidth adjustment:', min = 0.2, max = 2, value = 1, step = 0.2))
)
),plotOutput('plt')
)
)
)
shinyApp(ui = ui, server = server)