glin / reactable

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

Filtering multiple sub-tables with a single filter widget #346

Open ilevantis opened 8 months ago

ilevantis commented 8 months ago

Hello and thanks for the great work on this package! It works fantastically well.

I was trying to improve an exisitng Rmarkdown HTML report by including interactive filtering for a table which contains a sub-table nested in the detail section of each row.

Here is some example code:

library(dplyr)
library(reactable)
library(htmltools)

df <- iris

summary_df <- data.frame(Species = c('setosa', 'versicolor', 'virginica'),
                         fun.fact = c('dark purple', 'medium purple', 'stripy purple'))

subtableFilter <- function(table_ids, col, min, max, value = NULL, step = NULL) {

  value <- if (!is.null(value)) value else min

  jsfunc <- paste0(
    paste0("try{Reactable.setFilter('", table_ids, "', '",col,"', this.value);}catch{}"),
    collapse = ";"
  )

  htmltools::tags$input(
    id = "subtable-filter-input",
    type = "number",
    min = min,
    max = max,
    step = step,
    value = value,
    oninput = jsfunc
  )
}

filterMinValue <- JS("function(rows, columnId, filterValue) {
  return rows.filter(function(row) {
    return row.values[columnId] >= filterValue
  })
}")

render_subtable <- function(mini_df, table_id) {
  htmlwidgets::onRender(
    reactable(mini_df,
      columns = list(Sepal.Width = colDef(filterMethod=filterMinValue)),
      elementId = table_id
    ),
    sprintf("
    function(el, x) {
      Reactable.setFilter('%s', 'Sepal.Width', 3.0);
    }
    ", table_id)
  )
}

htmltools::tagList(
  subtableFilter(paste0('subtable-', seq_along(summary_df$Species)), 'Sepal.Width', 0.0, 100.0, value = 3.0, step = 0.1),
  reactable(summary_df,
    pagination = FALSE,
    details = function(idx){
      mini_df <- df %>%
        filter(Species == summary_df$Species[idx]) %>%
        arrange(desc(Sepal.Width))
      render_subtable(mini_df, sprintf("subtable-%d",idx))
    }
  )
)

With this code, the filter widget successfully filters any subtables that are currently expanded as the value in the filter is changed. However my attempt to filter a subtable as soon as it is expanded using htmlwidgets::onRender and a hardcoded default value does not seem to work. I'm not sure how to successfully achieve this though, does anyone have any ideas?

Secondly, assuming I can get the filtering to work on a subtable as soon as it expands, how would I go about referencing the current value of the filter widget rather than a hardcoded value? I don't know enough about JS and React to properly understand how it all fits together, but from some googling it seems I would maybe need to make use of the React useRef hook?

glin commented 7 months ago

Hi, I can't think of any good way to do either unfortunately. In general, nested subtables are really hard to do state-ful things with because they're only created on details expansion, and then destroyed on details collapse or page change. Getting filter state to persist with these changes will be really tough.

For the first issue, it's probably another good reason to add a default table filter state (related: https://github.com/glin/reactable/issues/326), so you don't have to hack it in via JavaScript. Using htmlwidgets::onRender() would usually work, but I think it doesn't work here because it's not supported by how reactable renders custom HTML.

For now, I could only suggest figuring out a different way to achieve this that doesn't use nested subtables, e.g. a Shiny app with a selectable table, where row selections will render the details table in a separate panel. Of course this is completely different being a Shiny app rather than a static doc, but just one idea that I think would actually work.

ilevantis commented 7 months ago

Using htmlwidgets::onRender() would usually work, but I think it doesn't work here because it's not supported by how reactable renders custom HTML.

Fair enough. This was a nice-to -have for me not a crucial feature so no worries that I've hit up against a limitation.

I'm sure there would be a way to restructure things to not use nested tables if it was crucial.

Thanks for taking a look at my use case.