glin / reactable

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

Dynamically styling table in shiny without re-rendering #255

Closed jyotirmoyjena closed 1 year ago

jyotirmoyjena commented 1 year ago

Hello,

Thank you for this excellent suite of tools. I'm struggling with dynamic row highlighting, so I thought I might post a question here.

I have a shiny application which updates in real time a table and a value based on which the table elements are computed.

e.g.

Value: 8235

Table:

image

Here the value updates in real time and the row that is highlighted is equal to the value of Column B rounded to the nearest 100.

Is there a way that the row styling can be updated as the value changes through time (e.g. if the value changes to 8300, the third row should be highlighted) - without the need for re-rendering the table as that is expensive. I believe this should be possible with client side (browser side) JS execution, but I'm unable to figure out how to implement it.

Any pointer will be most appreciated.

Regards JJ

glin commented 1 year ago

Hi, just so I understand, the table needs to be conditionally styled based on some changing data in the Shiny app, and the table data never changes?

If so, that's an interesting question, and I can't think of a great way to do this today. That conditionally styling rule will probably have to be part of the table or column options (e.g., a custom row style function), but there's no way to change either the table row style or the column definitions of an existing table today. Maybe a future enhancement to updateReactable() or the JavaScript API could make that possible though, like updateReactable(rowStyle = newRowStyleFunction) or updateReactable(columns = newColumns).

I've also been thinking about an unrelated feature to attach arbitrary metadata to a table for custom rendering/styling purposes, that doesn't have to go in the table data. That might be even easier to use. For example, the JavaScript API could provide a way to access that custom data like state.meta['myCustomValue'] that custom style functions could use, and you'd be able to update that value using updateReactable(meta = list(myCustomValue= 8300)).


While typing this up, I remembered that you can actually use a hidden column in the data to store arbitrary metadata for custom rendering/styling, and it's used pretty commonly today but just kind of hacky. An example would be the Show data from other columns example. This data can be updated using updateReactable() without rerendering the table, with the caveat that the entire table data will be sent even though you just need to update one column.

But here's a quick implementation of the "arbitrary metadata" idea using a hidden column. This Shiny app has a MASS::Cars93 table that highlights a row if the Price column matches a highlightedValue value in the hidden column. The highlightedValue value is a reactive value in Shiny that changes to a different random value every second.

library(shiny)
library(reactable)

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

# Add a hidden column for arbitrary metadata, filled with empty lists/objects
data$.meta <- list(list())

ui <- fluidPage(
  verbatimTextOutput("highlightedValue"),
  reactableOutput("table")
)

server <- function(input, output, session) {
  rv <- reactiveValues(highlightedValue = NA)

  observe({
    # Set highlighted value to a new random value from the Price column every second
    invalidateLater(1000, session)
    rv$highlightedValue <- sample(data$Price, 1)
    # Update the metadata column with the new highlighted value.
    # We don't need to fill the entire column, so just use the first row.
    data$.meta[[1]] <- list(highlightedValue = rv$highlightedValue)
    updateReactable("table", data = data)
  })

  output$highlightedValue <- renderPrint({
    reactiveValuesToList(rv)
  })

  output$table <- renderReactable({
    reactable(
      data,
      columns = list(
        # Hide the metadata column
        .meta = colDef(show = FALSE)
      ),
      # Style rows based on the highlighted value from the table metadata
      rowStyle = JS("(rowInfo, state) => {
        const meta = state.data[0]['.meta']
        if (rowInfo.values['Price'] === meta.highlightedValue) {
          return { backgroundColor: 'orange' }
        }
      }")
    )
  })
}

shinyApp(ui, server)
jyotirmoyjena commented 1 year ago

Hello,

Thank you for your response. re the clarification in your post.

just so I understand, the table needs to be conditionally styled based on some changing data in the Shiny app, and the table data never changes?

The data changes every few seconds. The change can be to any column to the dataframe (except column B) but for the purpose of simplicity I am updating the entire table. The way datatables have been conceived, I believe is to render large amounts of static data for analysis purposes. However there are multiple use cases where all the data in the table can be dynamic (think about state monitoring of multiple devices). I do not think there are elegant solutions in the R universe yet which allow for this to be implemented.

I have been able to implement this with a hacky solution of using a hidden column and styling the row with css. The hidden column can have boolean or integer data which can be used as a selector for styling a particular row. So for now it works, but as you have mentioned the solution is hacky. It'll be nice to have a JS based approach where any cell/column/row can be styled based on either the cell value or whenever a cell/row value changes where the JS is evaluated. I'm glad you've added this as an enhancement. Look forward to a nice solution.

Regards

glin commented 1 year ago

Custom metadata is now a built-in feature in latest development version:

The hacky hidden column won't be necessary anymore with this, and you can simplify the app a bit:

library(shiny)
library(reactable)

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

ui <- fluidPage(
  verbatimTextOutput("highlightedValue"),
  reactableOutput("table")
)

server <- function(input, output, session) {
  rv <- reactiveValues(highlightedValue = NA)

  observe({
    # Set highlighted value to a new random value from the Price column every second
    invalidateLater(1000, session)
    rv$highlightedValue <- sample(data$Price, 1)
    updateReactable("table", meta = list(highlightedValue = rv$highlightedValue))
  })

  output$highlightedValue <- renderPrint({
    reactiveValuesToList(rv)
  })

  output$table <- renderReactable({
    reactable(
      data,
      # Style rows based on the highlighted value from the table metadata
      rowStyle = JS("(rowInfo, state) => {
        if (rowInfo.values['Price'] === state.meta.highlightedValue) {
          return { backgroundColor: 'orange' }
        }
      }"),
      meta = list(highlightedValue = NULL)
    )
  })
}

shinyApp(ui, server)

Also, thanks for the context on the dynamically changing data. updateReactable() should work pretty well for real-time data updates as long as the dataset isn't too large. But if the data is really large, then perhaps server-side data processing could help (only send one page of data at a time, rather than the full data), or some sort of feature that allows you to patch/modify an existing dataset instead of replacing it entirely.

jyotirmoyjena commented 1 year ago

cheers, great work. thank you.