rshinyplotlyhtmlwidgetsonrender

Shiny reactive value directly in onRender() function of htmlWidgets


I have a Shiny application that has two plots. If a user clicks on a point in the top plot, the x and y coordinate of that point are saved to a reactive Shiny variable (in the code below, it is called pointSel).

In the bottom plot, I would like the x and y coordinate from this pointSel to be plotted as a green point. I have this working currently (as seen in the script below). However, each time the pointSel object is updated, the second plot is plotted all over again. Instead, I am trying to keep the second plot background unplotted and to simply overlay a new green point on top of it.

I believe this would require two things:

1) The isolate() function applied to "data = pointSel()" in the onRender() function.

2) Some syntax that alerts to only add the green dot trace if the pointSel has been updated. I put tentative syntax for this in comments "$('#pointSel').on('click',function()".

Below is my code:

library(plotly)
library(GGally)
library(hexbin)
library(htmlwidgets)
library(tidyr)
library(shiny)
library(edgeR)
library(EDASeq)
library(dplyr)
library(data.table)
library(ggplot2)

ui <- shinyUI(fluidPage(
  plotlyOutput("plot1"),
  plotlyOutput("plot2")
))

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

  data <- data.frame(mpg=mtcars$mpg,qsec=mtcars$qsec)

  output$plot1 <- renderPlotly({

    p <- qplot(data$mpg,data$qsec)
    pP <- ggplotly(p)

    pP %>% onRender("
      function(el, x, data) {

      el.on('plotly_click', function(e) {
        var pointSel = [e.points[0].x, e.points[0].y]
        Shiny.onInputChange('pointSel', pointSel);
      })}

      ", data = data)
  })

  pointSel <- reactive(input$pointSel)

  output$plot2 <- renderPlotly({

    p2 <- qplot(mpg,qsec,data=data, geom="point", alpha=I(0))
    pP2 <- ggplotly(p2)

    pP2 %>% onRender("
      function(el, x, data) {
        console.log('Whole bottom plot is being redrawn')
        var myX = data[0]
        var myY = data[1]
      //$('#pointSel').on('click',function() {
        var Traces = [];
        var trace = {
          x: [myX],
          y: [myY],
          mode: 'markers',
          marker: {
            color: 'green',
            size: 10
          }
        };
        Traces.push(trace);
        Plotly.addTraces(el.id, Traces);
      //})
    }", data = pointSel())
  })
})

shinyApp(ui, server)

Note: This is a similar problem to what I posted earlier and has a helpful answer (Shiny actionButton() output in onRender() function of htmlWidgets). I have been coming across variants of this problem (being unable to overlay aspects of a plot to a background plot in the onRender() function), and this current post is simply another variant of that problem. I am trying to find a similar answer for this situation! Thank you for any advice.


Solution

  • You can use define a custom message handler in your onRender function and use it in server.R to pass the selected point coordinates.

    The onRender function of the second plot could look like:

    function(el, x, data) {
      Shiny.addCustomMessageHandler('draw_point',
      function(point) {
        var Traces = [];
        var trace = {
          x: [point[0]],
          y: [point[1]],
          mode: 'markers',
          marker: {
            color: 'green',
            size: 10
          }
        };
        Traces.push(trace);
        console.log(Traces);
        Plotly.addTraces(el.id, Traces);
      });
    }
    

    And in your server.R you could do:

      observe({
        session$sendCustomMessage(type = "draw_point", input$pointSel)
      })
    

    Whenever a point is selected, the coordinates will be sent to the function defined in the onRender and the point will be drawn.