How to set up an independent progress bar - r

I'm trying to include a progress bar during the computations in my shiny application. Description of my problem:
My computation takes a while, like 30 seconds
I'm able to evaluate in advance the exact time a computation will take
however, the computation is in one chunk, not splitable in small parts that I could use to manually increment the progress bar, think of it as a large model fitting process.
Currently there are some questions related to the problem but no satisfying answer:
here, here for instance.
Is there a way to implement an bar that progresses on top of a calculation, independently and continuously, for a fixed amount of time (or maybe insert an animation of the bar in a pop-up that mimics the bar?)
Thanks
Edit: I tried to mimic a progress bar with an animated sliderInput, but I couldn't find how programmatically trigger the animation...

I think this would be a lot easier when Shiny releases its async support. But for now, it'd have to be a custom, client-side JavaScript solution.
My take on it uses the same Bootstrap 3 progress bars that Shiny uses. Out of laziness, I also leveraged Shiny's progress bar CSS classes (top bar style), so this will conflict with Shiny's progress bars. Ideally it'd be a widget with its own styles.
I used jQuery's animate to set the width of the progress bar over a fixed duration. animate has some nice options out of the box like easing. I also let the progress bar linger after 100% by default, thinking it'd be better for the server to explicitly close the progress bar in case the timing isn't exact.
library(shiny)
progressBarTimer <- function(top = TRUE) {
progressBar <- div(
class = "progress progress-striped active",
# disable Bootstrap's transitions so we can use jQuery.animate
div(class = "progress-bar", style = "-webkit-transition: none !important;
transition: none !important;")
)
containerClass <- "progress-timer-container"
if (top) {
progressBar <- div(class = "shiny-progress", progressBar)
containerClass <- paste(containerClass, "shiny-progress-container")
}
tagList(
tags$head(
tags$script(HTML("
$(function() {
Shiny.addCustomMessageHandler('progress-timer-start', function(message) {
var $progress = $('.progress-timer-container');
var $bar = $progress.find('.progress-bar');
$bar.css('width', '0%');
$progress.show();
$bar.animate({ width: '100%' }, {
duration: message.duration,
easing: message.easing,
complete: function() {
if (message.autoClose) $progress.fadeOut();
}
});
});
Shiny.addCustomMessageHandler('progress-timer-close', function(message) {
var $progress = $('.progress-timer-container');
$progress.fadeOut();
});
});
"))
),
div(class = containerClass, style = "display: none;", progressBar)
)
}
startProgressTimer <- function(durationMsecs = 2000, easing = c("swing", "linear"),
autoClose = FALSE, session = getDefaultReactiveDomain()) {
easing <- match.arg(easing)
session$sendCustomMessage("progress-timer-start", list(
duration = durationMsecs,
easing = easing,
autoClose = autoClose
))
}
closeProgressTimer <- function(session = getDefaultReactiveDomain()) {
session$sendCustomMessage("progress-timer-close", list())
}
ui <- fluidPage(
numericInput("seconds", "how many seconds your calculation will last?", value = 6),
progressBarTimer(top = TRUE),
actionButton("go", "Compute")
)
server <- function(input, output, session) {
observeEvent(input$go, {
startProgressTimer(input$seconds * 1000, easing = "swing")
Sys.sleep(input$seconds) # simulate computation
closeProgressTimer()
showNotification("Computation finished!", type = "error")
})
}
shinyApp(ui, server)

Not a complete answer, since my suggestion would be to use progress bars, but I hope it helps a bit.
Here's a way to trigger clicking a slider animate button using some javascript with the shinyjs package:
library(shiny)
library(shinyjs)
jscode <- "
shinyjs.play = function() {
$('.slider-animate-button').trigger('click');
}
"
ui <- fluidPage(
useShinyjs(),
extendShinyjs(text = jscode),
sliderInput("slider", label = "", width = '600px',
min = 0,
max = 20,
value = 0,
step = 1,
animate = animationOptions(
interval = 100,
playButton = "Play",
pauseButton = "Pause"
)
)
)
server <- function(input, output,session) {
observe( {
js$play()
})
}
shinyApp(ui, server)
Please note that the js code references the slider-animate-button class, so it will trigger every slider animation option in the app.

Thanks to the answer of #GyD, I now propose an improved solution (that has something of a hack I admit).
The long computation is simulated here by a sys.sleep of the desired duration. You see that there is still slider movement during the 'sleep'. I put the animated slider into a RenderUI so we can control the speed:
library(shiny); library(shinyjs); library(shinyWidgets)
jscode <- "
shinyjs.play = function() {
$('.slider-animate-button').trigger('click');
}
"
ui <- fluidPage(
tags$head(tags$style(HTML('.irs-from, .irs-to, .irs-min, .irs-max, .irs-grid-text, .irs-grid-pol, .irs-slider {visibility:hidden !important;}'))),
useShinyjs(), extendShinyjs(text = jscode),
numericInput("seconds", "how many seconds your calculation will last?", value=6),
uiOutput("UI"),
actionButton("go", "Compute"))
server <- function(input, output,session) {
disable("slider")
observeEvent(input$go, priority=10, {
js$play()
Sys.sleep(input$seconds) # simulate computation
showNotification("Computation finished!", type="error")})
output$UI = renderUI({
sliderInput("slider", label = "", width = '300px',min = 0,max = 100,value = 0,step = 1,
post="% done",
animate = animationOptions(
interval = (as.numeric(input$seconds)*8),
playButton = "",
pauseButton = ""))})}
shinyApp(ui, server)
The slider really looks like a bar, doesn't it?

Related

How to conditionally play an audio clip in R Shiny?

Goal
Play an audio clip for navigation whenever the reactive value of position is in a certain range
What I tried
Based on other questions, I found that a button can be used to play an audio clip. For example:
An example with action button
library(shiny)
addResourcePath("Music", "Music")
audio_file1 <- "Music/in 200 m turn left.mp3"
ui <- fluidPage(
basicPage(
actionButton("play", "Play the Audio")
)
)
server <- function(input, output, session) {
observeEvent(input$play, {
insertUI(selector = "#play",
where = "afterEnd",
ui = tags$audio(src = audio_file1, type = "audio/mp3", autoplay = NA, controls = NA, style="display:none;")
)
})
}
shinyApp(ui, server)
My modifications without an action button
I want to autoplay the audio clip whenever the position x is a certain value. For keeping the question short, I am providing x inside server in this example:
server <- function(input, output, session) {
x <- 1
observeEvent(if (x==1){return(x)}, {
insertUI(selector = "#play",
where = "afterEnd",
ui = tags$audio(src = audio_file1, type = "audio/mp3", autoplay = TRUE, controls = NA, style="display:none;")
)
})
}
However, that threw an error:
Warning: Error in eval_tidy: no function to return from, jumping to top level
[No stack trace available]
I have also separately tried using the uiOutput and renderUI as follows (server part):
output$audioo <- renderUI({
if (x > 0.5 & x < 1.5) {
tags$audio(src = audio_file_200_TL, type = "audio/mp3", autoplay = TRUE, controls = NA)
} else {
tags$h1("My header")
}
})
But that plays the audio when the app is just launched and the audio does not play when x is in the provided range. How can I conditionally play an audio clip without using an action button?
So x is not an input nor a reactive. We need this as R process has to know the time to rerun the cycle. There are other hardcore solution which I am not recommending like shiny::invalidateLater().
Try sth like:
x_r <- reactive(x)
observeEvent(x_r(), {
if (x_r() == 1) {
insertUI(selector = "#play",
where = "afterEnd",
ui = tags$audio(src = audio_file1, type = "audio/mp3", autoplay = TRUE, controls = NA, style="display:none;"), immediate = TRUE
)
}
})
Btw be very careful with insertUI, I do not like to use it as we might add the same element many times. More elegant strategy for me is to add the element by default and later only edit in the DOM, hide/show/edit. we could use shinyjs package for such action. insertUI often need immediate = TRUE argument.
EDIT:
This app is working perfectly for me. The question is what is the x in your app. mp3 taken from https://samplelib.com/sample-mp3.html.
library(shiny)
addResourcePath("Music", "Music")
audio_file1 <- "Music/sample-3s.mp3"
ui <- fluidPage(
basicPage(
tags$div(id = "AUDIO_MY"),
selectInput("STH", "STH", 1:10, 1)
)
)
server <- function(input, output, session) {
observeEvent(req(input$STH == "1"), {
insertUI(selector = "#AUDIO_MY",
where = "afterEnd",
ui = tags$audio(src = audio_file1, type = "audio/mp3", autoplay = NA, controls = NA, style="display:none;")
, immediate = TRUE)
})
}

Forcing updates to htmlOutput within a long running function

I have a Shiny application that runs a long process and I would like to alert the user that the process is actually running. In the example below, I have a toggle switch that executes a block of code with a 1 second delay (my actual application runs for about 20 seconds), and I have an HTMLoutput box that should let the user know something is happening. But, since the underlying bootstrap process only updates the UI elements after the function exits, users only see the last message, "Done".
I've seen other questions like this one with answers that suggest creating a reactive value and then wrapping the renderUI() function in an observe() function (here, for example), but this has the same problem.
I also tried wrapping the htmlOutput() in withSpinner() from the shinycssloaders package, but I get an error saying "missing value where TRUE/FALSE needed". I assume this is coming from shinydashboardPlus because it doesn't like the withspinner() output in the tagList() elements. I was hopeful that this would at least give me an animated spinner on the HTMLoutput indicating that it is running.
Any input on getting this specific setup to work or alternatives to give users some feedback that the process is active is greatly appreciated.
library(shiny)
library(shinycssloaders)
library(shinydashboard)
library(shinydashboardPlus)
library(shinyWidgets)
# Define UI for application that draws a histogram
ui <- dashboardPage(skin = 'blue',
shinydashboardPlus::dashboardHeader(title = 'Example',
leftUi = tagList(
switchInput(inputId = 'swtLabels', label = 'Labels', value = TRUE,
onLabel = 'Label 1', offLabel = 'Label 2',
onStatus = 'info', offStatus = 'info', size = 'mini',
handleWidth = 230),
htmlOutput(outputId = 'labelMessage')
#withSpinner(htmlOutput(outputId = 'labelMessage')) # leads to error noted in text
)
),
dashboardSidebar(),
dashboardBody()
)
server <- function(input, output) {
rv <- reactiveValues()
rv$labelMessage <- 'Start'
observeEvent(input$swtLabels, {
rv$labelMessage <- 'Updating labels...'
Sys.sleep(1)
rv$labelMessage <- 'Done'
})
output$labelMessage <- renderUI(HTML(rv$labelMessage))
}
# Run the application
shinyApp(ui = ui, server = server)
I found a workaround for this using the shinyjs package, code below. The take home message is that by using shinjs::html(), the effect on the htmlOutput is immediate. I even added a fancy fade out at the end to hide the message.
It does create yet another package dependency, but it solves the problem. I'm sure there is a way that one could write a small JavaScript function and add it to the Shiny application to accomplish this same result. Unfortunately, I don't know JavaScript. (References for including JS code in a Shiny app - JavaScript Events in Shiny, Add JavaScript and CSS in Shiny)
library(shiny)
library(shinycssloaders)
library(shinydashboard)
library(shinyjs)
library(shinydashboardPlus)
library(shinyWidgets)
# Define UI for application that draws a histogram
ui <- dashboardPage(skin = 'blue',
shinydashboardPlus::dashboardHeader(title = 'Example',
leftUi = tagList(
useShinyjs(),
switchInput(inputId = 'swtLabels', label = 'Labels', value = TRUE,
onLabel = 'Label 1', offLabel = 'Label 2',
onStatus = 'info', offStatus = 'info', size = 'mini',
handleWidth = 230),
htmlOutput(outputId = 'labelMessage')
#withSpinner(htmlOutput(outputId = 'labelMessage')) # leads to error noted in text
)
),
dashboardSidebar(),
dashboardBody()
)
server <- function(input, output) {
observeEvent(input$swtLabels, {
shinyjs::html(id = 'labelMessage', html = 'Starting...')
shinyjs::showElement(id = 'labelMessage')
Sys.sleep(1)
shinyjs::html(id = 'labelMessage', html = 'Done')
shinyjs::hideElement(id = 'labelMessage', anim = TRUE, animType = 'fade', time = 2.0)
})
}
# Run the application
shinyApp(ui = ui, server = server)

R Shiny - How to remove flickering when using SliderInput to animate static images?

I am building a shiny dashboard and plan to use SliderInput to animate a set of exisiting pngs. To do so, in the UI I have:
tabItem(tabName = 'Image',
fluidRow(
box(title = "", status="primary",solidHeader = F,width = 9,
uiOutput("animate_img"),
tags$style(type="text/css", "recalculating { opacity: 1.0 !important; }") # NOT WORKING
),
box(
title = "Options", status="info",solidHeader = TRUE,width = 3,
sliderInput("dates_img",
"Dates:",
min = as.Date("2017-01-01","%Y-%m-%d"),
max = as.Date("2018-12-31","%Y-%m-%d"),
value=as.Date("2017-01-01"),
timeFormat="%Y-%m-%d",
animate=animationOptions(interval=1000, loop = TRUE))
)
)
)
and in the server I have:
output$animate_img <- renderUI({
y <- year(input$dates_img)
d <- yday(input$dates_img)
filename <- sprintf("img_%d_%d.png",d,y)
tags$img(src = filename, width="100%")
})
While this code works to display the images, when I use the "play" button on the sliderInput to animate the images, there is flickering as each image loads. I would like to have a smooth animation if possible.
As suggested here, I have tried adding tags$style(type="text/css", "recalculating { opacity: 1.0 !important; }") to the UI, but this does not work.
Any recommendations for how to prevent the images from flickering as the animation plays? Thank you!
I was able to get it to work without any flickering by simply adjusting how the CSS is included in the rendered HTML. I used shinyjs::inlineCSS in my example, but the same could be done via sourcing an external stylesheet .css file with tags$head and tags$script or via includeCSS, etc. The key is to have the CSS loaded into the full HTML document's head (can verify via browser DevTools):
library(shiny)
library(shinydashboard)
library(shinyjs)
library(lubridate)
ui <- fluidPage(
shinyjs::inlineCSS(
"recalculating { opacity: 1.0 !important; }"
),
fluidRow(
box(title = "",
status = "primary",
solidHeader = F,
width = 9,
uiOutput("animate_img")
),
box(
title = "Options",
status = "info",
solidHeader = TRUE,
width = 3,
sliderInput("dates_img",
"Dates:",
min = as.Date("2017-01-01","%Y-%m-%d"),
max = as.Date("2018-12-31","%Y-%m-%d"),
value = as.Date("2017-01-01"),
timeFormat = "%Y-%m-%d",
animate = animationOptions(interval = 1000, loop = TRUE))
)
)
)
server <- function(input, output) {
output$animate_img <- renderUI({
y <- year(input$dates_img)
d <- yday(input$dates_img)
filename <- sprintf("img_%d_%d.png",d,y)
tags$img(src = filename, width="100%")
})
}
shinyApp(ui = ui, server = server)
Just make sure that your image files are placed directly in the www folder and it should work.
Thanks,
Jimmy

Adjust size of Shiny progress bar and center it

I'm working with a Shiny app where I need to calculate processes and while the calc progress is executing, I'm using a progressBar to show the process.
The problem is that the progress bar is too small, and I don't like the way is shown.
So, I was thinking that maybe there's a way to implement a progress bar using a Shiny modal (there's a function called modalDialog).
My idea is that when the user runs the calc, a modal will be opened showing a progressBar.
This is the progress code:
withProgress(message = 'Runing GSVA', value = 0, {
incProgress(1, detail = "This may take a while...")
functionToGenerate()
})
Any idea?
I would suggest customizing the CSS class of the notification:
If you inspect the element of the notifier you see that it has the class "shiny-notification". So you can overwrite some properties of that class with tags$style(). In the example below (for the template: see ?withProgress) i decided to adjust height+width to make it bigger and top+left to center it.
ui <- fluidPage(
tags$head(
tags$style(
HTML(".shiny-notification {
height: 100px;
width: 800px;
position:fixed;
top: calc(50% - 50px);;
left: calc(50% - 400px);;
}
"
)
)
),
plotOutput("plot")
)
server <- function(input, output) {
output$plot <- renderPlot({
withProgress(message = 'Calculation in progress',
detail = 'This may take a while...', value = 0, {
for (i in 1:15) {
incProgress(1/15)
Sys.sleep(0.25)
}
})
plot(cars)
})
}
runApp(shinyApp(ui, server), launch.browser = TRUE)
I wrote a progress bar function in the package shinyWidgets, you can put it in a modal, but it's tricky to use with shiny::showModal, so you can create your own modal manually like the below. It's more code to write but it works fine.
library("shiny")
library("shinyWidgets")
ui <- fluidPage(
actionButton(inputId = "go", label = "Launch long calculation"), #, onclick = "$('#my-modal').modal().focus();"
# You can open the modal server-side, you have to put this in the ui :
tags$script("Shiny.addCustomMessageHandler('launch-modal', function(d) {$('#' + d).modal().focus();})"),
tags$script("Shiny.addCustomMessageHandler('remove-modal', function(d) {$('#' + d).modal('hide');})"),
# Code for creating a modal
tags$div(
id = "my-modal",
class="modal fade", tabindex="-1", `data-backdrop`="static", `data-keyboard`="false",
tags$div(
class="modal-dialog",
tags$div(
class = "modal-content",
tags$div(class="modal-header", tags$h4(class="modal-title", "Calculation in progress")),
tags$div(
class="modal-body",
shinyWidgets::progressBar(id = "pb", value = 0, display_pct = TRUE)
),
tags$div(class="modal-footer", tags$button(type="button", class="btn btn-default", `data-dismiss`="modal", "Dismiss"))
)
)
)
)
server <- function(input, output, session) {
value <- reactiveVal(0)
observeEvent(input$go, {
shinyWidgets::updateProgressBar(session = session, id = "pb", value = 0) # reinitialize to 0 if you run the calculation several times
session$sendCustomMessage(type = 'launch-modal', "my-modal") # launch the modal
# run calculation
for (i in 1:10) {
Sys.sleep(0.5)
newValue <- value() + 1
value(newValue)
shinyWidgets::updateProgressBar(session = session, id = "pb", value = 100/10*i)
}
Sys.sleep(0.5)
# session$sendCustomMessage(type = 'remove-modal', "my-modal") # hide the modal programmatically
})
}
shinyApp(ui = ui, server = server)

shinyBS Modal within checkbox group

I use shinyBS::bsModal() to place explanations of the UI elements there. It works great when I place a bsButton() behind the title of the checkbox.
Now I want to place it behind the checkbox options.
A first hint could be this answer where the same for a tooltip is done (but my modification do not work).
Minimal Example:
library(shiny)
library(shinyBS)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
checkboxGroupInput("qualdim",
tags$span("Chekboxoptions",
bsButton("modalbt", "?", style = "inverse", size = "extra-small")),
c("Option_1" = "Option_1",
"Option_2" = "Option_2"))
),
mainPanel(
bsModal("modalExample", "Modal", "modalbt", size = "large",
verbatimTextOutput("helptext")))
)
)
server <- function(input, output) {
output$helptext <- renderText({"I can trigger a shinyBS::bsModal() from here, but I want to place two buttons behind `Option_1` and `Option_2`" })
}
shinyApp(ui = ui, server = server)
The bsModal works anywhere and just takes the button id as a trigger. So the only thing you need to do is to get a suitable button inside the checkboxGroup. From the previous Question/Answer you linked, you already have the function to get a bsButton inside the group input. (Just erase the line where the tooltip has been assigned. This is not needed here.)
The code below basically is copy paste now. I just added some extra bsButton settings like size, style and id (this one is important! was not important in the linked question with the tooltips!), such that you can use the function more like you would use bsButton.
library(shiny)
library(shinyBS)
makeCheckboxButton <- function(checkboxValue, buttonId, buttonLabel, size = "default", style = "default"){
size <- switch(size, `extra-small` = "btn-xs", small = "btn-sm",
large = "btn-lg", "default")
style <- paste0("btn-", style)
tags$script(HTML(paste0("
$(document).ready(function() {
var inputElements = document.getElementsByTagName('input');
for(var i = 0; i < inputElements.length; i++){
var input = inputElements[i];
if(input.getAttribute('value') == '", checkboxValue, "'){
var button = document.createElement('button');
button.setAttribute('id', '", buttonId, "');
button.setAttribute('type', 'button');
button.setAttribute('class', '", paste("btn action-button", style , size), "');
button.appendChild(document.createTextNode('", buttonLabel, "'));
input.parentElement.parentElement.appendChild(button);
};
}
});
")))
}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
checkboxGroupInput("qualdim", label = "Chekboxoptions", choices = c("Option_1", "Option_2")),
makeCheckboxButton("Option_1", "modalbt", "?", size = "extra-small", style = "inverse")
),
mainPanel(
bsModal("modalExample", "Modal", "modalbt", size = "large",
verbatimTextOutput("helptext")))
)
)
server <- function(input, output) {
output$helptext <- renderText({"I can trigger a shinyBS::bsModal() from here, but I want to place two buttons behind `Option_1` and `Option_2`" })
}
shinyApp(ui = ui, server = server)

Resources