Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

let expand button execute a renderPlot function #367

Open
jwijffels opened this issue Mar 27, 2024 · 2 comments
Open

let expand button execute a renderPlot function #367

jwijffels opened this issue Mar 27, 2024 · 2 comments
Labels
enhancement New feature or request

Comments

@jwijffels
Copy link

jwijffels commented Mar 27, 2024

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?

Video_240327162620.mp4
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
Copy link
Owner

glin commented Apr 16, 2024

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.

@glin glin added the enhancement New feature or request label Apr 16, 2024
@jwijffels
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants