glin / reactable

Interactive data tables for R
https://glin.github.io/reactable
Other
612 stars 79 forks source link

let expand button execute a renderPlot function #367

Open jwijffels opened 3 months ago

jwijffels commented 3 months ago

I'm trying to see if I can make a row expandable and in the same time when clicking the expand button that it updates the plot based on the information in that button which is clicked. By using the following code. If I click on the column which has that expand button the onClick event is not triggered.

I would like to be able to update the plot when a user clicks the expand button. How would you do that? It looks like this is currently not supported or maybe I'm missing something?

https://github.com/glin/reactable/assets/1710810/2e4a2c9b-cf83-4ce3-ba5c-78b81bfbaac5

library(reactable)
library(shiny)

ui <- fluidPage(
    titlePanel("reactable example"),
    reactableOutput("table")
)

server <- function(input, output, session) {
    iris2 <- iris
    iris2$interactionid_key <- paste("key", 1:nrow(iris))

    rv <- reactiveValues(index = "TEST")
    observe({
        print(rv$index)
        output$plot <- renderPlot(MASS::truehist(rnorm(1000), col = "lightblue", border = "white"))
    })
    observeEvent(input$analytics_show_details, {
        print("triggered by analytics_show_details")
        output$plot <- renderPlot(MASS::truehist(rnorm(1000), col = "lightblue", border = "white"))
    })
    output$table <- renderReactable({
        reactable(iris2, resizable = TRUE, showPageSizeOptions = TRUE, 
                  highlight = TRUE, height = "auto", 
                  columns = list(
                      Sepal.Length = colDef(name = "Sepal Length", aggregate = "max", format = colFormat(suffix = " cm", digits = 1),
                                            details = function(index, column) {
                                                #output$plot <- renderPlot(plot(1:10))
                                                ok <- index
                                                print(ok)
                                                rv$index <- ok
                                                tabsetPanel(
                                                    tabPanel("plot",     plotOutput("plot")),
                                                    #tabPanel("subtable", reactable(iris[1:3, 1:2], fullWidth = FALSE)),
                                                    tabPanel("TEST", tags$div(index, column))
                                                )
                                            }), 
                      Sepal.Width = colDef(name = "Sepal Width", defaultSortOrder = "desc", aggregate = "mean", 
                                                      format = list(aggregated = colFormat(suffix = " (avg)", digits = 2)), 
                                                      cell = function(value) {
                                                          if (value >= 3.3) {
                                                              classes <- "tag num-high"
                                                          } else if (value >= 3) {
                                                              classes <- "tag num-med"
                                                          } else {
                                                              classes <- "tag num-low"
                                                          }
                                                          value <- format(value, nsmall = 1)
                                                          span(class = classes, value)
                                                          }
                                                      ), 
                      Petal.Length = colDef(name = "Petal Length", aggregate = "sum"), 
                      Petal.Width = colDef(name = "Petal Width", aggregate = "count"), 
                      Species = colDef(aggregate = "frequency")), 
                  #selection = "single",
                  onClick = JS(sprintf("function(rowInfo, colInfo) {
                    //if (colInfo.id !== 'details') {
                    //  return
                    //}
                    window.alert('Clicked ' + colInfo.id + '-' + rowInfo.values)
                    if (window.Shiny) {
                      //window.alert('Details for row ' + rowInfo.index + ':\\n' + JSON.stringify(rowInfo.values.interactionid_key, null, 2))
                      Shiny.setInputValue('%s', { interactionid_key: rowInfo.values.interactionid_key }, { priority: 'event' })
                    }
                  }", "analytics_show_details")),
                  details = function(index, column) {
                      #output$plot <- renderPlot(plot(1:10))
                      ok <- index
                      print(ok)
                      rv$index <- ok
                      tabsetPanel(
                          tabPanel("plot",     plotOutput("plot")),
                          #tabPanel("subtable", reactable(iris[1:3, 1:2], fullWidth = FALSE)),
                          tabPanel("Row/Column", tags$div(index, column))
                      )
                  })
    })
}
shinyApp(ui, server)
glin commented 2 months ago

I think there are a few different ways to answer this. So first, there's currently no officially supported way to take an action when a user expands a row. It should eventually be supported in the JavaScript API, but for now, it's an undocumented feature that may still change in the future. If you want to try this with the risks, here's a quick example of watching for expanded state changes using Reactable.onStateChange(), and sending the list of expanded rows back to Shiny:

library(shiny)

data <- MASS::Cars93[, c("Manufacturer", "Model", "Type", "Price")]

ui <- fluidPage(
  reactableOutput("tbl"),
  verbatimTextOutput("tbl_state")
)

server <- function(input, output) {
  output$tbl <- renderReactable({
    tbl <- reactable(
      data,
      details = function(index) paste("Details for row", index),
      searchable = TRUE
    )

    htmlwidgets::onRender(tbl, "() => {
      Reactable.onStateChange('tbl', state => {
        const { expanded } = state
        Shiny.setInputValue('tbl_expanded', { expanded })
      })
    }")
  })

  output$tbl_state <- renderPrint({
    writeLines("Table expanded rows (zero-based indices):\n")
    print(input$tbl_expanded)
  })
}

shinyApp(ui, server)

The custom onClick action doesn't trigger because the click event of the expandable cells are reserved for row expansion right now. An alternative that may be possible in the future would be to render a custom button in a cell that both toggles the row expansion and sets some input value in Shiny. But that would also depend on a JavaScript API that currently isn't supported.

Or to sidestep the expand button completely, you could potentially render a different, row or column-specific plotOutput in each row details, rather than reusing the same output for all details. I'm not even sure if using the same plotOutput("plot") for all row details can work because multiple rows would be sharing the same Shiny output ID, which isn't allowed (i.e. the duplicate binding error). Both the row index and column name are available in the row details function, so you can create a unique output ID for that row and/or column:

output$plot_row_1 <- renderPlot({ ... })

details = function(index, column) {
  plotOutput(paste0("plot_row_", index))
}

And finally, a completely different solution that I often see with drill down style tables is to put the plot completely outside of the table, and use row selection to control what the plot shows. This would be a lot simpler to implement today, and can avoid some of the awkward UX with having lots of information in expanded row details.

jwijffels commented 2 months ago

I'm not even sure if using the same plotOutput("plot") for all row details can work because multiple rows would be sharing the same Shiny output ID, which isn't allowed

Your correct on this. That is why I wanted to have full control over understanding which row was expanded or not. Thanks for the solution with Reactable.onStateChange.

This would be a lot simpler to implement today, and can avoid some of the awkward UX with having lots of information in expanded row details

Indeed that's the easiest but a bit less integrated way in my point of view.