How to show a loading screen when the output is being calculated in a background process?
Regarding your first concern: this approach won't block other sessions. However, the polling via invalidateLater()
will create some load.
A great library to look at in this context is ipc and its introductory vignette.
Regarding the second issue: There is a simple fix for this behaviour. We can use req
and its cancelOutput
parameter - see ?req
:
cancelOutput: If TRUE and an output is being evaluated, stop
processing as usual but instead of clearing the output, leave it in
whatever state it happens to be in.
library(shiny)
library(uuid)
library(ggplot2)
library(waiter)
ui <- fluidPage(
useWaiter(),
titlePanel("Test background job"),
actionButton("start","Start Job"),
actionButton("stop", "Stop job"),
plotOutput("plot")
)
# the toy example job
slow_func <- function(var){
library(ggplot2)
Sys.sleep(5)
ggplot(mtcars, aes(drat, !!sym(var))) +
geom_point()
}
server <- function(input, output, session) {
w <- Waiter$new(id = "plot")
token <- reactiveValues(var = NULL, id = NULL, last_id = NULL)
jobs <- reactiveValues()
# When I press "start", run the slow function and append the output to
# the list of jobs. To render the plot, check if the background process is
# finished. If it's not, re-check one second later.
long_run <- eventReactive(input$start, {
token$var <- c(token$var, sample(names(mtcars), 1))
token$id <- c(token$id, UUIDgenerate())
token$last_id <- token$id[[length(token$id)]]
message(paste0("running task with id: ", token$last_id))
jobs[[token$last_id]] <- callr::r_bg(
func = slow_func,
args = list(var = token$var[[length(token$var)]])
)
return(jobs[[token$last_id]])
})
observeEvent(input$start, {
output$plot <- renderPlot({
w$show()
if (long_run()$poll_io(0)["process"] == "timeout") {
invalidateLater(1000)
req(FALSE, cancelOutput = TRUE)
} else {
jobs[[token$last_id]]$get_result()
}
})
})
# When I press "stop", kill the last process, remove it from the list of
# jobs (because it didn't produce any output so it is useless), and display
# the last process (which by definition is the last plot produced)
observeEvent(input$stop, {
if (length(token$id) > 0) {
jobs[[token$last_id]]$kill()
message(paste0("task ", token$last_id, " stopped"))
token$id <- token$id[-length(token$id)]
if (length(token$id) > 0) {
token$last_id <- token$id[[length(token$id)]]
}
}
output$plot <- renderPlot({
if (length(token$id) > 0) {
print(token$last_id)
jobs[[token$last_id]]$get_result()
} else {
return(NULL)
}
})
})
}
shinyApp(ui = ui, server = server)
running background process in R Shiny
As explained in the examples here, wait()
means that we have to wait for the result of the background job before continuing the rest of the processes.
One way to keep the clock updating while running the background job is to use poll_io()
to check whether the job is finished (note that it is better to use poll_io()
than is_alive()
, as explained in this Github comment). I have done something similar in this question, although the general app is a bit more complicated.
Here's what you need to modify in the server:
observeEvent(input$startjob, {
global$result <- r_bg(func = printLS, supervise = F)
})
output$LS <- renderUI({
req(input$startjob)
if (global$result$poll_io(0)["process"] == "timeout") {
invalidateLater(1000)
} else {
p(HTML(paste(global$result$get_result(), sep = "", collapse = "<br>")))
}
})
Note that system2(command = "ls", args = c("-l"), stdout = TRUE, stderr = TRUE)
didn't work on my laptop so I used system("ls -l")
instead.
R shiny: display loading... message while function is running
I'm already using a simpler and more reliable way than the one I posted before.
A combination of
tags$style(type="text/css", "
#loadmessage {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
padding: 5px 0px 5px 0px;
text-align: center;
font-weight: bold;
font-size: 100%;
color: #000000;
background-color: #CCFF66;
z-index: 105;
}
")
with
conditionalPanel(condition="$('html').hasClass('shiny-busy')",
tags$div("Loading...",id="loadmessage")
)
Example:
runApp(list(
ui = pageWithSidebar(
headerPanel("Test"),
sidebarPanel(
tags$head(tags$style(type="text/css", "
#loadmessage {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
padding: 5px 0px 5px 0px;
text-align: center;
font-weight: bold;
font-size: 100%;
color: #000000;
background-color: #CCFF66;
z-index: 105;
}
")),
numericInput('n', 'Number of obs', 100),
conditionalPanel(condition="$('html').hasClass('shiny-busy')",
tags$div("Loading...",id="loadmessage"))
),
mainPanel(plotOutput('plot'))
),
server = function(input, output) {
output$plot <- renderPlot({ Sys.sleep(2); hist(runif(input$n)) })
}
))
tags$head() is not required, but it's a good practice to keep all the styles inside head tag.
How to show a shiny modal only if a certain operation takes longer than expected?
Here is an example on what I've outlined in the comments. However, for a delay time of only 0.5s to show the modal, the overhead to create a background process might be too much.
library(shiny)
library(shinyjs)
library(callr)
ui <- fluidPage(
useShinyjs(),
actionButton("saveButton", "save"),
)
server <- function(input, output, session) {
rv <- reactiveValues(bg_process = NULL, retry_count = 0)
observeEvent(input$saveButton, {
disable("saveButton")
unknown_duration <- round(runif(1L, max = 2L), digits = 1)
print(paste0(Sys.time(), " - unknown process duration: ", unknown_duration, "s"))
rv$bg_process <- r_bg(
func = function(duration){
Sys.sleep(duration)
return("result")
},
args = list(duration = unknown_duration))
})
observe({
req(rv$bg_process)
if (rv$bg_process$poll_io(0)["process"] == "ready") {
print(paste(Sys.time(), "-", rv$bg_process$get_result()))
rv$retry_count <- 0
enable("saveButton")
removeModal()
if(rv$bg_process$is_alive() == FALSE){
rv$bg_process <- NULL # reset
}
} else {
invalidateLater(1000)
if(isolate({rv$retry_count}) == 3L){
print(paste(Sys.time(), "- showModal"))
showModal(modalDialog("Saving..."))
} else {
print(paste(Sys.time(), "- waiting"))
}
}
isolate({rv$retry_count <- rv$retry_count + 1})
})
}
shinyApp(ui, server)
Displaying something in kivy while a process is running background
Just found the solution thanks to this thread
you have to use the kivy Clock.schedule_once()
method : kivy clock doc
I my case, I just create a function fermerbanc_schedule in charge of :
- Displaying the waiting message on the screen
- called the fermerBanc function via
schedule_once
def fermerBanc_schedule(self, *args):
self.msgWarning = Label(text="Merci de patienter...", font_size='55sp', size=(100, 50), pos_hint={'center_x': .5, 'center_y':.5})
self.add_widget(self.msgWarning)
Clock.schedule_once(lambda dt: self.fermerBanc(self, *args), 0)
Display a loading bar before the entire page is loaded
Use a div #overlay
with your loading info / .gif that will cover all your page:
<div id="overlay">
<img src="loading.gif" alt="Loading" />
Loading...
</div>
jQuery:
$(window).load(function(){
// PAGE IS FULLY LOADED
// FADE OUT YOUR OVERLAYING DIV
$('#overlay').fadeOut();
});
Here's an example with a Loading bar:
jsBin demo
;(function(){
function id(v){ return document.getElementById(v); }
function loadbar() {
var ovrl = id("overlay"),
prog = id("progress"),
stat = id("progstat"),
img = document.images,
c = 0,
tot = img.length;
if(tot == 0) return doneLoading();
function imgLoaded(){
c += 1;
var perc = ((100/tot*c) << 0) +"%";
prog.style.width = perc;
stat.innerHTML = "Loading "+ perc;
if(c===tot) return doneLoading();
}
function doneLoading(){
ovrl.style.opacity = 0;
setTimeout(function(){
ovrl.style.display = "none";
}, 1200);
}
for(var i=0; i<tot; i++) {
var tImg = new Image();
tImg.onload = imgLoaded;
tImg.onerror = imgLoaded;
tImg.src = img[i].src;
}
}
document.addEventListener('DOMContentLoaded', loadbar, false);
}());
*{margin:0;}
body{ font: 200 16px/1 sans-serif; }
img{ width:32.2%; }
#overlay{
position:fixed;
z-index:99999;
top:0;
left:0;
bottom:0;
right:0;
background:rgba(0,0,0,0.9);
transition: 1s 0.4s;
}
#progress{
height:1px;
background:#fff;
position:absolute;
width:0; /* will be increased by JS */
top:50%;
}
#progstat{
font-size:0.7em;
letter-spacing: 3px;
position:absolute;
top:50%;
margin-top:-40px;
width:100%;
text-align:center;
color:#fff;
}
<div id="overlay">
<div id="progstat"></div>
<div id="progress"></div>
</div>
<div id="container">
<img src="http://placehold.it/3000x3000/cf5">
</div>
Displaying stdout of a background process in specific location of the terminal
This is a simple solution if you're willing to accept output just above the current prompt line.
bufferout () {
local buffer
while IFS= read -r line; do # buffer stdin
buffer="$buffer$line\n"
done
print -rn -- $terminfo[dl1] # delete current line
printf "$buffer" # print buffer
kill -USR1 $$ # send USR1 when done
}
TRAPUSR1 () { # USR1 signal handler
zle -I # invalidate prompt
unhash -f TRAPUSR1 bufferout # remove ourselves
}
./testout 2>&1 | bufferout &! # run in background, disowned
When the job is done, the current prompt and input buffer will be removed and the entirety of the command's stdout
and stderr
will be printed.
Note that this assumes it will be run exactly once and removes itself afterwards. If you want to keep reusing this functionality in the same shell, remove the unhash -f
line in TRAPUSR1
.
This answer includes an improvement suggested by Clint Priest in the comments. Thanks!
Related Topics
Organize Text on Geom_Point Using Geom_Text
How to Select All Factor Variables in R
R: Clustering Results Are Different Everytime I Run
Update an Entire Row in Data.Table in R
How to Load CSV File into Sparkr on Rstudio
How to Plot Histogram/ Frequency-Count of a Vector with Ggplot
Equal Frequency Discretization in R
How to Use R Package "Formattable" in Shiny Dashboard
Let Each Plot in Facet_Grid Have Its Own Y-Axis Value
Combine Result from Top_N with an "Other" Category in Dplyr
Rmarkdown Table with Cells That Have Two Values
Dplyr Pipes - How to Change the Original Dataframe
Data.Table Join and J-Expression Unexpected Behavior
How to Plot X-Axis Labels and Bars Between Tick Marks in Ggplot2 Bar Plot
Plot Emojis/Emoticons in R with Ggplot
R: Row-Wise Dplyr::Mutate Using Function That Takes a Data Frame Row and Returns an Integer