Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

additional support for column groups in interactive tables #1618 #1623

Merged
merged 4 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

* `gtsave()` saves correctly to .rtf if using `cols_label()` and `summary_rows()` or `grand_summary_rows()` (@olivroy, #1233)

* interactive tables are now rendering the first level of column groups, added by `tab_spanner()` (@obsaditelnost, #1618)

# gt 0.10.1

## Improvements to nanoplots
Expand Down
55 changes: 51 additions & 4 deletions R/render_as_i_html.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ render_as_ihtml <- function(data, id) {
# Determine if the rendered table should have a header section
has_header_section <- dt_heading_has_title(data = data)

# Determine if there are tab spanners
has_tab_spanners <- dt_spanners_exists(data = data)

# Obtain the language from the `locale`, if provided
locale <- dt_locale_get_value(data = data)

Expand Down Expand Up @@ -340,6 +343,48 @@ render_as_ihtml <- function(data, id) {
footer_component <- NULL
}

# Generate the column groups, if there are any tab_spanners
colgroups_def <- NULL

if (has_tab_spanners) {
col_groups <- (dt_spanners_get(data = data) %>% dplyr::filter(spanner_level == 1))

if (max(dt_spanners_get(data = data)$spanner_level) > 1) {
first_colgroups <- base::paste0(col_groups$built, collapse = "|")

cli::cli_warn(c(
"When displaying an interactive gt table, there must not be more than 1 level of column groups (tab_spanners)",
"*" = "Currently there are {max(dt_spanners_get(data = data)$spanner_level)} levels of tab spanners.",
"i" = "Only the first level will be used for the interactive table, that is: [{first_colgroups}]"
))
}

colgroups_def <-
apply(
col_groups, 1,
FUN = function(x) {
reactable::colGroup(
name = x$spanner_label,
columns = x$vars,
header = x$built,
html = TRUE,
align = NULL,
headerVAlign = NULL,
sticky = NULL,
headerClass = NULL,
headerStyle = list(
fontWeight = "normal",
borderBottomStyle = column_labels_border_bottom_style,
borderBottomWidth = column_labels_border_bottom_width,
borderBottomColor = column_labels_border_bottom_color,
marginLeft = "4px",
marginRight = "4px"
)
)
}
)
}

# Generate the default theme for the table
tbl_theme <-
reactable::reactableTheme(
Expand All @@ -353,15 +398,17 @@ render_as_ihtml <- function(data, id) {
style = list(
fontFamily = font_family_str
),
tableStyle = NULL,
headerStyle = list(
tableStyle = list(
borderTopStyle = column_labels_border_top_style,
borderTopWidth = column_labels_border_top_width,
borderTopColor = column_labels_border_top_color,
borderTopColor = column_labels_border_top_color
),
headerStyle = list(
borderBottomStyle = column_labels_border_bottom_style,
borderBottomWidth = column_labels_border_bottom_width,
borderBottomColor = column_labels_border_bottom_color
),
# individually defined for the margins left+right
groupHeaderStyle = NULL,
tableBodyStyle = NULL,
rowGroupStyle = NULL,
Expand All @@ -387,7 +434,7 @@ render_as_ihtml <- function(data, id) {
reactable::reactable(
data = data_tbl,
columns = col_defs,
columnGroups = NULL,
columnGroups = colgroups_def,
rownames = NULL,
groupBy = NULL,
sortable = use_sorting,
Expand Down
4 changes: 4 additions & 0 deletions R/tab_create_modify.R
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ tab_header <- function(
#' and `spanners`, placing the spanner label where it will fit. The first
#' spanner level (right above the column labels) is `1`.
#'
#' In combination with [opt_interactive()] or `ihtml.active = TRUE` in
#' [tab_options()] only level `1` is supported, additional levels would be
#' discarded.
#'
#' @param id *Spanner ID*
#'
#' `scalar<character>` // *default:* `label`
Expand Down
5 changes: 5 additions & 0 deletions inst/css/gt_styles_default.scss
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,9 @@
display: inline-flex !important;
margin-bottom: 0.75em !important;
}

div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
height: 0px !important;
}

}
6 changes: 5 additions & 1 deletion man/tab_spanner.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions tests/testthat/_snaps/tab_spanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# multiple levels of `tab_spanner()` are not compatible with interactive tables [plain]

Code
local({
tmp <- exibble[, 1:4] %>% gt() %>% tab_spanner(label = "spanner_numdat",
columns = c(num, date)) %>% tab_spanner(label = "spanner_char", columns = c(
char)) %>% tab_spanner(label = "spanner_numdatchar", columns = c(num, date,
char)) %>% opt_interactive()
})

189 changes: 189 additions & 0 deletions tests/testthat/test-tab_spanner.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ selection_text <- function(html, selection) {
rvest::html_text(rvest::html_nodes(html, selection))
}

# returns the json-object of the reactable javascript-part
reactive_table_to_json <- function(reactable_obj){
tmp_html_tag <- reactable_obj %>%
htmltools::as.tags() %>%
htmltools::doRenderTags() %>%
stringr::str_match(pattern = '<script type="application/json" data-for="[^>]+>(?<found>.+)</script>') %>%
tibble::as_tibble(.name_repair = "minimal") %>%
.$found %>%
as.character() %>%
jsonlite::parse_json()
}

test_that("A gt table contains the expected spanner column labels", {

# Check that specific suggested packages are available
Expand Down Expand Up @@ -935,3 +947,180 @@ test_that("The `dt_spanners_print_matrix()` util function works well", {
)
)
})

test_that("`tab_spanner()` is compatible with interactive tables", {
check_suggests()
skip_if_not_installed("jsonlite")
skip_if_not_installed("tibble")

#
# simple example with 1 spanner, 1 level
#

# reactable should have the expected spanners
interactive_tbl <-
exibble[, 1:4] %>%
gt() %>%
tab_spanner(label = "spanner_datechar", columns = c(date, char)) %>%
opt_interactive() %>%
reactive_table_to_json()

interactive_tbl_colgroups <- do.call(rbind, interactive_tbl$x$tag$attribs$columnGroups) %>%
tibble::as_tibble() %>%
dplyr::mutate(across(c(-columns), ~ .x %>% sapply("[[", 1)))

testthat::expect_equal(nrow(interactive_tbl_colgroups), 1)
testthat::expect_equal(
interactive_tbl_colgroups$name %>% sort(),
c("spanner_datechar")
)

# expected spanners should contain the expected cols
testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_datechar") %>% .$columns %>% .[[1]] %>% sapply("[[", 1) %>% sort(),
c("char", "date")
)

#
# more complex example with multiple spanners, 1 level
#

interactive_tbl <-
exibble[, 1:4] %>%
gt() %>%
tab_spanner(label = "spanner_numchar", columns = c(num, char)) %>%
tab_spanner(label = "spanner_dat", columns = c(date)) %>%
opt_interactive() %>%
reactive_table_to_json()

interactive_tbl_colgroups <- do.call(rbind, interactive_tbl$x$tag$attribs$columnGroups) %>%
tibble::as_tibble() %>%
dplyr::mutate(across(c(-columns), ~ .x %>% sapply("[[", 1)))

testthat::expect_equal(nrow(interactive_tbl_colgroups), 2)
testthat::expect_equal(
interactive_tbl_colgroups$name %>% sort(),
c("spanner_dat", "spanner_numchar")
)

# expected spanners should contain the expected cols
testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_numchar") %>% .$columns %>% .[[1]] %>% sapply("[[", 1) %>% sort(),
c("char", "num")
)

testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_dat") %>% .$columns %>% .[[1]] %>% sapply("[[", 1) %>% sort(),
c("date")
)

#
# MD, HTML should be rendered - raw text should be escaped
#

interactive_tbl <-
exibble[, 1:4] %>%
gt() %>%
tab_spanner(label = md("*md spanner*"), columns = c(num)) %>%
tab_spanner(label = html("<u>html spanner</u>"), columns = c(date)) %>%
tab_spanner(label = "normal spanner with <u>tags</u> and *more*", columns = c(char)) %>%
opt_interactive() %>%
reactive_table_to_json()

interactive_tbl_colgroups <- do.call(rbind, interactive_tbl$x$tag$attribs$columnGroups) %>%
tibble::as_tibble() %>%
dplyr::mutate(across(c(-columns), ~ .x %>% sapply("[[", 1)))

testthat::expect_match(interactive_tbl_colgroups %>% dplyr::filter(name == "*md spanner*") %>% dplyr::select(header) %>% as.character(),
regexp = "<span.+md spanner.+"
)

testthat::expect_match(interactive_tbl_colgroups %>% dplyr::filter(name == "<u>html spanner</u>") %>% dplyr::select(header) %>% as.character(),
regexp = "<u>html spanner</u>"
)

testthat::expect_match(interactive_tbl_colgroups %>% dplyr::filter(name == "normal spanner with <u>tags</u> and *more*") %>% dplyr::select(header) %>% as.character(),
regexp = "normal spanner with &lt;u&gt;.+"
)

#
# spanners with same name but different ID must not be a problem
#

interactive_tbl <-
exibble[, 1:4] %>%
gt() %>%
tab_spanner(label = "spanner_label", columns = c(num), id = 1) %>%
tab_spanner(label = "spanner_label", columns = c(date, fctr), id = 2) %>%
tab_spanner(label = "another_label", columns = c(char), id = 3) %>%
opt_interactive() %>%
reactive_table_to_json()

interactive_tbl_colgroups <- do.call(rbind, interactive_tbl$x$tag$attribs$columnGroups) %>%
tibble::as_tibble() %>%
dplyr::mutate(across(c(-columns), ~ .x %>% sapply("[[", 1))) %>%
dplyr::rowwise() %>%
dplyr::mutate(size = length(columns)) %>%
dplyr::arrange(size)

testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_label") %>% .$columns %>% .[[1]] %>% sapply("[[", 1) %>% sort(),
"num"
)

testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_label") %>% .$columns %>% .[[2]] %>% sapply("[[", 1) %>% sort(),
c("date", "fctr")
)
})

# spanners with multiple levels result in a warning message
cli::test_that_cli("multiple levels of `tab_spanner()` are not compatible with interactive tables",
configs = c("plain"), code = {
check_suggests()

expect_snapshot(local({
tmp <- exibble[, 1:4] %>%
gt() %>%
tab_spanner(label = "spanner_numdat", columns = c(num, date)) %>%
tab_spanner(label = "spanner_char", columns = c(char)) %>%
tab_spanner(label = "spanner_numdatchar", columns = c(num, date, char)) %>%
opt_interactive()
}))
}
)

test_that("multiple levels of `tab_spanner()` are not compatible with interactive tables and only use 1st level", {
check_suggests()
skip_if_not_installed("jsonlite")

suppressWarnings({
interactive_tbl <-
exibble[, 1:4] %>%
gt() %>%
tab_spanner(label = "spanner_numdat", columns = c(num, date)) %>%
tab_spanner(label = "spanner_char", columns = c(char)) %>%
tab_spanner(label = "spanner_numdatchar", columns = c(num, date, char)) %>%
opt_interactive() %>%
reactive_table_to_json()
})


interactive_tbl_colgroups <- do.call(rbind, interactive_tbl$x$tag$attribs$columnGroups) %>%
tibble::as_tibble() %>%
dplyr::mutate(across(c(-columns), ~ .x %>% sapply("[[", 1))) %>%
dplyr::rowwise() %>%
dplyr::mutate(size = length(columns)) %>%
dplyr::arrange(size)

# expected spanners should contain the expected cols
testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_char") %>% .$columns %>% .[[1]] %>% sapply("[[", 1) %>% sort(),
c("char")
)

testthat::expect_equal(
interactive_tbl_colgroups %>% dplyr::filter(name == "spanner_numdat") %>% .$columns %>% .[[1]] %>% sapply("[[", 1) %>% sort(),
c("date", "num")
)
})
Loading