4

I wrote a loop that made 10 graphs in R:

library(plotly)

for (i in 1:10)

{

d_i = data.frame(x = rnorm(100,100,100), y = rnorm(100,100,100))

title_i = paste0("title_",i)

p_i = plot_ly(data = d_i, x = ~x, y = ~y) %>% layout(title = title_i)

htmlwidgets::saveWidget(as_widget(p_i), paste0("plot_",i, ".html"))

}

I have this code (Input menu contents do not overflow row box in flexdashboard) that makes a dashboard in R:

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
runtime: shiny
---

```{r setup, include=FALSE}
library(flexdashboard)
library(shiny)
```

Column {data-width=100}
-----------------------------------------------------------------------

### Window 1

```{r}
selectInput("project", label = NULL, choices = c("A","B","C","D"))
```


Column {data-width=400}
-----------------------------------------------------------------------

### Chart B

```{r}
renderPlot({
  plot(rnorm(1000), type = "l", main = paste("Project:",input$project, " Data:", input$data))
})
```

I would like to adapt this code so that the drop down menu allows the user to load the previously created graph/html file (e.g. from "My Documents") that is being searched for. For example, if the user searches for "plot_7", then plot_7 is displayed.

I tried the following code:

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
runtime: shiny
---

```{r setup, include=FALSE}
library(flexdashboard)
library(shiny)
```

Column {data-width=100}
-----------------------------------------------------------------------

### Window 1

```{r}
plots = rep("plot", 10)
i = seq(1:100)
choice = paste0(plots, "_",i)
selectInput("project", label = NULL, choices = choice)
```


Column {data-width=400}
-----------------------------------------------------------------------

### Chart B

```{r}
renderPlot({
<object class="one" type="text/html" data="plot_i.html"></object>
})
```

But this returns the following error:

Error: <text<:2:1 unexpected '<' 
1: renderPlot({
2:<
 ^

Can someone please show me how I can fix this? And is it possible to do this WITHOUT shiny? (i.e. only in flexdashboard)

Thank you!

ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
stats_noob
  • 5,401
  • 4
  • 27
  • 83
  • You are mixing html and R code. Try `renderUI` from [this example](https://stackoverflow.com/questions/64267297/how-can-i-dynamically-render-html-links-in-shiny) – Ric Oct 19 '22 at 22:45

3 Answers3

5

This answers your next question:

Just a question: In the first answer you provided, you were able to "type in" which plot you wanted to see. In the second answer, you can only "scroll". Is there a way to add the "type in" which plot you want to see for the second answer?

Short answer: yes

... and how to do that?

I actually tried to use selectize.js in an ironic full circle of sorts, but it didn't work out...violence was considered...but it's an inanimate object...so...ya, I lost by default

This uses the JS library/package (whatever they call it for that language) select2.

flexdashboard is SUPER FUN! It really didn't want me to add this library with JS (that would have been too easy, ya know? So this puppy had to get added to the YAML.

The YAML to make this work.

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
    extra_dependencies: !expr list(htmltools::htmlDependency('select2.min.js', '1.0', src = list(href = 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist'), script='js/select2.min.js', all_files = FALSE, style = 'css/select2.min.css'))
---

By default, it will look like this.

enter image description here

I figured your very next question would be about appearance... so I jumped the gun.

As far as I understand it, (I'm new to select2), when widening the search box, you have to move the dropdown arrow, which accounts for the first 3 of the entries in this CSS.

The next two are for highlighting when you mouse over in the dropdown. By default, the previous selection is highlighted grey, and the currently hovered-over is highlighted light blue. I added these so that you could change the colors if you wanted to. The final call in CSS is setting the font family. I chose the default family in Plotly (so they matched).

```{css}

.select2-container--default .select2-selection--single{ 
  min-height: 40px;
  padding: 6px 6px;
  width: 175px;
  position: relative;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
  right: 0px;
  width: 20px;
  min-height: 34px;   /* parent min-height, minus top padding 40 - 6 */
  position: absolute;
}
.select2-dropdown { /* the chunk requires 'important' */
  width: 175px !important; /* so they're the same width */
  top: 50%;
  padding: 6px; 12px;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
  background-color: #F5F0E3;
  color: black;    /* in dropdown, item hovered on bg and text */
}                  /* default is background-color: #5897fb; default blue  */
.select2-container--default .select2-results__option--selected {
  background-color: #fbfaf5;
  color: black;    /* in dropdown, PREVIOUS selection bg and text  */
}                  /* default background-color: #ddd; yucky grey */
option {
  font-family: verdana;       /*  to match plotly  */
}

```

Creating the plot list, the dropdown, and rendering the plots in R code didn't change.

The JS didn't change that much.


/* doesn't catch that the first plot is default; set manually */
setTimeout(function(){
  $('select').select2();                /* connect to the select2 library */
  plt = document.querySelectorAll('div.plotly.html-widget');
  for(i = 0; i < plt.length; i++) {
    if(i === 0) {
      plt[i].style.display = 'block';
    } else {
      plt[i].style.display = 'none';
    }
  }
}, 200) /* run once; allow for loading*/

/* goes with the dropdown; this shows/hides plots based on dropdown */
function getPlot(opt) {
  plt = document.querySelectorAll('div.plotly.html-widget');
  for(i = 0; i < plt.length; i++) {      /* switched to plt from opt here */
    opti = opt.options[i];
    if(opti.selected) {
      plt[i].style.display = 'block';
    } else {
      plt[i].style.display = 'none'
    }
  }
}

That all gives you this.

enter image description here enter image description here

All the code altogether.

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
    extra_dependencies: !expr list(htmltools::htmlDependency('select2.min.js', '1.0', src = list(href = 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist'), script='js/select2.min.js', all_files = FALSE, style = 'css/select2.min.css'))
---

```{r setup, include=FALSE}

library(flexdashboard)
library(plotly)
library(tidyverse)
library(htmltools)
library(shinyRPG)   #  devtools::install_github("RinteRface/shinyRPG")

plts <- vector(mode = "list")   # store plot list

for (i in 1:10) {
  d_i = data.frame(x = rnorm(100,100,100), y = rnorm(100,100,100))
  title_i = paste0("title_",i)

  plts[[i]] <- plot_ly( # make a list of objects; no .html
    data = d_i, x = ~x, y = ~y, height = 400, 
    mode = "markers", type = "scatter") %>%
    layout(title = title_i)
  }

```


```{css}

.select2-container--default .select2-selection--single{ /* outer container of dropdown */
  min-height: 40px;
  padding: 6px 6px;
  width: 175px;
  position: relative;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
  right: 0px;
  width: 20px;
  min-height: 34px;   /* parent min-height, minus top padding 40 - 6 */
  position: absolute;
}
.select2-dropdown { /* the chunk requires 'important' */
  width: 175px !important; /* so they're the same width */
  top: 50%;
  padding: 6px; 12px;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
  background-color: #F5F0E3;
  color: black;    /* in dropdown, item hovered on bg and text */
}                  /* default is background-color: #5897fb; default blue  */
.select2-container--default .select2-results__option--selected {
  background-color: #fbfaf5;
  color: black;    /* in dropdown, PREVIOUS selection bg and text  */
}                  /* default background-color: #ddd; yucky grey */
option {
  font-family: verdana;       /*  to match plotly  */
}

```


Column {data-width=100}
-----------------------------------------------------------------------

### Window 1 {data-height=500}

```{r makeGoodChoices}

opts <- choice <-  paste0("plot_", 1:100) # this line replaces last 3 lines
namedChoices = setNames(opts, choice)

newInput <- rpgSelect(         # <----- I'm new; the dropdown
  "selectBox",
  NULL,
  namedChoices,
  multiple = F)

newInput$children[[2]]$attribs$onchange <- "getPlot(this)"

newInput  # add dropdown to webpage

```

<!--- make space between dropdown and plot --->
<div id="plots" style="margin-top:3rem; margin-bottom:3rem;">

```{r dynoPlots,results='asis'}

tagList(plts) # print every plot (so they're all in the HTML)

```

</div>

```{r giveItUp,results='asis',engine='js'}

/* doesn't catch that the first plot is default, set manually */
setTimeout(function(){
  $('select').select2();                /* connect to the select2 library */
  plt = document.querySelectorAll('div.plotly.html-widget');
  for(i = 0; i < plt.length; i++) {
    if(i === 0) {
      plt[i].style.display = 'block';
    } else {
      plt[i].style.display = 'none';
    }
  }
}, 200) /* run once; allow for loading*/

/* goes with the dropdown; this shows/hides plots based on dropdown */
function getPlot(opt) {
  plt = document.querySelectorAll('div.plotly.html-widget');
  for(i = 0; i < plt.length; i++) {     /* switched to plt from opt here */
    opti = opt.options[i];
    if(opti.selected) {
      plt[i].style.display = 'block';
    } else {
      plt[i].style.display = 'none'
    }
  }
}

```
Kat
  • 15,669
  • 3
  • 18
  • 51
  • Thank you so much Kat! I tried running this new code and got the following output: https://imgur.com/a/OdntMkU – stats_noob Oct 21 '22 at 03:49
  • It looks like I am missing an additional "bar" which lets you type in things? – stats_noob Oct 21 '22 at 03:50
  • 1
    Hmmm, I just started a brand new RMD, copied exactly what I have coded here, and knit. Everything works as expected in the Viewer pane and in Chrome. If you originally edited an existing script, can you try that? Start a brand new RMD and try the code again? I'm not sure where to start, since I'm not having that problem. If that doesn't work, I suppose we could see if we have the same versions of the libraries...I'm not sure how that would make a difference, though. – Kat Oct 21 '22 at 13:02
  • Should I open a bounty on this question to see if someone knows how to import the plotly files from "my documents"? – stats_noob Oct 22 '22 at 00:22
  • Quick question: do you know anything about this? https://stackoverflow.com/questions/74148561/sending-an-email-through-html – stats_noob Oct 22 '22 at 00:22
  • As far as importing the images, I found a way to make it work---ish. With only 10 self-contained widgets, the RMD rendered an HTML page that is 61MB! IMO I've got a pretty powerful computer. After about five minutes I closed the webpage, because it still hadn't finished loading. Of that 61MB, about 56MB is just the Plotly dependencies repeated for each of the graphs. – Kat Oct 23 '22 at 02:14
  • Hi Kat! Thank you for all the effort you have put into this! I wanted to ask you your opinion on this. In reality, I have 200 widgets (e.g. corresponding to each country of the world)- each widget contains 4 mini plotly plots (e.g. pie, scatter). I want the user to be able to type in any of these countries and show the widget corresponding to that country. I thought that it might be easier for the computer to first create these 200 widgets, save them to my personal folder, and then import them when the user calls for them - instead of creating all 200 plots when launching the dashboard. – stats_noob Oct 23 '22 at 04:41
  • Thus, do you think it might be better to first create/save all 200 widgets and call each one individually as the user wishes .... or would it better to create them prior to launching the dashboard (i.e. within the dashboard)? – stats_noob Oct 23 '22 at 04:42
  • I think having them as external files will make it so slow no one will want to look at the plots. I suggest that you create them in the RMD that you use to render them. – Kat Oct 23 '22 at 04:53
  • Thank you for this suggestion! I would have thought along the lines of "the early bird gets the worm". I might as well create all the plots on my own time because in my mind, loading a pre-existing plot has to be faster than creating a plot and then loading it ... but now you have convinced me that I was likely wrong about this! Thank you so much - I will try to do this instead! – stats_noob Oct 23 '22 at 05:25
4

This isn't exactly what you're looking for. This doesn't import the file. I'm still going to try to figure that part out.

I'm still trying to make the external file call work. Right now, it just wasn't to give me the literal HTML. I've tried a few approaches. I'm sure it's being a pain because this is probably not a good way to do this. For example, each plot will bring in the full HTML, which means that if there were 100 plots, you've got the entire plotly.js 100 times. (Whoa!)

If you're set on using external files and planning on rendering them in RMD, especially when using Shiny, you may want to consider an approach that keeps them R objects, like Rda or RData. That will use a LOT less memory.

In this version, I've only created the plots as objects (not saved, external files).

This is modified from your question. It creates an object for each for iteration.

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
runtime: shiny
---

```{r setup, include=FALSE}
library(flexdashboard)
library(shiny)
library(plotly)
library(tidyverse)
library(htmltools)

for (i in 1:10) {
  d_i = data.frame(x = rnorm(100,100,100), y = rnorm(100,100,100))
  title_i = paste0("title_",i)
  # p_i = plot_ly(data = d_i, x = ~x, y = ~y) %>% layout(title = title_i)
  assign(paste0("plot_", i, ".html"),   # name them plot_1.html, plot_2.html and so on
         plot_ly(data = d_i, x = ~x, y = ~y, height = 400) %>% 
           layout(title = title_i))
        # not using right now!
  # htmlwidgets::saveWidget(as_widget(p_i), paste0("plot_",i, ".html")) 
  }

# htmlwidgets::saveWidget(as_widget(plot_1.html), "plot_1.html") # created for testing
```

I've modified your called to selectInput, as well. I made this a named vector, so that you would have plot_1.html called when the user picked plot_1.

I've kept your code in there, so you can see what's changed.

```{r makeGoodChoices}

# plots = rep("plot", 10)
# i = seq(1:100)
# choice = paste0(plots, "_",i)

choice = paste0("plot_", 1:100) # this line replaces last 3 lines
opts <- paste0(choice, ".html")

namedChoices = setNames(opts, choice)

# selectInput("project", label = NULL, choices = choice) # originally
selectInput("project", label = NULL, choices = namedChoices)
```

Since this is an R object (not an external file), this is how you would call the plots from the dropdown.

```{r dynoPlots}

renderPlotly(get(input$project)) # show me!

```

enter image description here

enter image description here

The RMarkdown altogether

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
runtime: shiny
---

```{r setup, include=FALSE}
library(flexdashboard)
library(shiny)
library(plotly)
library(tidyverse)
library(htmltools)

for (i in 1:10) {
  d_i = data.frame(x = rnorm(100,100,100), y = rnorm(100,100,100))
  title_i = paste0("title_",i)
  # p_i = plot_ly(data = d_i, x = ~x, y = ~y) %>% layout(title = title_i)
  assign(paste0("plot_", i, ".html"),   # name them plot_1.html, plot_2.html and so on
         plot_ly(data = d_i, x = ~x, y = ~y, height = 400) %>% 
           layout(title = title_i))
        # not using right now!
  # htmlwidgets::saveWidget(as_widget(p_i), paste0("plot_",i, ".html"))
  }

# htmlwidgets::saveWidget(as_widget(plot_1.html), "plot_1.html") # created for testing
```

Column {data-width=100}
-----------------------------------------------------------------------

### Window 1 {data-height=500}

```{r makeGoodChoices}

# plots = rep("plot", 10)
# i = seq(1:100)
# choice = paste0(plots, "_",i)

choice = paste0("plot_", 1:100) # this line replaces last 3 lines
opts <- paste0(choice, ".html")

namedChoices = setNames(opts, choice)

# selectInput("project", label = NULL, choices = choice) # originally
selectInput("project", label = NULL, choices = namedChoices)
```

```{r dynoPlots}

renderPlotly(get(input$project)) # show me!

```
Kat
  • 15,669
  • 3
  • 18
  • 51
4

I decided to make this an entirely different answer because it really is a different question.

This is based on the assumption that you won't import external files. This does not use Shiny runtime, but does the same thing as above.

BTW, I didn't check if selectInput would work, I went with shinyRPG because I knew it would work.

Here's a summary of changes from the answer to your original question:

  • dropped shiny: runtime from YAML
  • dropped library(shiny)
  • added library(shinyRPG)
  • dropped plot names (they're in a list now)
  • added list to store plots; sent plots to list when created
  • dropped .html from dropdown option names (they can be anything you want now)
  • rpgSelect replaced selectInput
  • added JS to connect plots to the dropdown

Here's what the bare bones looks like (almost exactly the same)

enter image description here

All of the code to make this happen with notes in the code for explanation. If anything is unclear, let me know.

---
title: "Test Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
---

```{r setup, include=FALSE}

library(flexdashboard)
library(plotly)
library(tidyverse)
library(htmltools)
library(shinyRPG)   # devtools::install_github("RinteRface/shinyRPG")

plts <- vector(mode = "list")   # stores plots

for (i in 1:10) {
  d_i = data.frame(x = rnorm(100,100,100), y = rnorm(100,100,100))
  title_i = paste0("title_",i)

  plts[[i]] <- plot_ly(                  # make a list of objects
    data = d_i, x = ~x, y = ~y, height = 400, 
    mode = "markers", type = "scatter") %>%
    layout(title = title_i)
  }

```

Column {data-width=100}
-----------------------------------------------------------------------

### Window 1 {data-height=500}

```{r makeGoodChoices}

opts <- choice <-  paste0("plot_", 1:100) # this line replaces last 3 lines
namedChoices = setNames(opts, choice)

newInput <- rpgSelect(         # <----- I'm new; the drop down (used same args)
  "selectBox",
  NULL,
  namedChoices,
  multiple = F)
newInput$children[[2]]$attribs$onchange <- "getPlot(this)"

newInput  # add dropdown to webpage
```

<!--- make space between dropdown and plot --->

<div id="plots" style="margin-top:3rem; margin-bottom:3rem;">

```{r dynoPlots,results='asis'}

tagList(plts) # print every plot (so they're all in the HTML)

```

</div>

```{r giveItUp,results='asis',engine='js'}

/* doesn't catch that the first plot is default, set manually */
setTimeout(function(){
  plt = document.querySelectorAll('div.plotly.html-widget');
  for(i = 0; i < plt.length; i++) {
    if(i === 0) {
      plt[i].style.display = 'block';
    } else {
      plt[i].style.display = 'none';
    }
  }
}, 200) /* run once; allow for loading*/

/* goes with the drop down; this shows/hides plots based on drop down */
function getPlot(opt) {
  plt = document.querySelectorAll('div.plotly.html-widget');
  for(i = 0; i < opt.length; i++) {
    opti = opt.options[i];
    if(opti.selected) {
      plt[i].style.display = 'block';
    } else {
      plt[i].style.display = 'none'
    }
  }
}

```
Kat
  • 15,669
  • 3
  • 18
  • 51
  • Thank you so much Kat! I will take a look at this soon! Since this code no longer depends on Shiny - this code that you have provided can be used to create a page within a regular flexdashboard? thanks again for all your help! – stats_noob Oct 20 '22 at 17:57
  • Absolutely. Just so you know, you can put that JS chunk literally anywhere. (I usually keep them at the end.) – Kat Oct 20 '22 at 18:06
  • I was thinking of this idea I am doing to try. Maybe I can create all the plotly plots in advance, mass import them into R, then convert them into html objects and synchronize them with the code you have written? I will attempt this now! – stats_noob Oct 20 '22 at 19:33
  • Just a question: In the first answer you provided, you were able to "type in" which plot you wanted to see. In the second answer, you can only "scroll". Is there a way to add the "type in" which plot you want to see for the second answer? – stats_noob Oct 20 '22 at 19:37
  • I feel like making a meme ... "when the same person provides two amazing answers to your questions and you don't know which one to accept as the official answer". Thanks so much for everything Kat! I appreciate all the help you have provided me! – stats_noob Oct 20 '22 at 19:38