glin / reactable

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

format multiply columns #197

Open XiangyunHuang opened 3 years ago

XiangyunHuang commented 3 years ago

Thanks @glin for the greate package! There may be a feature request. To illustrate, I simulate a dataset, df

df <- data.frame(
  A = runif(100),
  B = rnorm(100),
  C = rnorm(n = 100, mean = 10, sd = 2),
  D = rnorm(n = 100, mean = 10, sd = 2)
)

Using reactable to format column A and B to percent formart, we need

library(reactable)

reactable(df, columns = list(
  A = colDef(format = colFormat(percent = T, digits = 2)), # call firstly
  B = colDef(format = colFormat(percent = T, digits = 2)), # call again
  C = colDef(format = colFormat(percent = F, digits = 2)), # call firstly
  D = colDef(format = colFormat(percent = F, digits = 2))  # call again
))

However, function formatPercentage() support some columns, simplify

library(DT)
library(magrittr)

DT::datatable(df) %>% 
  DT::formatPercentage(columns = c("A", "B"), digits = 2) %>% 
  DT::formatRound(columns = c("C", "D"), digits = 3)
glin commented 3 years ago

I know this is a really common issue, especially when you're working on more complex tables with many columns. There are a few options for reusing column definition code today, but they aren't well documented. I'd like to add better documentation for this at some point, but here are some tips for now:

Default column definitions

The first option I recommend is to use a default column definition if possible. For example:

reactable(
  df,
  defaultColDef = colDef(format = colFormat(percent = TRUE, digits = 2)),
  columns = list(
    C = colDef(format = colFormat(percent = FALSE, digits = 2)),
    D = colDef(format = colFormat(percent = FALSE, digits = 2)) 
  )
)

Default column definitions work really well in some cases, but they don't reduce too much repetition here.

Variables

The next option is to store column definitions or formatters in a variable that can be reused. colDef() and colFormat() are just objects (or named lists) in R that can be created outside of reactable() and passed around. For example, to reuse the column formatting options, you can create a colFormat() for a percent column, and a colFormat() for a numeric column:

pct_format <- colFormat(percent = TRUE, digits = 2)
num_format <- colFormat(digits = 2)

reactable(
  df,
  columns = list(
    A = colDef(format = pct_format),
    B = colDef(format = pct_format),
    C = colDef(format = num_format),
    D = colDef(format = num_format)
  )
)

This cuts down more repetition than before, but is still somewhat limited to simpler cases.

Functions

When you need to reuse more complex code, another option would be to use functions. It's like how you'd reuse R code in general. You can write functions to generate colDef() or colFormat() objects, or to format data values, and use those as reusable blocks in your table.

For example, here's a function to reuse column formatters:

num_format <- function(percent = FALSE) {
  colFormat(percent = percent, digits = 2)
}

reactable(
  df,
  columns = list(
    A = colDef(format = num_format(percent = TRUE)),
    B = colDef(format = num_format(percent = TRUE)),
    C = colDef(format = num_format()),
    D = colDef(format = num_format())
  )
)

Or a function to reuse column definitions:

num_column <- function(percent = FALSE) {
  colDef(format = colFormat(percent = percent, digits = 2))
}

reactable(
  df,
  columns = list(
    A = num_column(percent = TRUE),
    B = num_column(percent = TRUE),
    C = num_column(),
    D = num_column()
  )
)

You could also combine the default column definition with a custom formatting function that understands which columns are numeric vs. percentage types:

reactable(
  df,
  defaultColDef = colDef(
    cell = function(value, index, name) {
      suffix <- ""
      # Format percent columns
      if (name %in% c("A", "B")) {
        value <- value * 100
        suffix <- "%"
      }
      value <- formatC(value, digits = 2, format = "f")
      paste0(value, suffix)
    }
  )
)

This default column definition only works in this specific table, so you could extract the data formatting logic into functions that could be reused across multiple tables:

# Generic formatting functions for percent or numeric values
fmt_pct <- function(value) paste0(formatC(value * 100, digits = 2, format = "f"), "%")
fmt_num <- function(value) formatC(value, digits = 2, format = "f")

reactable(
  df,
  defaultColDef = colDef(
    cell = function(value, index, name) {
      if (name %in% c("A", "B")) {
        fmt_pct(value)
      } else {
        fmt_num(value)
      }
    }
  )
)

Examples and other comments

There are some examples of these techniques in the docs, particularly the Women's World Cup demo where there are a ton of columns, and groups of columns with similar properties. So far, code reuse via functions has worked pretty well for these complex tables.

Also, based on experience with building tables in the past, I really like having all the column configuration to be co-located in one place (the column definition). Maintaining a complex table gets easier when I know exactly where to look to change a column. Writing out repetitive column definitions might take more effort in the beginning, but it makes change easier in the long term.

Configuration that works across columns like DT::formatPercentage(columns = c("A", "B"), ...) works nicely when you have a simple table, but isn't as effective in larger tables where you have to look in multiple places to see how a column is customized. It's also harder to change because it touches multiple columns. If you only want to change one column, you have to be careful not to mess up the other columns, or you have to split the config across multiple columns anyway.

Since similar questions have come up before, you might be interested in these other issues: