dynamic ggplot layers in shiny with nearPoints()

后端 未结 1 645
伪装坚强ぢ
伪装坚强ぢ 2021-02-08 01:59

I\'m familiar with the basics of shiny but struggling with something here. I would like to be able to add a ggplot layer when a point is clicked to highlight that point. I know

1条回答
  •  爱一瞬间的悲伤
    2021-02-08 02:37

    Please, try this:

    Approach 1 (recommended)

    library(shiny)
    library(ggplot2)
    
    # initialize global variable to record selected (clicked) rows
    selected_points <- mtcars[0, ]
    str(selected_points)
    
    
    shinyApp(
      ui = shinyUI(
        plotOutput("plot", click = "clicked")
      ),
    
      server = shinyServer(function(input, output) {
    
        selected <- reactive({
          # add clicked
          selected_points <<- rbind(selected_points, nearPoints(mtcars, input$clicked))
          # remove _all_ duplicates if any (toggle mode) 
          # http://stackoverflow.com/a/13763299/3817004
          selected_points <<- 
            selected_points[!(duplicated(selected_points) | 
                                duplicated(selected_points, fromLast = TRUE)), ]
          str(selected_points)
          return(selected_points)
        })
    
        output$plot <- renderPlot({
          ggplot(mtcars, aes(x = mpg, y = wt)) +
            geom_point() +
            geom_point(data = selected(), colour = "red", size = 5)
        })
      })
    )
    

    If you click a point one time it is highlighted. If you click it a second time the highlight is turned off again (toggling).

    The code uses a global variable selected_points to store the actually highlighted (selected) points and an reactive expression selected() which updates the global variable whenever a point is clicked.

    The str(selected_points) may help to visualize the working but can be removed.

    Approach 2 (alternative)

    There is a slightly different approach which uses observe() instead of reactive() and references the global variable selected_points directly instead of returning the object from a function:

    library(shiny)
    library(ggplot2)
    
    selected_points <- mtcars[0, ]
    str(selected_points)
    
    
    shinyApp(
      ui = shinyUI(
        plotOutput("plot", click = "clicked")
      ),
    
      server = shinyServer(function(input, output) {
    
        observe({
          # add clicked
          selected_points <<- rbind(selected_points, nearPoints(mtcars, input$clicked))
          # remove _all_ duplicates (toggle)
          # http://stackoverflow.com/a/13763299/3817004
          selected_points <<- 
            selected_points[!(duplicated(selected_points) | 
                                duplicated(selected_points, fromLast = TRUE)), ]
          str(selected_points)
        })
    
        output$plot <- renderPlot({
          # next statement is required for reactivity
          input$clicked
          ggplot(mtcars, aes(x = mpg, y = wt)) +
            geom_point() +
            geom_point(data = selected_points, colour = "red", size = 5)
        })
      })
    )
    

    Of course, you can use the global variable selected_points directly in the ggplot call instead of calling the reactive function selected(). However, you have to ensure that renderPlot() is executed whenever input$clicked is changed. Therefore, the dummy reference to input$clicked has to be included in the code within renderPlot().

    Now, the reactive function selected() is no longer needed and can be replaced by an observe() expression. As opposed to reactive(), observe() doesn't return a value. It just updates the global variable selected_points whenever input$clicked is modified.

    Approach 3 (reactive values)

    This approach avoids a global variable. Instead, it uses reactiveValues to create a list-like object rvwith special capabilities for reactive programming (see ?reactiveValues).

    library(shiny)
    library(ggplot2)
    
    shinyApp(
      ui = shinyUI(
        plotOutput("plot", click = "clicked")
      ),
    
      server = shinyServer(function(input, output) {
    
        rv <- reactiveValues(selected_points = mtcars[0, ])
    
        observe({
          # add clicked
          rv$selected_points <- rbind(isolate(rv$selected_points), 
                                               nearPoints(mtcars, input$clicked))
          # remove _all_ duplicates (toggle)
          # http://stackoverflow.com/a/13763299/3817004
          rv$selected_points <- isolate(
            rv$selected_points[!(duplicated(rv$selected_points) | 
                                   duplicated(rv$selected_points, fromLast = TRUE)), ])
          str(rv$selected_points)
        })
    
        output$plot <- renderPlot({
          ggplot(mtcars, aes(x = mpg, y = wt)) +
            geom_point() +
            geom_point(data = rv$selected_points, colour = "red", size = 5)
        })
      })
    )
    

    Please, note that in the observer part references to rv need to be encapsulated in isolate() to ensure that only changes to input$clicked will trigger execution of the code in observer. Otherwise, we'll get an endless loop. Execution of renderPlot is triggered whenever the reactive value rv is changed.

    Conclusion

    Personally, I prefer approach 1 using reactive functions which make the dependencies (reactivity) more explicit. I find the dummy call to input$clicked in approach 2 less intuitive. Approach 3 requires a thorough understanding of reactivity and using isolate() in the right places.

    0 讨论(0)
提交回复
热议问题