Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

dynamic ggplot layers in shiny with nearPoints()

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 this is possible with ggvis and there is a nice example in the gallery, but I would like to be able to use nearPoints() to capture the click as ui input.

I have tried something (see below) which works apart from the ggplot layer appears and then disappears. I have tried all kinds of edits to this with reactive(), eventReactive() and so on.

Any help is much appreciated...

library(shiny)
library(ggplot2)

shinyApp(
  ui = shinyUI(
        plotOutput("plot", click = "clicked")
    ),

  server = shinyServer(function(input, output) {
    output$plot <- renderPlot({
      ggplot(mtcars, aes(x = mpg, y = wt)) +
        geom_point() +
        geom_point(data = nearPoints(mtcars, input$clicked), colour = "red", size = 5)
    })
  })
)

I think I understand conceptually why this doesn't work. The plot has a dependency on input$clicked which means that when input$clicked changes the plot re-renders but this in turn resets input$clicked. Bit of a catch 22 situation.

like image 744
roman Avatar asked Nov 25 '16 12:11

roman


1 Answers

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.

like image 128
Uwe Avatar answered Sep 24 '22 16:09

Uwe