rshinyplotlyhtmlwidgetsonrender

Erasing all selectizeInput() values without Shiny app closing after onRender() has been called


I am trying to create a Shiny app to explore a data frame with 4 variables/columns (A, B, C, D) and 10,000 rows. There is an input field where users must select 2 of the 4 variables/columns. Once they have done so, then a scatterplot is shown on the right. The scatterplot is a Plotly object with hexagon binning summarizing the values of the 10,000 rows between the two user-selected variables/columns.

At this point, the user can select a "Go!" button, which causes an orange dot corresponding to the first row of those 2 variables/columns to be superimposed onto the Plotly object. The user can sequentially select "Go!" and then the orange dot corresponding to the second, third, fourth, etc. row will be superimposed onto the Plotly object. The name of the row ID is output above the scatterplot matrix.

For the most part, the app is working. There are only 2 things I am trying to improve upon:

1) I would like the user to be able to select new pairs in the input field. This works for the most part. However, there is one specific situation where this will cause the app to close suddenly. It happens after an orange point has been overlaid onto the scatterplot. If the user then erases the two input pairs, the app suddenly closes. I would like the user to be able to erase both input pair values and input two new pair values without the app closing even after orange points have been plotted to the scatterplot.

2) I notice that the output of the row ID lags somewhat after the orange dot is plotted. I wonder why this happens since I output the row ID before plotting the orange dot in the script. I would prefer for there to be less of a lag, but am uncertain how to approach that.

Any suggestions on how to solve either of these two issues would be greatly appreciated! My MWE showing this issue is below.

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

myPairs <- c("A", "B", "C", "D")

ui <- shinyUI(fluidPage(
  titlePanel("title panel"),

  sidebarLayout(position = "left",
    sidebarPanel(
      selectizeInput("selPair", "Pairs:", choices = myPairs, multiple = TRUE, options = list(maxItems = 2)),
      actionButton("goButton", "Go!"),
      width = 3
    ),
    mainPanel(
      verbatimTextOutput("info"),
      plotlyOutput("scatMatPlot")
    )
  )
))

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

  # Create data and subsets of data based on user selection of pairs
  dat <- data.frame(ID = paste0("ID", 1:10000), A = rnorm(10000), B = rnorm(10000), C = rnorm(10000), D = rnorm(10000))
  pairNum <- reactive(input$selPair)
  group1 <- reactive(pairNum()[1])
  group2 <- reactive(pairNum()[2])
  sampleIndex <- reactive(which(colnames(dat) %in% c(group1(), group2())))

  # Create data subset based on two letters user chooses
  datSel <- eventReactive(sampleIndex(), {
    datSel <- dat[, c(1, sampleIndex())]
    datSel$ID <- as.character(datSel$ID)
    datSel <- as.data.frame(datSel)
    datSel
  })

  sampleIndex1 <- reactive(which(colnames(datSel()) %in% c(group1())))
  sampleIndex2 <- reactive(which(colnames(datSel()) %in% c(group2())))

  # Create background Plotly graph with hex binning all 100 rows of the two user-selected columns
  ggPS <- eventReactive(datSel(), {
    minVal = min(datSel()[,-1])
    maxVal = max(datSel()[,-1])
    maxRange = c(minVal, maxVal)
    xbins=7
    buffer = (maxRange[2]-maxRange[1])/xbins/2
    x = unlist(datSel()[,(sampleIndex1())])
    y = unlist(datSel()[,(sampleIndex2())])
    h <- hexbin(x=x, y=y, xbins=xbins, shape=1, IDs=TRUE, xbnds=maxRange, ybnds=maxRange)
    hexdf <- data.frame (hcell2xy (h),  hexID = h@cell, counts = h@count)
    attr(hexdf, "cID") <- h@cID
    p <- ggplot(hexdf, aes(x=x, y=y, fill = counts, hexID=hexID)) + geom_hex(stat="identity") + geom_abline(intercept = 0, color = "red", size = 0.25) + coord_cartesian(xlim = c(maxRange[1]-1*buffer, maxRange[2]+buffer), ylim = c(maxRange[1]-1*buffer, maxRange[2]+buffer)) + coord_equal(ratio=1) + labs(x = colnames(datSel()[sampleIndex1()]), y = colnames(datSel()[sampleIndex2()]))
    ggPS <- ggplotly(p)
    ggPS})

  # Output hex bin plot created just above
  output$scatMatPlot <- renderPlotly({
    # Each time user pushes Go! button, the next row of the data frame is selected
    datInput <- eventReactive(input$goButton, {
      g <- datSel()$ID[input$goButton]

      # Output ID of selected row
      output$info <- renderPrint({
        g
      })

      # Get x and y values of seleced row
      currGene <- datSel()[which(datSel()$ID==g),]
      currGene1 <- unname(unlist(currGene[,sampleIndex1()]))
      currGene2 <- unname(unlist(currGene[,sampleIndex2()]))
      c(currGene1, currGene2)
    })

    # Send x and y values of selected row into onRender() function
    observe({
      session$sendCustomMessage(type = "points", datInput())
    })

    # Use onRender() function to draw x and y values of seleced row as orange point
    ggPS() %>% onRender("
      function(el, x, data) {

      noPoint = x.data.length;

      Shiny.addCustomMessageHandler('points', function(drawPoints) {
        if (x.data.length > noPoint){
          Plotly.deleteTraces(el.id, x.data.length-1);
        }

        var Traces = [];
        var trace = {
          x: drawPoints.slice(0, drawPoints.length/2),
          y: drawPoints.slice(drawPoints.length/2, drawPoints.length),
          mode: 'markers',
          marker: {
            color: 'orange',
            size: 7
          },
          hoverinfo: 'none'
        };
        Traces.push(trace);
        Plotly.addTraces(el.id, Traces);
      });}")
    })
})

shinyApp(ui, server)

Solution

  • As @HubertL mentioned, it's better to avoid nesting reactive functions. Your app will probably run more smoothely if you change that.

    About your first problem, req and validate are probably the best way to go. These functions check if the user inputs are valid and deal with the invalid ones.

    I've adjusted your code a bit following these sugetions, but you still can change it more. If you take a closer look to ggPS you may notice that it only uses datSel() so you could turn it into a function.

    library(plotly)
    library(GGally)
    library(hexbin)
    library(htmlwidgets)
    library(tidyr)
    library(shiny)
    library(dplyr)
    library(data.table)
    library(ggplot2)
    library(tibble)
    myPairs <- c("A", "B", "C", "D")
    
    ui <- shinyUI(fluidPage(
      titlePanel("title panel"),
      sidebarLayout(
        position = "left",
        sidebarPanel(
          selectizeInput("selPair", "Pairs:", choices = myPairs, multiple = TRUE,
                         options = list(maxItems = 2)),
          actionButton("goButton", "Go!"),
          width = 3
        ),
        mainPanel(
          verbatimTextOutput("info"),
          plotlyOutput("scatMatPlot")
        )
      )
    ))
    
    server <- shinyServer(function(input, output, session) {
      # Create data and subsets of data based on user selection of pairs
      dat <- data.frame(
        ID = paste0("ID", 1:10000), A = rnorm(10000),
        B = rnorm(10000), C = rnorm(10000), D = rnorm(10000),
        stringsAsFactors = FALSE
      )
    
      # Create data subset based on two letters user chooses
      datSel <- eventReactive(input$selPair, {
        validate(need(length(input$selPair) == 2, "Select a pair."))
        dat[c("ID", input$selPair)]
      }, ignoreNULL = FALSE)
    
      # Create background Plotly graph with hex binning all 100 rows of the two user-selected columns
      ggPS <- eventReactive(datSel(), {
        minVal = min(datSel()[,-1])
        maxVal = max(datSel()[,-1])
        maxRange = c(minVal, maxVal)
        xbins=7
        buffer = (maxRange[2]-maxRange[1])/xbins/2
        x = unlist(datSel()[input$selPair[1]])
        y = unlist(datSel()[input$selPair[2]])
        h <- hexbin(x=x, y=y, xbins=xbins, shape=1, IDs=TRUE,
                    xbnds=maxRange, ybnds=maxRange)
        hexdf <- data.frame (hcell2xy (h),  hexID = h@cell, counts = h@count)
        attr(hexdf, "cID") <- h@cID
        p <- ggplot(hexdf, aes(x=x, y=y, fill = counts, hexID=hexID)) +
          geom_hex(stat="identity") + geom_abline(intercept = 0, color = "red", size = 0.25) +
          coord_cartesian(xlim = c(maxRange[1]-1*buffer, maxRange[2]+buffer),
                          ylim = c(maxRange[1]-1*buffer, maxRange[2]+buffer)) +
          coord_equal(ratio = 1) +
          labs(x = input$selPair[1], y = input$selPair[2])
        ggPS <- ggplotly(p)
        ggPS
      })
    
      # Output ID of selected row
      output$info <- renderPrint({ datSel()$ID[req(input$goButton)] })
    
      # Output hex bin plot created just above
      output$scatMatPlot <- renderPlotly({
        # Use onRender() function to draw x and y values of seleced row as orange point
        ggPS() %>% onRender("
                            function(el, x, data) {
                            noPoint = x.data.length;
                            Shiny.addCustomMessageHandler('points', function(drawPoints) {
                            if (x.data.length > noPoint){
                            Plotly.deleteTraces(el.id, x.data.length-1);
                            }
                            var Traces = [];
                            var trace = {
                            x: drawPoints.slice(0, drawPoints.length/2),
                            y: drawPoints.slice(drawPoints.length/2, drawPoints.length),
                            mode: 'markers',
                            marker: {
                            color: 'orange',
                            size: 7
                            },
                            hoverinfo: 'none'
                            };
                            Traces.push(trace);
                            Plotly.addTraces(el.id, Traces);
                            });}")
      })
    
      observe({
        # Get x and y values of seleced row
        currGene <- datSel()[input$goButton, -1]
        # Send x and y values of selected row into onRender() function
        session$sendCustomMessage(type = "points", unname(unlist(currGene)))
      })
    })
    
    shinyApp(ui, server)