12

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.

roman
  • 1,340
  • 9
  • 33

1 Answers1

15

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.

Uwe
  • 41,420
  • 11
  • 90
  • 134
  • thank you that's perfect. at first i could not understand why the second `geom_point` layer has the data source `selected()` but now I can see that the `selected()` function returns the `selected_points` object. out of interest, could i have `selected_points` as the data source and not return the object from the function? if so, why would you do it this way rather than the other? – roman Nov 28 '16 at 16:38
  • 1
    Well, it's all about reactivity or what piece of code will be executed when a particular data is changed (reactivity). Yes, you can use `selected_points` but you have to enforce reactivity by adding a dummy call to `input$clicked` (see _Approach 2_ of my edited answer). I prefer to use reactive functions which make the dependencies more explicit. – Uwe Nov 28 '16 at 23:54
  • Add-on question: How do you add a 'Reset' button to this interface which de-selects all selected points and allows user to start over with no points selected? – Brian D Aug 03 '18 at 22:02
  • Answering my own question... Not sure this is correct, but I added a Reset button using Approach 3. Changing existing `observe` to `observeEvent(input$clicked, {...})` and adding `observeEvent(input$reset, { rv$selected_points <- mtcars[0,] })` where `input$reset` is `actionButton("reset", "Reset Selection")` in the ui section. Nice answer! – Brian D Aug 03 '18 at 22:09