Search code examples
rshinyr-plotlyplotly.js

Restyling traces using plotlyProxy in a scatterplot is unstable when points are colored according to category


I have a Shiny app that builds a scatterplot and highlights the clicked points by restyling the marker outline via plotlyProxy. The app also subsets the data and moves the entries corresponding to the clicked points from the original "Data table" to an "Outlier table".

This seems to work fine when the markers are all the same color, or when they are colored by a continuous variable. But when I color the points by a categorical variable (like "Species"), it has a weird behavior, restyling a marker from each category instead of the clicked one. The data subsets correctly.

I think the restyle function should update all traces unless specified otherwise, so I am not sure where exactly lies the problem.

Here is my code:

library(plotly)
library(DT)

    ui <- fluidPage(
     mainPanel(
      fluidRow(
       div(
        column(
            width = 2,
            uiOutput('chartOptions')),
        column(width = 5,
               h3("Scatter plot"),
               plotlyOutput("scatterplot"),
               verbatimTextOutput("click")
        )
      )
),
    hr(),
    div(
        column(width = 6,
               h2("Data Table"),
               div(
                   DT::dataTableOutput(outputId = "table_keep"),
                   style = "height:auto; overflow-y: scroll;overflow-x: scroll;")),
        
        column(width = 6,
               h2("Outlier Data"),
               div(
                   DT::dataTableOutput(outputId = "table_outliers"),
                   style = "height:auto; overflow-y: scroll;overflow-x: scroll;"))
    )
 ))
server <- function(input, output, session){
  datasetInput <- reactive({
     df <- iris
       return(df)
  })

output$chartOptions <- renderUI({#choose variables to plot
    if(is.null(datasetInput())){}
    else {
        list(
            selectizeInput("xAxisSelector", "X Axis Variable",
                           colnames(datasetInput())),
            selectizeInput("yAxisSelector", "Y Axis Variable",
                           colnames(datasetInput())),
            selectizeInput("colorBySelector", "Color By:",
                           c(c("Do not color",colnames(datasetInput()))))
        )      
    }
})

vals <- reactiveValues(#define reactive values for:
    data = NULL,
    data_keep = NULL,
    data_exclude = NULL)

observe({
    vals$data <- datasetInput()
    vals$data_keep <- datasetInput()
    
})

## Datatable 
output$table_keep <- renderDT({
    vals$data_keep      
},options = list(pageLength = 5))

output$table_outliers <- renderDT({
    vals$data_exclude      
},options = list(pageLength = 5))

# mechanism for managing selected points
keys <- reactiveVal()

observeEvent(event_data("plotly_click", source = "outliers", priority = "event"), {
    req(vals$data)
    is_outlier <- NULL
    key_new <- event_data("plotly_click", source = "outliers")$key
    key_old <- keys()
    if (key_new %in% key_old){
        keys(setdiff(key_old, key_new))
    } else {
        keys(c(key_new, key_old))
    }
    is_outlier <- rownames(vals$data) %in% keys()
    
    vals$data_keep <- vals$data[!is_outlier, ]
    vals$data_exclude <- vals$data[is_outlier, ]
    
    plotlyProxy("scatterplot", session) %>%
        plotlyProxyInvoke(
            "restyle", 
            list(marker.line = list(
                    color = as.vector(ifelse(is_outlier,'black','grey')),
                    width = 2
                
            ))
        )
})

observeEvent(event_data("plotly_doubleclick", source = "outliers"), {
    req(vals$data)
    keys(NULL)
    vals$data_keep <- vals$data
    vals$data_exclude <- NULL
    plotlyProxy("scatterplot", session) %>%
        plotlyProxyInvoke(
            "restyle", 
            list(marker.line = list(
                    color = 'grey',
                    width = 2
                )
            ))
        
})

output$scatterplot <- renderPlotly({
    req(vals$data,input$xAxisSelector,input$yAxisSelector)
    dat <- vals$data
    key <- rownames(vals$data)
    x <- input$xAxisSelector
    y <- input$yAxisSelector
    
    if(input$colorBySelector != "Do not color"){
        color <-  dat[, input$colorBySelector] 
    }else{
        color <- "orange"
    }
    
    scatterplot <- dat %>%
        plot_ly(x = dat[,x], y = dat[,y], source = "outliers") %>%
        add_markers(key = key,color = color,
                    marker = list(size = 10, line = list(
                        color = 'grey',
                        width = 2
                    ))) %>%
        layout(showlegend = FALSE)
    
    return(scatterplot)
})


output$click <- renderPrint({#click event data
    d <- event_data("plotly_click", source = "outliers")
    if (is.null(d)) "click events appear here (double-click to clear)" else d
})
}
 shinyApp(ui, server)

Solution

  • The problem with your above code is that no traceIndices argument is provided for restyle. Please see this.

    In your example, once you switch coloring to the factor Species plotly no longer creates one trace, but three. This happens in JS so counting is done from 0 to 2.

    To restyle those traces you can address them via curveNumber (in this case 0:2) and pointNumber (50 data points in each trace 0:49)

    With a single trace your example works as your key and your trace have the same length (150).

    As your provided code is pretty long I just focused on the "Species" problem. It won't work in all other cases, but you should be able to deduce a more general approach from it:

    library(shiny)
    library(plotly)
    library(DT)
    
    ui <- fluidPage(
      mainPanel(
        fluidRow(
          div(
            column(
              width = 2,
              uiOutput('chartOptions')),
            column(width = 5,
                   h3("Scatter plot"),
                   plotlyOutput("scatterplot"),
                   verbatimTextOutput("click")
            )
          )
        ),
        hr(),
        div(
          column(width = 6,
                 h2("Data Table"),
                 div(
                   DT::dataTableOutput(outputId = "table_keep"),
                   style = "height:auto; overflow-y: scroll;overflow-x: scroll;")),
          
          column(width = 6,
                 h2("Outlier Data"),
                 div(
                   DT::dataTableOutput(outputId = "table_outliers"),
                   style = "height:auto; overflow-y: scroll;overflow-x: scroll;"))
        )
      ))
    server <- function(input, output, session){
      datasetInput <- reactive({
        df <- iris
        df$is_outlier <- FALSE
        return(df)
      })
      
      output$chartOptions <- renderUI({#choose variables to plot
        if(is.null(datasetInput())){}
        else {
          list(
            selectizeInput("xAxisSelector", "X Axis Variable",
                           colnames(datasetInput())),
            selectizeInput("yAxisSelector", "Y Axis Variable",
                           colnames(datasetInput())),
            selectizeInput("colorBySelector", "Color By:",
                           c(c("Do not color",colnames(datasetInput()))))
          )      
        }
      })
      
      vals <- reactiveValues(#define reactive values for:
        data = NULL,
        data_keep = NULL,
        data_exclude = NULL)
      
      observe({
        vals$data <- datasetInput()
        vals$data_keep <- datasetInput()
        
      })
      
      ## Datatable 
      output$table_keep <- renderDT({
        vals$data_keep      
      },options = list(pageLength = 5))
      
      output$table_outliers <- renderDT({
        vals$data_exclude      
      },options = list(pageLength = 5))
      
      # mechanism for managing selected points
      keys <- reactiveVal()
      
      myPlotlyProxy <- plotlyProxy("scatterplot", session)
      
      observeEvent(event_data("plotly_click", source = "outliers", priority = "event"), {
        req(vals$data)
        is_outlier <- NULL
        plotlyEventData <- event_data("plotly_click", source = "outliers")
        key_new <- plotlyEventData$key
        key_old <- keys()
        if (key_new %in% key_old){
          keys(setdiff(key_old, key_new))
        } else {
          keys(c(key_new, key_old))
        }
        vals$data[keys(),]$is_outlier <- TRUE
        is_outlier <- vals$data$is_outlier
        vals$data_keep <- vals$data[!is_outlier, ]
        vals$data_exclude <- vals$data[is_outlier, ]
        print(paste("pointNumber:", plotlyEventData$pointNumber))
        print(paste("curveNumber:", plotlyEventData$curveNumber))
          plotlyProxyInvoke(
            myPlotlyProxy,
            "restyle", 
            list(marker.line = list(
              color = as.vector(ifelse(vals$data[vals$data$Species %in% vals$data[plotlyEventData$key, ]$Species, ]$is_outlier,'black','grey')),
              width = 2
            )), plotlyEventData$curveNumber
          )
      })
      
      observeEvent(event_data("plotly_doubleclick", source = "outliers"), {
        req(vals$data)
        keys(NULL)
        vals$data_keep <- vals$data
        vals$data_exclude <- NULL
          plotlyProxyInvoke(
            myPlotlyProxy,
            "restyle",
            list(marker.line = list(
              color = 'grey',
              width = 2
            )
            ))
    
      })
      
      output$scatterplot <- renderPlotly({
        req(datasetInput(),input$xAxisSelector,input$yAxisSelector)
        dat <- datasetInput()
        key <- rownames(dat)
        x <- input$xAxisSelector
        y <- input$yAxisSelector
        
        if(input$colorBySelector != "Do not color"){
          color <-  dat[, input$colorBySelector] 
        }else{
          color <- "orange"
        }
        
        scatterplot <- dat %>%
          plot_ly(x = dat[,x], y = dat[,y], source = "outliers") %>%
          add_markers(key = key,color = color,
                      marker = list(size = 10, line = list(
                        color = 'grey',
                        width = 2
                      ))) %>%
          layout(showlegend = FALSE)
        
        return(scatterplot)
      })
      
      
      output$click <- renderPrint({#click event data
        d <- event_data("plotly_click", source = "outliers")
        if (is.null(d)) "click events appear here (double-click to clear)" else d
      })
    }
    shinyApp(ui, server)
    

    result