shinycssloaders icon indicating copy to clipboard operation
shinycssloaders copied to clipboard

Using shinycssloaders inside an rmarkdown

Open reshdesu opened this issue 6 years ago • 23 comments

I am using flexidashboard and shinycssloader v0.2.0. When I try to get the loading animation I get the message "Loading..." instead of the animation.

reshdesu avatar Sep 27 '17 11:09 reshdesu

Do you have an example of how you apply the spinner in flexdashboard? As far as I know with Rmarkdown (runtime: shiny) you don't have control over the ui elements, they are automatically created based on the corresponding renderXXX function (which are in the server function in Shiny).

When I will have a bit more time I'll try to investigate if this can be circumvented somehow...

andrewsali avatar Sep 29 '17 18:09 andrewsali

I don't know if it is documented anywhere, but you can use the output variable in a RMarkdown Shiny document. This allows use of the plotOutput, etc. For example, the renderPlot portion of the boilerplate RMarkdown Shiny document can be modified to use shinycssloader as below. This example has a 2 second delay added to renderPlot so you can see the "Loading..." message (and not the spinner).

output$plot = renderPlot({
  Sys.sleep(2) # Make it take some time
  hist(faithful$eruptions, probability = TRUE, breaks = as.numeric(input$n_breaks),
       xlab = "Duration (minutes)", main = "Geyser eruption duration")
  
  dens <- density(faithful$eruptions, adjust = input$bw_adjust)
  lines(dens, col = "blue")
})

shinycssloaders::withSpinner(plotOutput('plot'))

kent37 avatar Nov 01 '17 11:11 kent37

The CSS for the loader doesn't seem to be inserted into the document.

kent37 avatar Nov 01 '17 14:11 kent37

I have added a new branch called rmarkdown. Installing the package from this branch provides an extra function called rmd_in_header. This can be used to create a static header file that can be included in an Rmd document.

Limitations are the following:

  • You need to explicitly call the relevant output function (e.g. plotOutput) in the Rmarkdown file, the i.e. having renderPlot in the document is not enough (as would be with a simple .Rmd document, where shiny does some more magic to create the output function), as the spinner needs to be applied to the output function directly.
  • You can only have one type of spinner in the document, size and color also have to be the same across spinners (the latter are fixed when you call rmd_in_header).
  • You need to make sure that you use the same type of spinner when generating the header file as you will use in the .Rmd file when calling withSpinner. The color and size arguments of withSpinner will have no effect. I will later create a dedicated withSpinnerRmd function to make this clearer, however wanted to have something working already out there.

A working example is given here.

andrewsali avatar Dec 17 '17 10:12 andrewsali

Hi @andrewsali , thank you very much for such a useful library.

I could add shinyccsloaders in my flexdashboard based document. However, I would like to know how to generate a new cssloader_in_header.html file using the rmd_in_header function. As you might suppose, I am using thecssloader_in_header.html example file.

Could you please give me a hint about how to use the rmd_in_header function?

Regards.

pernaletec avatar Sep 20 '19 02:09 pernaletec

Is this possible to use with flexdashboard? Following from the example you linked, I don't understand how to add it to the yaml when I am not outputting an html_document.

---
title: "Dynamic Model Fit"
runtime: shiny
output: 
  flexdashboard::flex_dashboard:
    orientation: columns
    source_code: embed
always_allow_html: yes
---

melissagwolf avatar May 14 '20 18:05 melissagwolf

shinycssloaders does not officially support flexdashboards because they are not shiny apps. It's possible that they might work with some special hacking, if someone here is able to get it to work and post their solution here that would be great, but it might not be possible.

daattali avatar May 14 '20 19:05 daattali

Cool, thank you! I definitely will not be the one to figure that out since I thought a flexdashboard was a shiny app, but hopefully someone else will :)

melissagwolf avatar May 14 '20 21:05 melissagwolf

flexdashboard is rmarkdown. The line between rmarkdown and shiny keeps getting fuzzier with time, but it's still not shiny, sorry :)

daattali avatar May 14 '20 21:05 daattali

Thanks! This explains a lot.

melissagwolf avatar May 14 '20 21:05 melissagwolf

Hi @melissagwolf @daattali . I was able to use it with Flexdashboard. I just did it the same way @andrewsali explained in his example. What is the exact problem you are having?

My yaml looks something like this:

title: "Dynamic Model Fit"
output: 
  flexdashboard::flex_dashboard:
    orientation: columns
    includes:
      in_header: cssloaders_in_header.html
runtime: shiny

pernaletec avatar May 15 '20 08:05 pernaletec

@pernaletec what version of shinycssloaders did you use? The master branch, or the special branch that @andrewsali created?

daattali avatar May 15 '20 13:05 daattali

My main reference was this example: https://github.com/daattali/shinycssloaders/tree/rmarkdown/example/rmarkdown I strictly followed the work of @andrewsali ... thanks BTW!

pernaletec avatar May 15 '20 14:05 pernaletec

Thanks for sharing

daattali avatar May 15 '20 14:05 daattali

@pernaletec @daattali Could one of you help me install the version from the special branch? I'm getting a pandoc error without it. Apologies for the entry level question!

I tried this:

library(devtools)
install_github("daattali/shinycssloaders", subdir="rmarkdown")

Downloading GitHub repo daattali/shinycssloaders@master Error: Failed to install 'shinycssloaders' from GitHub: Does not appear to be an R package (no DESCRIPTION)

melissagwolf avatar May 15 '20 17:05 melissagwolf

"rmarkdown" is not a subdirectory, it's a branch. So don't use the "subdir" parameter. I forget what the right parameter is for installing a specific branch, it might be "ref".

daattali avatar May 15 '20 17:05 daattali

Ah. I got it to install, but I had to bypass the update for glue that was requested by the branch (only the binary version is available and my computer won't install it). This causes shinycss to fail when I run @andrewsali 's demo:

pandoc.exe: C:\Users\missg\AppData\Local\Temp\RtmpMBZ78l\cssloaders_in_header.html: openBinaryFile: does not exist (No such file or directory)

Warning: Error in : pandoc document conversion failed with error 1

C'est la vie - I will find another package! Thanks all for your help - I really appreciate it.

melissagwolf avatar May 15 '20 21:05 melissagwolf

Hi, this package and thread is new to me, but after some trial and error I sort of succeeded in my application:

I largely followed @andrewsali instructions, which worked mostly from the beginning. The only two things didn't work well were the spinner was on the very top edge of the container, and my plotlyOutput(height = "100%") was changed to default 400px.

I did the following to correct the two issues: in the cssloaders_in_header.html file, change from: .shiny-spinner-output-container { position: relative; } to: .shiny-spinner-output-container { height: 100%; }

This worked for my narrow-scoped application, but I didn't test for broader situations.

Best, Xiangnan

xiangnandang avatar Jun 04 '20 06:06 xiangnandang

Thanks! If you think you got it working very well in a robust way, feel free to add a small section to the README telling future people how to do this :)

daattali avatar Jun 04 '20 20:06 daattali

Hi @daattali , thanks for your comment. I did a one-off trouble-shoot around the existing code, and provided the necessary modifications to make it successfully run in my only application. I'd definitely love to contribute more and do more robust testing. However, the fact that this function is not in the master branch (it's only in the rmarkdown branch last updated three years ago), makes it hard to contribute. Do you have plan to merge the rmarkdown branch to master?

Best, Xiangnan

xiangnandang avatar Jun 05 '20 04:06 xiangnandang

You're right, I forgot this was all dependent on another branch. I suppose the best solution if we wanted to support Rmd would be to create a separate function and separate files, in the master branch, that would work with rmd. If anybody wants to take on that initiative, I would welcome that.

daattali avatar Jun 05 '20 04:06 daattali

I just checked, there's no merge conflict between rmarkdown and master branches, do you want me to create a pull request, or it's easier for you to do it. I could take a better look at the rmd_in_header function by @andrewsali and hopefully improve upon.

xiangnandang avatar Jun 05 '20 05:06 xiangnandang

I'll let you take care of that :) Bring the necessary files in and please do some testing to make sure the regular withSpinner isn't affected

daattali avatar Jun 05 '20 05:06 daattali

For completion, here is the solution @andrewsali (original author) proposed years ago (I'm copying it here because I want to clean up stale git branches). It includes 3 files:

File 1: R/rmd_in_header.R
#' Generate an html file to be included as `in_header` for Rmarkdown documents
#' @param header_file The path to the html header file to be created
#' @param type
#' @param color
#' @param size
#' @param proxy.height
rmd_in_header <- function(header_file="cssloaders_in_header.html", type=getOption("spinner.type",default=1),color=getOption("spinner.color",default="#0275D8"),size=getOption("spinner.size",default=1),color.background=getOption("spinner.color.background")) {
  file.remove(header_file)

  .shinycssloaders_headers <- file(header_file,"w"); 
  writeLines("<style>",.shinycssloaders_headers)
  writeLines(readLines(system.file(sprintf("css-loaders/css/load%s.css",type),package="shinycssloaders")),.shinycssloaders_headers)
  writeLines(readLines(system.file("assets/spinner.css",package="shinycssloaders"),warn = FALSE),.shinycssloaders_headers); 
  color.alpha <- sprintf("rgba(%s,0)",paste(grDevices::col2rgb(color),collapse=","))

  if (type==1) {
    writeLines(
      glue::glue(".load{type} .loader, .load{type} .loader:before, .load{type} .loader:after {{background: {color}}} .load{type} .loader {{color: {color}}}"),.shinycssloaders_headers
    )
  }

  if (type %in% c(2,3) && is.null(color.background)) {
    stop("For spinner types 2 & 3 you need to specify manually a background color. This should match the background color of the container.")
  }

  if (type == 2) {
    writeLines(
      glue::glue(".load{type} .loader {{color: {color}}} .load{type} .loader:before, .load{type} .loader:after {{background: {color.background};}}"),.shinycssloaders_headers
    )
  }

  if (type == 3) {
    writeLines(
      glue::glue(
        ".load{type} .loader {{
        background: -moz-linear-gradient(left, {color} 10%, {color.alpha} 42%);
        background: -webkit-linear-gradient(left, {color} 10%, {color.alpha} 42%);
        background: -o-linear-gradient(left, {color} 10%, {color.alpha} 42%);
        background: -ms-linear-gradient(left, {color} 10%, {color.alpha} 42%);
        background: linear-gradient(to right, {color} 10%, {color.alpha} 42%);
  }} 
        .load{type} .loader:before {{
        background: {color}
        }}  
        .load{type} .loader:after {{
        background: {color.background};
        }}
        "),.shinycssloaders_headers
    )
  }

  if (type %in% c(4,6,7)) {
    writeLines(
      glue::glue(".load{type} .loader {{color: {color}}}")
    ,.shinycssloaders_headers)
  }


  if (type == 8) {
    writeLines(
      glue::glue("
.load{type} .loader {{
      border-top: 1.1em solid rgba({paste(grDevices::col2rgb(color),collapse=',')}, 0.2);
      border-right: 1.1em solid rgba({paste(grDevices::col2rgb(color),collapse=',')}, 0.2);
      border-bottom: 1.1em solid rgba({paste(grDevices::col2rgb(color),collapse=',')}, 0.2);
      border-left: 1.1em solid {color};
}}
      "),.shinycssloaders_headers
    )
  }

  # get default font-size from css, and cut it by 25%, as for outputs we usually need something smaller
  size <- round(c(11,11,10,20,25,90,10,10)[type] * size * 0.75)
  writeLines(
    glue::glue(".load{type} .loader {{font-size: {size}px}}"),.shinycssloaders_headers
  )
  writeLines("</style>",.shinycssloaders_headers)

  writeLines("<script>",.shinycssloaders_headers)
  writeLines(readLines(system.file("assets/spinner.js",package="shinycssloaders")),.shinycssloaders_headers); 
  writeLines("</script>",.shinycssloaders_headers)
  close(.shinycssloaders_headers); 
}
File 2: cssloaders_in_header.html
<style>
.load1 .loader,
.load1 .loader:before,
.load1 .loader:after {
  background: #ffffff;
  -webkit-animation: load1 1s infinite ease-in-out;
  animation: load1 1s infinite ease-in-out;
  width: 1em;
  height: 4em;
}
.load1 .loader {
  color: #ffffff;
  text-indent: -9999em;
  margin: 88px auto;
  position: relative;
  font-size: 11px;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
  -webkit-animation-delay: -0.16s;
  animation-delay: -0.16s;
}
.load1 .loader:before,
.load1 .loader:after {
  position: absolute;
  top: 0;
  content: '';
}
.load1 .loader:before {
  left: -1.5em;
  -webkit-animation-delay: -0.32s;
  animation-delay: -0.32s;
}
.load1 .loader:after {
  left: 1.5em;
}
@-webkit-keyframes load1 {
  0%,
  80%,
  100% {
    box-shadow: 0 0;
    height: 4em;
  }
  40% {
    box-shadow: 0 -2em;
    height: 5em;
  }
}
@keyframes load1 {
  0%,
  80%,
  100% {
    box-shadow: 0 0;
    height: 4em;
  }
  40% {
    box-shadow: 0 -2em;
    height: 5em;
  }
}
.shiny-spinner-output-container {
  position: relative;
}

.load-container {
  position: absolute;
  top: 50%;
  -webkit-transform: translateY(-50%);
  transform: translateY(-50%);
  /* to avoid showing a vertical scrollbar
  http://stackoverflow.com/questions/38251204/horizontal-animation-causes-vertical-scrollbar-in-css */
  overflow:hidden; 
  width: 100%;
}

.shiny-spinner-hidden {
  position: absolute;
  top:0;
  left:0;
  z-index: -1;
  visibility:hidden;
}
.load1 .loader, .load1 .loader:before, .load1 .loader:after {background: #0275D8} .load1 .loader {color: #0275D8}
.load1 .loader {font-size: 8px}
</style>
<script>
(function() {
var output_states = [];

function show_spinner(id) {
    $("#"+id).siblings(".load-container, .shiny-spinner-placeholder").removeClass('shiny-spinner-hidden');
    $("#"+id).siblings(".load-container").siblings('.shiny-bound-output, .shiny-output-error').css('visibility', 'hidden');
    // if there is a proxy div, hide the previous output
    $("#"+id).siblings(".shiny-spinner-placeholder").siblings('.shiny-bound-output, .shiny-output-error').addClass('shiny-spinner-hidden');
}

function hide_spinner(id) {
    $("#"+id).siblings(".load-container, .shiny-spinner-placeholder").addClass('shiny-spinner-hidden');
    $("#"+id).siblings(".load-container").siblings('.shiny-bound-output').css('visibility', 'visible');
    // if there is a proxy div, show the previous output in case it was hidden
    $("#"+id).siblings(".shiny-spinner-placeholder").siblings('.shiny-bound-output').removeClass('shiny-spinner-hidden');
}

function update_spinner(id) {
  if (!(id in output_states)) {
    show_spinner(id);
  }
  if (output_states[id] <= 0) {
    show_spinner(id);
  } else {
    hide_spinner(id);
  }
}

$(document).on('shiny:bound', function(event){ 
  /* if not bound before, then set the value to 0 */
  if (!(event.target.id in output_states)) {
    output_states[event.target.id] = 0;
  }
  update_spinner(event.target.id);
});

/* When recalculating starts, show the spinner container & hide the output */
$(document).on('shiny:outputinvalidated', function(event) {
  output_states[event.target.id] = 0;
  update_spinner(event.target.id);
});

/* When new value or error comes in, hide spinner container (if any) & show the output */
$(document).on('shiny:value shiny:error', function(event) {
  output_states[event.target.id] = 1;
  update_spinner(event.target.id);
});
}());
</script>
File 3: rmarkdown_example.Rmd
---
title: "shinycssloaders test"
author: "Andras Sali"
date: "12/16/2017"
output: 
  html_document:
    includes:
      in_header: cssloaders_in_header.html
runtime: shiny
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
library(tidyverse)
```

## Including Plots

You can also embed plots, for example:

```{r pressure, echo=FALSE}
sliderInput("sleep_time","Sleep time:",1,10,5)
plotOutput("pressure") %>% shinycssloaders::withSpinner(type=1)
output$pressure <- renderPlot({
  Sys.sleep(input$sleep_time)
  plot(1:5,1:5)
})
```

Note that the `echo = FALSE` parameter was added to the code chunk to prevent printing of the R code that generated the plot.

If anyone has a good idea to add rmd support in a clean way, I'd be happy for a PR!

daattali avatar Apr 17 '23 23:04 daattali

@mkinare @kent37 @pernaletec @xiangnandang @melissagwolf @M-Z @intael @robbfitzsimmons @seabbs @lvalnegri @fawda123 @juliasilge @pernaletec @razielar @MarcoFelipeKing

The latest github version of shinycssloaders should now support rmarkdown. Can you please try it and let me know if it works or not for you. Thanks!

daattali avatar Apr 18 '23 04:04 daattali

My simple example works.

kent37 avatar Apr 19 '23 21:04 kent37

@mkinare @kent37 @pernaletec @xiangnandang @melissagwolf @M-Z @intael @robbfitzsimmons @seabbs @lvalnegri @fawda123 @juliasilge @pernaletec @razielar @MarcoFelipeKing

The latest github version of shinycssloaders should now support rmarkdown. Can you please try it and let me know if it works or not for you. Thanks!

It works well for me! showPageSpinner() and hidePageSpinner() were very easy to implement in my rmarkdown.

melissagwolf avatar Nov 05 '23 20:11 melissagwolf