reactable icon indicating copy to clipboard operation
reactable copied to clipboard

Option to drop NAs from aggregated groups?

Open tylerlittlefield opened this issue 4 years ago • 3 comments

Take the following example:

library(reactable)

x <- data.frame(
  Group = letters[1:2],
  Value = NA
)

reactable(x, groupBy = "Group", columns = list(
  Value = colDef(
    aggregate = "count",
    format = list(
      aggregated = colFormat(suffix = " selected")
    )
  )
))

image

Is it possible to drop the NAs from the count? So instead the group would read "a (0)" and the value would read "0 Selected".

My particular use case is that I am using this in a shiny app as a kind of "filter summary". As the user starts selecting things from specific inputs, I collect them in the table above. But when the app is first initialized, nothing is selected and so my solution has been to only render specific groups once they are selected. Ideally, I'd like the table to render even if nothing is selected and display groups with count 0.

tylerlittlefield avatar Mar 04 '20 16:03 tylerlittlefield

You can drop NAs from the count using a custom aggregate function, like:

library(reactable)

x <- data.frame(
  Group = letters[1:2],
  Value = c(NA, 0)
)

reactable(x, groupBy = "Group", columns = list(
  Value = colDef(
    aggregate = JS("function(values) {
      const isNotNA = function(x) { return x != null && x !== 'NA' }
      return values.filter(isNotNA).length
    }"),
    format = list(
      aggregated = colFormat(suffix = " selected")
    )
  )
))

screenshot of table

However, there's no way to drop rows completely from a group. The group value could easily be made customizable and take a render function, allowing you to display "a (0)". But the row would still be expandable with that one NA row in the group.

I think what would fit better is a tree table with separate parent and child rows. That's not built into React Table, but it's something I've thought of adding before. In the meantime, you may be able to use expandable details + nested tables like in this example: https://glin.github.io/reactable/articles/cookbook/cookbook.html#nested-tables

For groups with 0 selected, you can return NULL from the details renderer to disable row expansion:

library(reactable)

x <- data.frame(
  Group = letters[1:2],
  Value = c(0, 2)
)

reactable(x, columns = list(
  Group = colDef(
    cell = function(value, index) {
      sprintf("%s (%s)", value, x$Value[index])
    },
    detail = function(index) {
      if (x$Value[index] > 0) {
        reactable(iris, outlined = TRUE)
      }
    }
  ),
  Value = colDef(
    format = colFormat(suffix = " selected")
  )
))

screenshot of table

glin avatar Mar 07 '20 16:03 glin

Awesome, thank you for the thorough response. We have gone with the nested table route, specifically something based on this example.

This has been such a fun package to play with!

tylerlittlefield avatar Mar 09 '20 18:03 tylerlittlefield

It's still not possible to drop rows from a group, but at least you can now customize the group value (https://github.com/glin/reactable/commit/1297d0cd28c1828bea8a9276d203715ac063b6f9).

  • colDef() gains a grouped argument to customize rendering for grouped cells in groupBy columns (#33, #94, #148).

Here's an example of excluding numeric NAs from the row count:

data <- data.frame(
  Group = c("a", "a", "b"),
  Value = c(1, NA, NA)
)

reactable(data, groupBy = "Group", columns = list(
  Group = colDef(
    grouped = JS("function(cellInfo) {
      const nonNAValues = cellInfo.subRows.filter(function(row) {
        return typeof row['Value'] === 'number'
      })
      return cellInfo.value + ' (' + nonNAValues.length + ')'
    }")
  ),
  Value = colDef(na = "NA")
))

reactable output

Or if the values are a character/date column, you can adjust this to filter out non-string values:

data <- data.frame(
  Group = c("a", "a", "b"),
  Value = c("X", NA, NA)
)

reactable(data, groupBy = "Group", columns = list(
  Group = colDef(
    grouped = JS("function(cellInfo) {
      const nonNAValues = cellInfo.subRows.filter(function(row) {
        return typeof row['Value'] === 'string'
      })
      return cellInfo.value + ' (' + nonNAValues.length + ')'
    }")
  ),
  Value = colDef(na = "NA")
))

glin avatar May 15 '21 22:05 glin