Replace plot in HTML viewer

Hi,

My apologies if this question is somehow trivial.

I am plotting an HTML-widget in a loop. As a simplifed example:

library (plotly)

for (i in 1:10){
  print(
    plotly::plot_ly( x = 0:i, y = exp(-(0:i)), type = "scatter", mode = "lines")
  )
}

Running this code in R-studio will create 10 different plots in its HTML viewer. I am looking for a way to update the existing plot in each cycle of the loop, such that at the end you have only one plot. One can also think of it as deleting the plot made in the previous cycle and creating a new one in the current cycle.

I have been unable to find how to achieve this, but presumably this is possible.

Any help is greatly appreciated.

The htmltools::tagList() function can be use to create a collection of HTML tags or htmlwidgets. The htmltools::browsable() functions ensure that when printing these tags, they generate an HTML page (instead of outputting the HTML string the console).

library(plotly)
library(htmltools)

plots <- lapply(1:10, function(i) {
  plot_ly(x = 0:i, y = exp(-(0:i)), type = "scatter", mode = "lines")
})
browsable(tagList(plots))

If you'd like to arrange your widgets in a grid-like layout, you can also use crosstalk::bscols() (or similar)

library(crosstalk)
do.call("bscols", c(plots, list(widths = c(6, 6))))

Note these functions should work for any htmlwidgets object -- if you have a collection of plotly plots, you could also use subplot() which gives you more options such as sharing axes -- see the chapter arranging views here for more info https://plotly-book.cpsievert.me

Thank you @cpsievert, I appreciate your assistance. It seems your solution is slightly different from what I was looking for. In your solution, you gather the plots in the loop, to display them all when the loop has finished.

I was looking for a way to update or replace the current plot while the loop is running. Imagine, for example, that a model is fitted iteratively during the loop and you want to plot how a 'goodness of fit' is developing while the fit is running. Imagine that each cycle in the loop takes more than 2 seconds and there are 100 cycles. In such a case it would be nice if you could track the progress of the goodness of fit while the fit is running, and abort if the goodness of fit develops is a way you do not want.

I was inspired by the function keras::fit() which shows the progress of a 'loss function' while training a neural net.

Of course, if it is not possible to achieve what I am looking for, I can make one plot when the loop has finished, as in

library(plotly)

y = numeric()
for (i in 0:10){
  y[i+1] = exp(-i)
}
print(
  plot_ly(x = 0:10, y = y, type = "scatter", mode = "lines")
)

This seems however a second-best solution.

Ahh, I see. Unless you can/want to precompute all the data ahead-of-time, this sort of thing is best handled in shiny...plotly provides a plotlyProxy() function for modifying particular components of graph via plotly.js method. In this case, you'd want to use the extendTraces method to add data to a line trace.

library(shiny)
library(plotly)

ui <- fluidPage(
  plotlyOutput("p")
)

server <- function(input, output, session) {
  
  output$p <- renderPlotly({
    plot_ly(x = c(0, 1), y = exp(c(0, -1))) %>%
      add_lines()
  })
  
  # a reactive value to track the current x value
  x <- reactiveVal(1)
  
  # invalidate this R code every 100 milliseconds
  observeEvent(invalidateLater(100, session), {
    
    y <- exp(-x())
    
    # update line chart data
    plotlyProxy("p", session) %>%
      plotlyProxyInvoke(
        "extendTraces", 
        list(
          y = list(list(y)),
          x = list(list(x()))
        ),
        list(0)
      )
    
    # update current x value
    x(x() + 1)

  }, ignoreNULL = FALSE)
  
}

shinyApp(ui, server)

You could also do this without shiny if you know a bit of JavaScript and can pre-compute every possible state ahead-of-time:

library(plotly)
library(htmltools)
library(htmlwidgets)

p <- plot_ly() %>%
  add_lines(x = c(0, 1), y = exp(-c(0, 1))) %>%
  onRender("
    function(el) {
      Plotly.d3.json('data.json', function(data) {
        var i = 0;
        setInterval(function() {
          var x = data.x[i];
          var y = data.y[i];
          Plotly.extendTraces(el.id, {x: [[x]], y: [[y]]}, [0])
          i = i + 1;
        }, 100)
      });
    }
")


x <- 2:100
y <- exp(-x)

withr::with_path(tempdir(), {
  jsonlite::write_json(list(x = x, y = y), "data.json")
  saveWidget(p, "index.html")
  if (interactive()) servr::httd()
})

Thank you @cpsievert.

In the case I have in mind, the new value that has to be added to the plot each cycle is the result of a non-trivial calculation, the result of which depends on the results in the previous cycle. In other words: it seems not possible to pre-compute the plot-values.

Following your post, that leaves me with the option to use shiny. This solution seems rather more complicated than I imagined (or hoped) it to be, but so be it. I will try this out. Thank you once again.

This topic was automatically closed 21 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.