2

Here is a working example of extracting the on-click event. I would like to ask you if there is a way to update the clicked point with either increase in size or highlight it etc.,?

library(shiny)
library(plotly)

ui <- fluidPage(
    plotlyOutput("plot"),
    verbatimTextOutput("click")
)

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

    nms <- row.names(mtcars)

    output$plot <- renderPlotly({
        p <- ggplot(mtcars, aes(x = mpg, y = wt, col = as.factor(cyl), key = nms)) + 
           geom_point()
        ggplotly(p)

    })

    output$click <- renderPrint({
        d <- event_data("plotly_click")
        if (is.null(d)) "Click events appear here (double-click to clear)" 
        else cat("Selected point associated with Car: ", d$key)
    })

}

shinyApp(ui, server)

enter image description here

I have searched SO and other appropriate sources for finding a solution to the below question, but could not find one.

Update:

  • This solution works better for this toy plot. But, my original use case contains 50+ levels for the variable of interest and there are high chances that Magenta or any other color would already be present. Also, it takes a considerable amount of time to change the color.
  • Is there any way to increase the size of the clicked point to differentiate it from 100s of nearby points?

A related question to change the shape of the clicked point has been asked here.

Prradep
  • 5,506
  • 5
  • 43
  • 84
  • There is a potentially related question ([dynamic ggplot layers in shiny with nearPoints()](https://stackoverflow.com/q/40805513/3817004)) which doesn't involve `plotly`. – Uwe Aug 31 '17 at 12:40
  • @Uwe Thanks, I will look into it. One main observation is `nearPoints()` does not work in case of `ggplotly` plots. Secondly, as the entire plot is plotting again, it might be of opinion that the factor levels will get new colors and difficult to identify the selected region from the entire plot. – Prradep Aug 31 '17 at 12:41

1 Answers1

6

You could add Plotly's events to your Shiny app with htmlwidget's onrender function.

ggplotly(p) %>% onRender(javascript)

An array of colors is passed to the restyle function. The selected point (pointNumber) is colored magenta while the others get the color from the legend. You could do the same thing with the marker size, marker symbol is a bit more tricky because Plotly does not accept arrays here.

function(el, x){
  el.on('plotly_click', function(data) {
    colors = [];
    var base_color = document.getElementsByClassName('legendpoints')[data.points[0].curveNumber].getElementsByTagName('path')[0].style['stroke']
    for (var i = 0; i < data.points[0].data.x.length; i += 1) {
      colors.push(base_color)
    };
    colors[data.points[0].pointNumber] = '#FF00FF';
    Plotly.restyle(el, 
                   {'marker':{color: colors}}, 
                   [data.points[0].curveNumber]
                  );

  });
}

library(shiny)
library(plotly)
library(htmlwidgets)

ui <- fluidPage(
  plotlyOutput("plot")
)

javascript <- "
function(el, x){
  el.on('plotly_click', function(data) {
    colors = [];

    var base_color = document.getElementsByClassName('legendpoints')[data.points[0].curveNumber].getElementsByTagName('path')[0].style['stroke']
    for (var i = 0; i < data.points[0].data.x.length; i += 1) {
      colors.push(base_color)
    };
    colors[data.points[0].pointNumber] = '#FF00FF';
    Plotly.restyle(el, 
                   {'marker':{color: colors}}, 
                   [data.points[0].curveNumber]
                  );
    //make sure all the other traces get back their original color
    for (i = 0; i < document.getElementsByClassName('plotly')[0].data.length; i += 1) {
      if (i != data.points[0].curveNumber) {
        colors = [];
        base_color = document.getElementsByClassName('legendpoints')[i].getElementsByTagName('path')[0].style['stroke'];
        for (var p = 0; p < document.getElementsByClassName('plotly')[0].data[i].x.length; p += 1) {
          colors.push(base_color);
        }
        Plotly.restyle(el, 
                       {'marker':{color: colors}}, 
                       [i]);
      }
    };
  });
}"
server <- function(input, output, session) {

  nms <- row.names(mtcars)

  output$plot <- renderPlotly({
    p <- ggplot(mtcars, aes(x = mpg, y = wt, col = as.factor(cyl), key = nms)) + 
      geom_point()
    ggplotly(p) %>% onRender(javascript)

  })
}

shinyApp(ui, server)

Some explanations:

  • Plotly's events, e.g. plotly_click pass some information (data in this case) in the event function
  • data.points contains the points for which the event was triggered
  • in this case, we only have one point data.points[0] where pointNumber is the index of the original array and curveNumber is the trace number.
  • Plotly stores all the input data in the div where the plot is drawn
  • We can access it via document.getElementsByClassName('plotly')[0].data
  • The legend can be accessed via document.getElementsByClassName('legendpoints')[i] where i is the index of the legend
  • ggplotly or plot_ly return htmlwidgets
  • Javascript or HTML code can be added with any of the htmlwidget functions, e.g. onrender in this case
Maximilian Peters
  • 30,348
  • 12
  • 86
  • 99
  • Thank you. It works brilliantly (+1). In my original use case, there are 50+ levels in the variable and there are very high chances that `magenta` will be one of them. In such a case, it will be difficult to identify the selected point. Do you think of any other options? (A small typo in your answer `plotlyOutput("plot"),` comma should not be present). – Prradep Aug 31 '17 at 08:21
  • 2
    Thanks for finding the leftover from the code edit! You could also change the size, e.g. `sizes.push(10);... sizes[...pointNumber] = 20;... 'marker': {size: sizes}` – Maximilian Peters Aug 31 '17 at 08:37
  • (Forgot to ask another question I had in mind) Why is that I am able to highlight a point for each factor level? In the above plot, I am able to highlight at max three points (one each belonging to the `cyl==4`, `cyl==6`, `cyl==8`). I would like to highlight at max one point per point before it disappears to highlight next click point. What are your thoughts? – Prradep Aug 31 '17 at 08:56
  • 1
    The proper way of doing it would be to restyle all traces/factors. In the example only the trace from the clicked point is restyled, I added the missing code to the example. – Maximilian Peters Aug 31 '17 at 09:35
  • Could you please point me to the sources, from where I can understand your code and build up on the directions you have hinted? I would like to see what other components can be extracted and how they can be modified. Thanks! – Prradep Aug 31 '17 at 10:42
  • I am sorry but except for the link in the answer it was mostly trial and error, and reverse engineering. There is not a lot of detailed documentation. – Maximilian Peters Aug 31 '17 at 11:15
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/153354/discussion-between-prradep-and-maximilian-peters). – Prradep Aug 31 '17 at 11:15
  • @Prradep please consider accepting this answer by clicking on the check mark to the left of the answer – CPak Aug 31 '17 at 13:13
  • @MaximilianPeters I have a follow up question, but for clarity I made a new SO post out of it. It involves adjusting your fantastic answer to work on scatter3d plotly objects. It is found here: https://stackoverflow.com/questions/47365402/shiny-highlight-a-point-in-plotly-scatter3d – Mark Nov 18 '17 at 11:18
  • @MaximilianPeters, I was not able make your answer based on the to plotly bar plot. I just changed server section as follows: `plot_ly(x = c(1,2,3,4,5)) %>% add_bars( y = c(1,2,2.5,4,5), marker = list(color=c(rep('red',5)))) %>% onRender(javascript)` . The remaining part is same. Could you please let me know what am I missing. – olcaysah Sep 28 '18 at 23:48