glin/reactable

Column formatting within colGroup()

JoeMarangos opened this issue · 2 comments

Hi everyone,

I've only just switched over from DT so I may be missing something obvious here.

I am having an issue with using format = colFormat within colGroup and have noticed that it can only be used within colDef.

I believe this to be a bit of an issue when you are dealing with Shiny applications as sometimes with dynamic filters or databases you may have changing columns.

As an example, lets say I have a data frame that is generated within the Shiny app. The first 5 columns are character, but the last 10 columns are percentages. Lets say the next time I generate the table the last 6 columns are percentages.

Using the colDef is impractical in this scenario as I would need to type out:

columns = list(Percentage1 = colDef(format = colFormat(perecent = T)),
                       Percntage2 = colDef(format = colFormat(perecent = T))
                        ) 

And so on... This also doesn't work if I have a dynamic database that is constantly changing its columns as otherwise I would have to have a renderReactable object for every permutation.

When I'm determining "sticky" columns the colGroup works really well. See the example code bellow for how I can use this on a dynamic database:

column_maker <- grep("Percentage",colnames(df),values=T)
columnGroups = list(
                      colGroup(name = "LOCKED",columns = c(column_marker),sticky = "left")
                    )

In this scenario I can use columnGroups and a grep function to essentially ensure that the reactable dynamically updates the sticky columns within a Shiny observeEvent. It would be great if format could be applied in this way so that:

columnGroups = list(
                      colGroup(name = "LOCKED",columns = c(column_marker),sticky = "left",format = colFormat(percent = T))
                    )

Am I missing something painfully stupid here or is this a current limitation of reactable? Would love for there to be a workaround so I can create a more dynamic Shiny environment.

Edit: Sorry, the same would also apply to things such as "footer = " ect.

Thanks,
Joe

The columns argument is a list so you can write code to generate it based on the data frame column names, types, and values. Seems fully flexible to me. For example, if you wanted to format numeric values one way and character another:

library(tidyverse)
library(reactable)

columns_input_fn <- function(x) {
  if (is.numeric(x)) {
    colDef(format = colFormat(digits = 4))
  } else {
    colDef()
  }
}

df <- iris |> head(5)
reactable(df, columns = df |> map(columns_input_fn))
glin commented

If I'm understanding correctly, I think @ArthurAndrews's suggestion of programmatically generating columns would work for these dynamic columns cases.

To add onto that, I have an unpublished doc with several examples of generating columns. Here are a few more examples that might help.

Reusing column definitions

library(reactable)

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)
)

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

Use a default column definition:

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)) 
  )
)

Create and reuse column formatters:

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)
  )
)

Use a function to generate 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())
  )
)

Use a function to generate 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()
  )
)

Use a custom render function that formats columns according to column name:

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)
    }
  )
)

Use custom format/render functions in the default column definition:

# 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)
      }
    }
  )
)

Dynamic column definitions

Assigning to a list:

columns <- list()

columns[["cyl"]] <- colDef(name = "Cylinders")

name <- "disp"
columns[[name]] <- colDef(name = "Displacement")

reactable(mtcars, columns = columns)

Using setNames():

names <- c("cyl", "disp")

columns <- setNames(
  list(
    colDef(name = "Cylinders"),
    colDef(name = "Displacement")
  ), 
  names
)

reactable(mtcars, columns = columns)

reactable(
  mtcars,
  columns = setNames(
    list(
      colDef(name = "Cylinders"),
      colDef(name = "Displacement")
    ), 
    names
  )
)

Adding columns

Use c() to combine lists:

# Separate named list of colDefs to add. Could be an empty list as well.
extra_columns <- list(
  cyl = colDef(name = "Cylinders"),
  disp = colDef(name = "Displacement")
)

reactable(
  mtcars,
  columns = c(
    list(
      mpg = colDef(name = "Miles per gallon"),
      hp = colDef(name = "Horsepower")
    ),
    extra_columns
  )
)

Column generating function

library(reactable)

data <- data.frame(
  Category = c("A", "B", "C"),
  January = c(1000, 4999, 42345),
  February = c(2000, 6342, 56734),
  March = c(3000, 6734, 75342)
)

column_defs <- function(data) {
  lapply(data, function(x) {
    if (is.numeric(x)) {
      # Numeric columns
      colDef(format = colFormat(separators = TRUE))
    } else {
      # Default columns
      colDef()
    }
  })
}

reactable(
  data,
  columns = column_defs(data)
)