JohnCoene / waiter

🕰️ Loading screens for Shiny
https://waiter.john-coene.com/
Other
496 stars 25 forks source link

[Experimental] Automatically hide waiter on start after all outputs render #82

Closed DivadNojnarg closed 3 years ago

DivadNojnarg commented 3 years ago

As discussed here: Here are my thoughts

You can create a counter that increases each time an output is rendered and compare it to the value of Object.keys(Shiny.shinyapp.$bindings).length. This gives you the number of output to render, when the app starts. Don't try Object.keys(Shiny.shinyapp.$values).length since values are computed 1 by 1 and its initial value is not the equal to the number of outputs to render. When counter is equal to the total number of outputs, you hide the waiter:

$(function() {
          let counter = 0, hasWaiter;

          const hideWaiterCallback = function() { 
            counter +=1;
            console.log(counter);
            hasWaiter = ($('.waiter-overlay').length > 0);
            if (hasWaiter) {
              if (counter === Object.keys(Shiny.shinyapp.$bindings).length) {
                window.hide_waiter(null);
                $(document).trigger('remove_waiter_on_start');
              }
            } 
          }

          $(document).on('shiny:value', hideWaiterCallback);

          $(document).on('remove_waiter_on_start', function() {
            $(document).off('shiny:value', hideWaiterCallback);
          });

        });

Moreover, since we don't want this event to trigger after the app is launched (It may mess with all other events), we trigger a custom remove_waiter_on_startevent and disable the hideWaiterCallback from the document.

It seems to work but what happens, let's say, if an observeEvent runs shortly after all output are rendered, updating an input, which invalidates one of the output and triggers a rather long computation? Is it wise the hide the waiter? This is basically not possible to answer...

Example:

library(shiny)
library(waiter)

ui <- fluidPage(
  use_waiter(),
  waiter_show_on_load(),
  tags$script(
    HTML(
      "$(function() {
          let counter = 0, hasWaiter;

          const hideWaiterCallback = function() { 
            counter +=1;
            console.log(counter);
            hasWaiter = ($('.waiter-overlay').length > 0);
            if (hasWaiter) {
              if (counter === Object.keys(Shiny.shinyapp.$bindings).length) {
                window.hide_waiter(null);
                $(document).trigger('remove_waiter_on_start');
              }
            } 
          }

          $(document).on('shiny:value', hideWaiterCallback);

          $(document).on('remove_waiter_on_start', function() {
            $(document).off('shiny:value', hideWaiterCallback);
          });

        });
      "
    )
  ),
  sliderInput("obs", "Number of observations:",
              min = 0, max = 1000, value = 500
  ),
  plotOutput("distPlot")
)

server <- function(input, output, session) {
  output$distPlot <- renderPlot({
    Sys.sleep(6)
    hist(rnorm(input$obs))
  })
}

shinyApp(ui, server)

Let's discuss and I can PR if needed.

DivadNojnarg commented 3 years ago

I edited the code above to consider hidden outputs that could happen in a tab based layout. I explored the Shiny.shinyapp.$initialInput object to recover the id of the hidden plot(s) and remove it/them from the Object.keys(Shiny.shinyapp.$bindings).length count:

let hiddenOutputsIds = Object.keys(Shiny.shinyapp.$initialInput)
              .filter(function(q){return /_hidden/.test(q)})
              .filter(function(t) {return Shiny.shinyapp.$initialInput[t] === true})
              .map(function(x) {return x.split('_output_')[1].split('_hidden')[0]})
              .length;

Then an example:

library(shiny)
library(waiter)

ui <- navbarPage(
  "App Title",
  tabPanel(
    "Tab 1",
    use_waiter(),
    waiter_show_on_load(),
    tags$script(
      HTML(
        "$(function() {
          let counter = 0, hasWaiter;

          const hideWaiterCallback = function() { 
            let hiddenOutputsIds = Object.keys(Shiny.shinyapp.$initialInput)
              .filter(function(q){return /_hidden/.test(q)})
              .filter(function(t) {return Shiny.shinyapp.$initialInput[t] === true})
              .map(function(x) {return x.split('_output_')[1].split('_hidden')[0]})
              .length;

            counter +=1;
            console.log({counter, hiddenOutputsIds});
            hasWaiter = ($('.waiter-overlay').length > 0);
            if (hasWaiter) {
              if (counter === Object.keys(Shiny.shinyapp.$bindings).length - hiddenOutputsIds) {
                window.hide_waiter(null);
                $(document).trigger('remove_waiter_on_start');
              }
            } 
          }

          $(document).on('shiny:value', hideWaiterCallback);

          $(document).on('remove_waiter_on_start', function() {
            $(document).off('shiny:value', hideWaiterCallback);
          });

        });
      "
      )
    ),
    sliderInput("obs", "Number of observations:",
                min = 0, max = 1000, value = 500
    ),
    plotOutput("distPlot")
  ),
  tabPanel(
    "tab 2",
    radioButtons("dist", "Distribution type:",
                 c("Normal" = "norm",
                   "Uniform" = "unif",
                   "Log-normal" = "lnorm",
                   "Exponential" = "exp")),
    plotOutput("distPlot2") 
  )
)

server <- function(input, output, session) {

  output$distPlot2 <- renderPlot({
    Sys.sleep(4)
    dist <- switch(input$dist,
                   norm = rnorm,
                   unif = runif,
                   lnorm = rlnorm,
                   exp = rexp,
                   rnorm)

    hist(dist(500))
  })

  output$distPlot <- renderPlot({
    Sys.sleep(2)
    hist(rnorm(input$obs))
  })
}

shinyApp(ui, server)
JohnCoene commented 3 years ago

Thanks David!

I was wondering if something simpler, that you suggested earlier, would work; observing the idle event.

library(shiny)
library(waiter)

script <- "
  window.loaded = false;
  $(document).on('shiny:idle', function(e) {
    if(!window.loaded)
      hide_waiter(null);

    window.loaded = true;
  })"

ui <- fluidPage(
  tags$head(
    tags$script(script)
  ),
  use_waiter(),
  waiter_show_on_load(),
  actionButton("draw", "render stuff"),
  uiOutput("x")
)

server <- function(input, output){

  Sys.sleep(3)

  output$x <- renderUI({
    input$draw
    h1("Stuff")
  })

}

shinyApp(ui, server)
DivadNojnarg commented 3 years ago

Umm, this is weird. I tried this yesterday and it failed because Shiny went idle twice between 2 rendering, which hide the waiter while the plot was still loading, for some reason.

DivadNojnarg commented 3 years ago

Today looks normal.

Screenshot 2021-01-29 at 11 42 28

DivadNojnarg commented 3 years ago

The problem I mentioned about observeEvent running after startup:

ui <- fluidPage(
  use_waiter(),
  waiter_show_on_load(),
  tags$script(
    HTML(
      "window.loaded = false;
        $(document).on('shiny:message', function(e) {
          if(!window.loaded) 
            console.log(e.message);
        });
$(document).on('shiny:idle', function(e) {
  if(!window.loaded)
    hide_waiter(null);

  window.loaded = true;
});

$(document).trigger('idle');
      "
    )
  ),
  sliderInput("obs", "Number of observations:",
              min = 0, max = 1000, value = 500
  ),
  plotOutput("distPlot"),
  radioButtons("dist", "Distribution type:",
               c("Normal" = "norm",
                 "Uniform" = "unif",
                 "Log-normal" = "lnorm",
                 "Exponential" = "exp")),
  plotOutput("distPlot2")
)

server <- function(input, output, session){

  observeEvent(input$obs, {
    updateRadioButtons(session, "dist", selected = "unif")
  })

  output$distPlot2 <- renderPlot({
    Sys.sleep(2)
    dist <- switch(input$dist,
                   norm = rnorm,
                   unif = runif,
                   lnorm = rlnorm,
                   exp = rexp,
                   rnorm)

    hist(dist(500))
  })

  output$distPlot <- renderPlot({
    Sys.sleep(4)
    hist(rnorm(input$obs))
  })
}

shinyApp(ui, server)