diff --git a/DESCRIPTION b/DESCRIPTION index ad8c8c76b..e277c69e3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: gtsummary Title: Presentation-Ready Data Summary and Analytic Result Tables -Version: 2.0.4.9006 +Version: 2.0.4.9007 Authors@R: c( person("Daniel D.", "Sjoberg", , "danield.sjoberg@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-0862-2018")), diff --git a/NAMESPACE b/NAMESPACE index 41c62aa02..dd3bb5f20 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -167,6 +167,7 @@ export(modify_fmt_fun) export(modify_footnote) export(modify_footnote_body) export(modify_footnote_header) +export(modify_footnote_spanning_header) export(modify_header) export(modify_source_note) export(modify_spanning_header) @@ -188,8 +189,10 @@ export(ratio_summary) export(remove_abbreviation) export(remove_footnote_body) export(remove_footnote_header) +export(remove_footnote_spanning_header) export(remove_row_type) export(remove_source_note) +export(remove_spanning_header) export(reset_gtsummary_theme) export(scope_header) export(scope_table_body) diff --git a/NEWS.md b/NEWS.md index 9ad9f9a32..eeecd7c6b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,10 @@ * Previously, source notes were an undocumented feature and only a single source note could be included in a table. We now export `modify_source_note()` and `remove_source_note()` to add and remove any number of source notes. Also, when merging and stacking tables, previously due to the one source note limit, only the first source note was retained. Now all source notes will be included below the resulting table. _This is different behavior compared to previous versions of the package and in rare cases may result in a different source note._ Moreover, `kableExtra` output now supports source notes, where previously they were omitted. +* The `modify_spanning_header(level)` argument has been added to allow for multiple levels of spanning headers in the resulting tables. The `remove_spanning_header()` function has also been added to ease the removal of spanning headers. (#2099) + +* The `modify_footnote_spanning_header()` function has been added to ease adding footnotes to spanning headers. A companion function, `remove_footnote_spanning_header()`, has been added to remove spanning headers. + * Language translations have been updated with a handful of missing translations. (#2100) * The `modify_caption(caption)` argument now accepts a vector of captions, instead of just a string. Note, however, that not all print engines support a vector of captions. (#2107) diff --git a/R/as_flex_table.R b/R/as_flex_table.R index fd6206527..99cef4cba 100644 --- a/R/as_flex_table.R +++ b/R/as_flex_table.R @@ -118,51 +118,60 @@ table_styling_to_flextable_calls <- function(x, ...) { # add_header_row ------------------------------------------------------------- # this is the spanning rows - any_spanning_header <- any(!is.na(x$table_styling$header$spanning_header)) + any_spanning_header <- nrow(x$table_styling$spanning_header) > 0L if (any_spanning_header == FALSE) { flextable_calls[["add_header_row"]] <- list() } else { - df_header0 <- - x$table_styling$header |> - dplyr::filter(.data$hide == FALSE) |> - dplyr::select("spanning_header") |> + flextable_calls[["add_header_row"]] <- + tidyr::expand_grid( + level = unique(x$table_styling$spanning_header$level), + column = x$table_styling$header$column[!x$table_styling$header$hide] + ) |> + dplyr::left_join( + x$table_styling$spanning_header[c("level", "column", "spanning_header")], + by = c("level", "column") + ) |> dplyr::mutate( - spanning_header = ifelse(is.na(.data$spanning_header), - " ", .data$spanning_header - ), + .by = "level", + spanning_header = + ifelse(is.na(.data$spanning_header), " ", .data$spanning_header), spanning_header_id = dplyr::row_number() - ) - # assigning an ID for each spanning header group - for (i in seq(2, nrow(df_header0))) { - if (df_header0$spanning_header[i] == df_header0$spanning_header[i - 1]) { - df_header0$spanning_header_id[i] <- df_header0$spanning_header_id[i - 1] - } - } - - df_header <- - df_header0 |> - dplyr::group_by(.data$spanning_header_id) |> - dplyr::mutate(width = dplyr::n()) |> - dplyr::distinct() |> - dplyr::ungroup() |> - dplyr::mutate( - column_id = map2(.data$spanning_header_id, .data$width, ~ seq(.x, .x + .y - 1L, by = 1L)) - ) + ) |> + dplyr::group_by(.data$level) |> + dplyr::group_map( + \(df_values, df_group) { + # assigning an ID for each spanning header group + for (i in seq(2, nrow(df_values))) { + if (df_values$spanning_header[i] == df_values$spanning_header[i - 1]) { + df_values$spanning_header_id[i] <- df_values$spanning_header_id[i - 1] + } + } - flextable_calls[["add_header_row"]] <- list( - expr( - # add the header row with the spanning headers - flextable::add_header_row( - values = !!df_header$spanning_header, - colwidths = !!df_header$width - ) - ) - ) + df_header <- + dplyr::bind_cols(df_group, df_values) |> + dplyr::select(-"column") |> + dplyr::group_by(.data$spanning_header_id) |> + dplyr::mutate(width = dplyr::n()) |> + dplyr::distinct() |> + dplyr::ungroup() |> + dplyr::mutate( + column_id = map2(.data$spanning_header_id, .data$width, ~ seq(.x, .x + .y - 1L, by = 1L)) + ) - flextable_calls[["compose_header_row"]] <- - .chr_with_md_to_ft_compose( - x = df_header$spanning_header, - j = df_header$column_id + c( + list(expr( + # add the header row with the spanning headers + flextable::add_header_row( + values = !!df_header$spanning_header, + colwidths = !!df_header$width + ) + )), + .chr_with_md_to_ft_compose( + x = df_header$spanning_header, + j = df_header$column_id + ) + ) + } ) } @@ -207,20 +216,28 @@ table_styling_to_flextable_calls <- function(x, ...) { flextable_calls[["autofit"]] <- expr(flextable::autofit()) # footnote_header ------------------------------------------------------------ + spanning_header_lvls <- x$table_styling$spanning_header$level |> append(0L) |> max() df_footnote_header <- - .number_footnotes(x, "footnote_header") |> - tidyr::nest(df_location = c("column", "column_id")) |> + dplyr::bind_rows( + x$table_styling$footnote_header |> dplyr::mutate(level = 0L), + x$table_styling$footnote_spanning_header + ) |> + dplyr::mutate( + row_numbers = .env$spanning_header_lvls - .data$level + 1L + ) %>% + .number_footnotes(x, type = .) |> + tidyr::nest(df_location = c("column", "column_id", "row_numbers")) |> dplyr::mutate( + row_numbers = map(.data$df_location, ~ getElement(.x, "row_numbers")), column_id = map(.data$df_location, ~ getElement(.x, "column_id")) ) - header_i_index <- ifelse(any_spanning_header == TRUE, 2L, 1L) flextable_calls[["footnote_header"]] <- map( seq_len(nrow(df_footnote_header)), ~ expr( flextable::footnote( - i = !!header_i_index, + i = !!df_footnote_header$row_numbers[[.x]], j = !!df_footnote_header$column_id[[.x]], value = flextable::as_paragraph(!!df_footnote_header$footnote[[.x]]), part = "header", @@ -231,7 +248,7 @@ table_styling_to_flextable_calls <- function(x, ...) { # footnote_body -------------------------------------------------------------- df_footnote_body <- - .number_footnotes(x, "footnote_body", start_with = nrow(df_footnote_header)) |> + .number_footnotes(x, type = x$table_styling$footnote_body, start_with = nrow(df_footnote_header)) |> tidyr::nest(df_location = c("column", "column_id", "row_numbers")) |> dplyr::mutate( row_numbers = map(.data$df_location, ~ getElement(.x, "row_numbers")), @@ -252,7 +269,6 @@ table_styling_to_flextable_calls <- function(x, ...) { ) ) - # abbreviation --------------------------------------------------------------- flextable_calls[["abbreviations"]] <- case_switch( diff --git a/R/as_gt.R b/R/as_gt.R index 17c318ce4..c8328ec0d 100644 --- a/R/as_gt.R +++ b/R/as_gt.R @@ -234,6 +234,25 @@ table_styling_to_gt_calls <- function(x, ...) { expr(gt::cols_label_with(fn = function(x) gsub(x = x, pattern = "\\n(?!\\\\)", replacement = "", fixed = FALSE, perl = TRUE))) ) + # spanning_header ------------------------------------------------------------ + gt_calls[["tab_spanner"]] <- + case_switch( + nrow(x$table_styling$spanning_header) > 0L ~ + x$table_styling$spanning_header |> + dplyr::group_by(.data$level, .data$spanning_header, .data$text_interpret) |> + dplyr::group_map( + \(.x, .y) { + expr(gt::tab_spanner( + columns = !!.x$column, + label = !!call2(parse_expr(.y$text_interpret), .y$spanning_header), + level = !!.y$level, + id = !!paste0("level ", .y$level, "; ", .x$column[1]), + gather = FALSE + )) + } + ), + .default = list() + ) # tab_footnote --------------------------------------------------------------- gt_calls[["tab_footnote"]] <- @@ -270,32 +289,27 @@ table_styling_to_gt_calls <- function(x, ...) { ) ) } - ) - ) - - # spanning_header ------------------------------------------------------------ - df_spanning_header <- - x$table_styling$header |> - dplyr::select("column", "interpret_spanning_header", "spanning_header") |> - dplyr::filter(!is.na(.data$spanning_header)) |> - tidyr::nest(cols = "column") |> - dplyr::mutate( - spanning_header = map2( - .data$interpret_spanning_header, .data$spanning_header, - ~ call2(parse_expr(.x), .y) ), - cols = map(.data$cols, ~ dplyr::pull(.x)) - ) |> - dplyr::select("spanning_header", "cols") - - gt_calls[["tab_spanner"]] <- - map( - seq_len(nrow(df_spanning_header)), - ~ expr(gt::tab_spanner( - columns = !!df_spanning_header$cols[[.x]], - label = gt::md(!!df_spanning_header$spanning_header[[.x]]), - gather = FALSE - )) + # spanning header footnotes + map( + seq_len(nrow(x$table_styling$footnote_spanning_header)), + function(i) { + expr( + gt::tab_footnote( + footnote = + !!call2( + parse_expr(x$table_styling$footnote_spanning_header$text_interpret[i]), + x$table_styling$footnote_spanning_header$footnote[i] + ), + locations = + gt::cells_column_spanners( + spanners = !!paste0("level ", x$table_styling$footnote_spanning_header$level[i], "; ", x$table_styling$footnote_spanning_header$column[i]), + levels = !!x$table_styling$footnote_spanning_header$level[i] + ) + ) + ) + } + ) ) # horizontal_line ------------------------------------------------------------ diff --git a/R/as_hux_table.R b/R/as_hux_table.R index c9e9cfe26..468afc20f 100644 --- a/R/as_hux_table.R +++ b/R/as_hux_table.R @@ -187,8 +187,9 @@ table_styling_to_huxtable_calls <- function(x, ...) { # footnote ------------------------------------------------------------------- vct_footnote <- dplyr::bind_rows( - .number_footnotes(x, "footnote_header"), - .number_footnotes(x, "footnote_body") + .number_footnotes(x, x$table_styling$footnote_spanning_header), + .number_footnotes(x, x$table_styling$footnote_header), + .number_footnotes(x, x$table_styling$footnote_body) ) |> dplyr::pull("footnote") %>% unique() @@ -331,30 +332,35 @@ table_styling_to_huxtable_calls <- function(x, ...) { expr(huxtable::insert_row(after = 0, !!!col_labels)) ) - any_spanning_header <- sum(!is.na(x$table_styling$header$spanning_header)) > 0 - if (any_spanning_header) { - header_content <- x$table_styling$header$spanning_header[x$table_styling$header$hide == FALSE] - huxtable_calls[["insert_row"]] <- append( - huxtable_calls[["insert_row"]], - expr(huxtable::insert_row(after = 0, !!!header_content)) - ) - - header_colspans <- rle(header_content)$lengths - header_colspan_cols <- cumsum(c( - 1, - header_colspans[-length(header_colspans)] - )) - huxtable_calls[["insert_row"]] <- append( - huxtable_calls[["insert_row"]], - expr( - huxtable::set_colspan( - row = 1, col = !!header_colspan_cols, - value = !!header_colspans - ) + if (nrow(x$table_styling$spanning_header) > 0L) { + huxtable_calls[["insert_row"]] <- + huxtable_calls[["insert_row"]] |> + append( + tidyr::expand_grid( + level = unique(x$table_styling$spanning_header$level), + column = x$table_styling$header$column[!x$table_styling$header$hide] + ) |> + dplyr::left_join( + x$table_styling$spanning_header[c("level", "column", "spanning_header")], + by = c("level", "column") + ) |> + dplyr::group_by(.data$level) |> + dplyr::group_map( + \(df_values, df_group) { + header_content <- df_values$spanning_header + header_colspans <- rle(header_content)$lengths + header_colspan_cols <- cumsum(c(1, header_colspans[-length(header_colspans)])) + + list( + expr(huxtable::insert_row(after = 0, !!!header_content)), + expr(huxtable::set_colspan(row = 1, col = !!header_colspan_cols, value = !!header_colspans)) + ) + } + ) ) - ) } - header_bottom_row <- if (any_spanning_header) 2 else 1 + + header_bottom_row <- length(unique(x$table_styling$spanning_header$level)) + 1L huxtable_calls[["insert_row"]] <- append( huxtable_calls[["insert_row"]], expr( @@ -366,7 +372,7 @@ table_styling_to_huxtable_calls <- function(x, ...) { ) # set_markdown --------------------------------------------------------------- - header_rows <- switch(any_spanning_header, 1:2) %||% 1L # styler: off + header_rows <- seq_len(length(unique(x$table_styling$spanning_header$level)) + 1L) # styler: off huxtable_calls[["set_markdown"]] <- list( set_markdown = diff --git a/R/as_kable_extra.R b/R/as_kable_extra.R index 19dfe55e1..4569b5f86 100644 --- a/R/as_kable_extra.R +++ b/R/as_kable_extra.R @@ -181,12 +181,10 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... # only removing from header and spanning header, as this is where default markdown # formatting is placed in a gtsummary object else { - x$table_styling$header <- - x$table_styling$header %>% - dplyr::mutate( - label = .strip_markdown(.data$label), - spanning_header = .strip_markdown(.data$spanning_header) - ) + x$table_styling$header$label <- + .strip_markdown(x$table_styling$header$label) + x$table_styling$spanning_header$spanning_header <- + .strip_markdown(x$table_styling$spanning_header$spanning_header) } # getting kable calls @@ -304,35 +302,55 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... } # add_header_above ----------------------------------------------------------- - if (any(!is.na(x$table_styling$header$spanning_header))) { - df_header0 <- - x$table_styling$header |> - dplyr::filter(.data$hide == FALSE) |> - dplyr::select("spanning_header") |> + # this is the spanning rows + any_spanning_header <- nrow(x$table_styling$spanning_header) > 0L + if (any_spanning_header == FALSE) { + kable_extra_calls[["add_header_above"]] <- list() + } else { + kable_extra_calls[["add_header_above"]] <- + tidyr::expand_grid( + level = unique(x$table_styling$spanning_header$level), + column = x$table_styling$header$column[!x$table_styling$header$hide] + ) |> + dplyr::left_join( + x$table_styling$spanning_header[c("level", "column", "spanning_header")], + by = c("level", "column") + ) |> dplyr::mutate( - spanning_header = ifelse(is.na(.data$spanning_header), - " ", .data$spanning_header - ), + .by = "level", + spanning_header = + ifelse(is.na(.data$spanning_header), " ", .data$spanning_header), spanning_header_id = dplyr::row_number() - ) - # assigning an ID for each spanning header group - for (i in seq(2, nrow(df_header0))) { - if (df_header0$spanning_header[i] == df_header0$spanning_header[i - 1]) { - df_header0$spanning_header_id[i] <- df_header0$spanning_header_id[i - 1] - } - } - - df_header <- - df_header0 |> - dplyr::group_by(.data$spanning_header_id) %>% - dplyr::mutate(width = dplyr::n()) %>% - dplyr::distinct() %>% - dplyr::ungroup() + ) |> + dplyr::group_by(.data$level) |> + dplyr::group_map( + \(df_values, df_group) { + # assigning an ID for each spanning header group + for (i in seq(2, nrow(df_values))) { + if (df_values$spanning_header[i] == df_values$spanning_header[i - 1]) { + df_values$spanning_header_id[i] <- df_values$spanning_header_id[i - 1] + } + } - header <- df_header$width |> set_names(df_header$spanning_header) + df_header <- + dplyr::bind_cols(df_group, df_values) |> + dplyr::select(-"column") |> + dplyr::group_by(.data$spanning_header_id) |> + dplyr::mutate(width = dplyr::n()) |> + dplyr::distinct() |> + dplyr::ungroup() |> + dplyr::mutate( + column_id = map2(.data$spanning_header_id, .data$width, ~ seq(.x, .x + .y - 1L, by = 1L)) + ) - kable_extra_calls[["add_header_above"]] <- - expr(kableExtra::add_header_above(header = !!header, escape = !!escape)) + expr( + kableExtra::add_header_above( + header = !!(df_header$width |> stats::setNames(df_header$spanning_header)), + escape = !!escape + ) + ) + } + ) } # horizontal_line_above ------------------------------------------------------ @@ -384,8 +402,9 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... # footnote ------------------------------------------------------------------- vct_footnote <- dplyr::bind_rows( - .number_footnotes(x, "footnote_header"), - .number_footnotes(x, "footnote_body") + .number_footnotes(x, x$table_styling$footnote_spanning_header), + .number_footnotes(x, x$table_styling$footnote_header), + .number_footnotes(x, x$table_styling$footnote_body) ) |> dplyr::pull("footnote") %>% unique() @@ -520,8 +539,8 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... linebreaker = linebreaker ) - x$table_styling$header$spanning_header <- - .escape_latex2(x$table_styling$header$spanning_header, newlines = FALSE) %>% + x$table_styling$spanning_header$spanning_header <- + .escape_latex2(x$table_styling$spanning_header$spanning_header, newlines = FALSE) %>% .markdown_to_latex2() %>% kableExtra::linebreak( align = "c", @@ -564,8 +583,8 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... x$table_styling$header$label <- .strip_markdown(x$table_styling$header$label) %>% .escape_html() - x$table_styling$header$spanning_header <- - .strip_markdown(x$table_styling$header$spanning_header) %>% + x$table_styling$spanning_header$spanning_header <- + .strip_markdown(x$table_styling$spanning_header$spanning_header) %>% .escape_html() # removing line breaks from footnotes diff --git a/R/bold_italicize_labels_levels.R b/R/bold_italicize_labels_levels.R index 4ec98864c..a30554696 100644 --- a/R/bold_italicize_labels_levels.R +++ b/R/bold_italicize_labels_levels.R @@ -148,22 +148,31 @@ bold_labels.tbl_cross <- function(x) { cols_to_style <- select(x$table_body, all_stat_cols(FALSE)) %>% - names() + names() |> + # remove hidden columns + setdiff(x$table_styling$header$column[x$table_styling$header$hide]) + + x$table_styling$spanning_header <- + x$table_styling$spanning_header |> + dplyr::mutate( + spanning_header = + ifelse( + .data$column %in% .env$cols_to_style & !is.na(.data$spanning_header), + paste0("**", .data$spanning_header, "**"), + .data$spanning_header + ) + ) x$table_styling$header <- - dplyr::mutate(x$table_styling$header, - spanning_header = - dplyr::case_when( - .data$hide == FALSE & (.data$column %in% cols_to_style) ~ - paste0("**", spanning_header, "**"), - TRUE ~ spanning_header - ) - ) %>% - dplyr::mutate(label = dplyr::case_when( - .data$hide == FALSE & (.data$column %in% c("stat_0", "p.value")) ~ - paste0("**", label, "**"), - TRUE ~ label - )) + x$table_styling$header |> + dplyr::mutate( + label = + dplyr::case_when( + .data$hide == FALSE & (.data$column %in% c("stat_0", "p.value")) ~ + paste0("**", label, "**"), + TRUE ~ label + ) + ) x } @@ -199,22 +208,30 @@ italicize_labels.tbl_cross <- function(x) { cols_to_style <- dplyr::select(x$table_body, all_stat_cols(FALSE)) %>% - names() + names() |> + # remove hidden columns + setdiff(x$table_styling$header$column[x$table_styling$header$hide]) + + x$table_styling$spanning_header <- + x$table_styling$spanning_header |> + dplyr::mutate( + spanning_header = + ifelse( + .data$column %in% .env$cols_to_style & !is.na(.data$spanning_header), + paste0("*", .data$spanning_header, "*"), + .data$spanning_header + ) + ) x$table_styling$header <- - dplyr::mutate(x$table_styling$header, - spanning_header = - dplyr::case_when( - .data$hide == FALSE & (.data$column %in% cols_to_style) ~ - paste0("*", spanning_header, "*"), - TRUE ~ spanning_header - ) - ) |> - dplyr::mutate(label = dplyr::case_when( - .data$hide == FALSE & (.data$column %in% c("stat_0", "p.value")) ~ - paste0("*", label, "*"), - TRUE ~ label - )) + x$table_styling$header |> + dplyr::mutate( + label = dplyr::case_when( + .data$hide == FALSE & (.data$column %in% c("stat_0", "p.value")) ~ + paste0("*", label, "*"), + TRUE ~ label + ) + ) x } diff --git a/R/modify.R b/R/modify.R index 37af7d3ed..0dc3cb9ec 100644 --- a/R/modify.R +++ b/R/modify.R @@ -24,6 +24,10 @@ #' String indicates whether text will be interpreted with #' [`gt::md()`] or [`gt::html()`]. Must be `"md"` (default) or `"html"`. #' Applies to tables printed with `{gt}`. +#' @param level (`integer`)\cr +#' An integer specifying which level to place the spanning header. +#' @param columns ([`tidy-select`][dplyr::dplyr_tidy_select])\cr +#' Columns from which to remove spanning headers. #' @param update,quiet `r lifecycle::badge("deprecated")` #' @param include_example `r lifecycle::badge("deprecated")` #' @@ -47,7 +51,7 @@ #' you may use `{N}` to insert the number of observations, and `{N_event}` #' for the number of events (when applicable). #' -#' @examples +#' @examplesIf (identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true")) && gtsummary:::is_pkg_installed(c("cardx", "broom", "broom.helpers")) #' # create summary table #' tbl <- trial |> #' tbl_summary(by = trt, missing = "no", include = c("age", "grade", "trt")) |> @@ -66,12 +70,6 @@ #' tbl |> #' modify_header(all_stat_cols() ~ "**{level}**, N = {n} ({style_percent(p)}%)") |> #' modify_spanning_header(all_stat_cols() ~ "**Treatment Received**") -#' -#' # Example 3 ---------------------------------- -#' # updating an abbreviation in table footnote -#' glm(response ~ age + grade, trial, family = binomial) |> -#' tbl_regression(exponentiate = TRUE) |> -#' modify_abbreviation("CI = Credible Interval") NULL #' @name modify @@ -124,12 +122,20 @@ modify_header <- function(x, ..., text_interpret = c("md", "html"), #' @name modify #' @export modify_spanning_header <- function(x, ..., text_interpret = c("md", "html"), + level = 1L, quiet, update) { set_cli_abort_call() - updated_call_list <- c(x$call_list, list(modify_footnote = match.call())) + updated_call_list <- c(x$call_list, list(modify_spanning_header = match.call())) # checking inputs ------------------------------------------------------------ check_class(x, "gtsummary") + check_scalar_integerish(level) + if (level < 1) { + cli::cli_abort( + "The {.arg level} argument must be a positive integer.", + call = get_cli_abort_call() + ) + } text_interpret <- arg_match(text_interpret) # process inputs ------------------------------------------------------------- @@ -151,11 +157,13 @@ modify_spanning_header <- function(x, ..., text_interpret = c("md", "html"), # updated header meta data x <- - modify_table_styling( + .modify_spanning_header( x = x, + level = level, columns = names(dots), spanning_header = unlist(dots), - text_interpret = text_interpret + text_interpret = text_interpret, + remove = FALSE ) # return object @@ -163,6 +171,58 @@ modify_spanning_header <- function(x, ..., text_interpret = c("md", "html"), x } +#' @name modify +#' @export +remove_spanning_header <- function(x, columns, level = 1L) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(remove_spanning_header = match.call())) + + # checking inputs ------------------------------------------------------------ + check_class(x, "gtsummary") + check_scalar_integerish(level) + if (level < 1) { + cli::cli_abort( + "The {.arg level} argument must be a positive integer.", + call = get_cli_abort_call() + ) + } + + # process inputs ------------------------------------------------------------- + cards::process_selectors(data = scope_header(x$table_body, x$table_styling$header), columns = {{ columns }}) + + # updated header meta data + x <- + .modify_spanning_header( + x = x, + level = level, + columns = columns, + spanning_header = rep_len(NA_character_, length.out = length(columns)), + remove = TRUE + ) + + # return object + x$call_list <- updated_call_list + x +} + +.modify_spanning_header <- function(x, columns, spanning_header, level = 1L, text_interpret = "md", remove = FALSE) { + # add updates to `x$table_styling$spanning_header` --------------------------- + x$table_styling$spanning_header <- + x$table_styling$spanning_header |> + dplyr::bind_rows( + dplyr::tibble( + level = level, + column = columns, + spanning_header = unname(spanning_header), + text_interpret = paste0("gt::", text_interpret), + remove = remove + ) + ) + + # return table --------------------------------------------------------------- + x +} + #' @name modify #' @export show_header_names <- function(x, include_example, quiet) { diff --git a/R/modify_footnote.R b/R/modify_footnote.R index 5aed9eeeb..087f24928 100644 --- a/R/modify_footnote.R +++ b/R/modify_footnote.R @@ -4,7 +4,11 @@ #' @param footnote (`string`)\cr #' a string #' @param columns ([`tidy-select`][dplyr::dplyr_tidy_select])\cr -#' columns to add footnote +#' columns to add footnote. +#' +#' For `modify_footnote_spanning_header()`, pass a single column name where +#' the spanning header begins. If multiple column names are passed, only +#' the first is used. #' @param rows (predicate `expression`)\cr #' Predicate expression to select rows in `x$table_body`. #' Review [rows argument details][rows_argument]. @@ -13,6 +17,8 @@ #' location with the specified footnote, or whether the specified should #' be added to the existing footnote(s) in the header/cell. Default #' is to replace existing footnotes. +#' @param level (`integer`)\cr +#' An integer specifying which level to place the spanning header footnote. #' #' @return Updated gtsummary object #' @name modify_footnote2 @@ -114,6 +120,53 @@ modify_footnote_body <- function(x, footnote, columns, rows, replace = TRUE, tex x } +#' @export +#' @rdname modify_footnote2 +modify_footnote_spanning_header <- function(x, footnote, columns, + level = 1L, replace = TRUE, + text_interpret = c("md", "html")) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(modify_footnote_body = match.call())) + + # check inputs --------------------------------------------------------------- + check_class(x, "gtsummary") + check_string(footnote) + check_scalar_integerish(level) + if (level < 1) { + cli::cli_abort( + "The {.arg level} argument must be a positive integer.", + call = get_cli_abort_call() + ) + } + check_scalar_logical(replace) + text_interpret <- arg_match(text_interpret, error_call = get_cli_abort_call()) + + # process columns ------------------------------------------------------------ + cards::process_selectors( + scope_header(x$table_body, x$table_styling$header), + columns = {{ columns }} + ) + if (!is_empty(columns)) columns <- columns[1] + check_scalar(columns) + + # evaluate the strings with glue --------------------------------------------- + lst_footnotes <- .evaluate_string_with_glue(x, list(footnote) |> stats::setNames(columns)) + + # add updates to `x$table_styling$footnote_body` ----------------------------- + x <- + .modify_footnote_spanning_header( + x, + lst_footnotes = lst_footnotes, + level = level, + text_interpret = text_interpret, + replace = replace + ) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + #' @export #' @rdname modify_footnote2 remove_footnote_header <- function(x, columns) { @@ -172,6 +225,44 @@ remove_footnote_body <- function(x, columns, rows) { x } +#' @export +#' @rdname modify_footnote2 +remove_footnote_spanning_header <- function(x, columns, level) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(remove_footnote_body = match.call())) + + # check inputs --------------------------------------------------------------- + check_class(x, "gtsummary") + check_scalar_integerish(level) + if (level < 1) { + cli::cli_abort( + "The {.arg level} argument must be a positive integer.", + call = get_cli_abort_call() + ) + } + + # process columns ------------------------------------------------------------ + cards::process_selectors( + scope_header(x$table_body, x$table_styling$header), + columns = {{ columns }} + ) + if (!is_empty(columns)) columns <- columns[1] + check_scalar(columns) + + # add updates to `x$table_styling$footnote_body` ----------------------------- + x <- + .modify_footnote_spanning_header( + x, + lst_footnotes = list(NA_character_) |> stats::setNames(columns), + level = level, + remove = TRUE + ) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + # this checks the rows argument evaluates to a lgl in `x$table_body` .check_rows_input <- function(x, rows) { rows <- enquo(rows) @@ -230,3 +321,25 @@ remove_footnote_body <- function(x, columns, rows) { # return table --------------------------------------------------------------- x } + + +.modify_footnote_spanning_header <- function(x, lst_footnotes, level = 1L, + text_interpret = "md", + replace = TRUE, remove = FALSE) { + # add updates to `x$table_styling$footnote_spanning_header` ------------------ + x$table_styling$footnote_spanning_header <- + x$table_styling$footnote_spanning_header |> + dplyr::bind_rows( + dplyr::tibble( + column = names(lst_footnotes), + footnote = unlist(lst_footnotes) |> unname(), + level = as.integer(level), + text_interpret = paste0("gt::", text_interpret), + replace = replace, + remove = remove + ) + ) + + # return table --------------------------------------------------------------- + x +} diff --git a/R/modify_table_styling.R b/R/modify_table_styling.R index 0baecd31d..3d486219a 100644 --- a/R/modify_table_styling.R +++ b/R/modify_table_styling.R @@ -216,11 +216,12 @@ modify_table_styling <- function(x, # spanning_header ------------------------------------------------------------ if (!is_empty(spanning_header)) { - x$table_styling$header <- - x$table_styling$header %>% - dplyr::rows_update( - dplyr::tibble(column = columns, interpret_spanning_header = paste0("gt::", text_interpret), spanning_header = spanning_header), - by = "column" + x <- + .modify_spanning_header( + x = x, + columns = columns, + spanning_header = spanning_header, + text_interpret = text_interpret ) } diff --git a/R/tbl_merge.R b/R/tbl_merge.R index 9712201cd..222a8c85b 100644 --- a/R/tbl_merge.R +++ b/R/tbl_merge.R @@ -231,7 +231,8 @@ tbl_merge <- function(tbls, tab_spanner = NULL) { ) %>% reduce(.rows_update_table_styling_header, .init = x$table_styling$header) - for (style_type in c("footnote_header", "footnote_body", "abbreviation", "source_note", + for (style_type in c("spanning_header", "footnote_header", "footnote_body", + "footnote_spanning_header", "abbreviation", "source_note", "fmt_fun", "indent", "text_format", "fmt_missing", "cols_merge")) { x$table_styling[[style_type]] <- map( diff --git a/R/tbl_stack.R b/R/tbl_stack.R index a556ad811..ed043e74d 100644 --- a/R/tbl_stack.R +++ b/R/tbl_stack.R @@ -105,7 +105,8 @@ tbl_stack <- function(tbls, group_header = NULL, quiet = FALSE) { dplyr::filter(.by = "column", dplyr::row_number() == 1) # cycle over each of the styling tibbles and stack them in reverse order ----- - for (style_type in c("footnote_header", "footnote_body", "abbreviation", "source_note", + for (style_type in c("spanning_header", "footnote_header", "footnote_body", + "footnote_spanning_header", "abbreviation", "source_note", "fmt_fun", "text_format", "indent", "fmt_missing", "cols_merge")) { results$table_styling[[style_type]] <- map( @@ -176,14 +177,13 @@ tbl_stack <- function(tbls, group_header = NULL, quiet = FALSE) { dplyr::mutate(..tbl_id.. = .y) ) |> dplyr::bind_rows() |> - dplyr::select("..tbl_id..", "column", "label", "spanning_header") |> - tidyr::pivot_longer(cols = c("label", "spanning_header")) |> + dplyr::select("..tbl_id..", "column", "label") |> + tidyr::pivot_longer(cols = c("label")) |> dplyr::group_by(.data$column, .data$name) |> dplyr::mutate( new_value = .data$value[1], name_fmt = dplyr::case_when( - name == "label" ~ "Column header", - name == "spanning_header" ~ "Spanning column header" + name == "label" ~ "Column header" ) ) |> dplyr::filter(.data$new_value != .data$value) |> diff --git a/R/utils-as.R b/R/utils-as.R index 87a4b695e..80f1150eb 100644 --- a/R/utils-as.R +++ b/R/utils-as.R @@ -127,6 +127,36 @@ dplyr::mutate(row_numbers = unlist(.data$row_numbers) %>% unname() %>% list()) %>% dplyr::ungroup() + # spanning_header ------------------------------------------------------------ + x$table_styling$spanning_header <- + x$table_styling$spanning_header |> + dplyr::mutate( + # this is a hold-over from old syntax where NA removed headers + remove = ifelse(is.na(.data$spanning_header), TRUE, .data$remove), + ) |> + # within a column and level, utilize the most recently added + dplyr::filter(.by = c("column", "level"), dplyr::n() == dplyr::row_number()) |> + # finally, remove the row if it's marked for removal or if the column is not printed in final table + dplyr::filter(!remove, .data$column %in% x$table_styling$header$column[!x$table_styling$header$hide]) |> + dplyr::arrange(.data$level) + + if (nrow(x$table_styling$spanning_header) > 0L && + !setequal(unique(x$table_styling$spanning_header$level), + seq_len(max(x$table_styling$spanning_header$level)))) { + max_level <- max(x$table_styling$spanning_header$level) + missing_lvls <- seq_len(max_level) |> + setdiff(unique(x$table_styling$spanning_header$level)) + + cli::cli_abort( + c("!" = "There is an error in the spanning headers structure.", + "!" = "Each spanning header level must be defined, that is, no levels may be skipped.", + "i" = "The {cli::qty(length(missing_lvls))} spanning header{?s} for level{?s} + {.val {missing_lvls}} {cli::qty(length(missing_lvls))} {?is/are} not present, + but level {.val {max_level}} is present."), + call = get_cli_abort_call() + ) + } + # footnote_header ------------------------------------------------------------ x$table_styling$footnote_header <- x$table_styling$footnote_header |> @@ -136,7 +166,7 @@ ) |> # within a column, if a later entry contains `replace=TRUE` or `remove=TRUE`, then mark the row for removal .filter_row_with_subsequent_replace_or_removal() |> - #finally, remove the row if it's marked for removal or if the column is not printed in final table + # finally, remove the row if it's marked for removal or if the column is not printed in final table dplyr::filter(!remove, .data$column %in% x$table_styling$header$column[!x$table_styling$header$hide]) # footnote_body -------------------------------------------------------------- @@ -159,6 +189,18 @@ dplyr::select(all_of(c("column", "row_numbers", "text_interpret", "footnote"))) |> dplyr::mutate(row_numbers = as.integer(.data$row_numbers)) # when there are no body footnotes, this ensures expected type/class + # footnote_spanning_header --------------------------------------------------- + x$table_styling$footnote_spanning_header <- + x$table_styling$footnote_spanning_header |> + dplyr::mutate( + # this is a hold-over from old syntax where NA removed footnotes. + remove = ifelse(is.na(.data$footnote), TRUE, .data$remove), + ) |> + # within a column/level, if a later entry contains `replace=TRUE` or `remove=TRUE`, then mark the row for removal + .filter_row_with_subsequent_replace_or_removal() |> + # finally, remove the row if it's marked for removal or if the column is not printed in final table + dplyr::filter(!remove, .data$column %in% x$table_styling$header$column[!x$table_styling$header$hide]) + # abbreviation --------------------------------------------------------------- abbreviation_cols <- x$table_styling$header$column[!x$table_styling$header$hide] |> @@ -223,7 +265,7 @@ # within a column/row, if a later entry contains `replace=TRUE` or `remove=TRUE`, then mark the row for removal dplyr::filter( .data = x, - .by = any_of(c("column", "row_numbers")), + .by = any_of(c("column", "level", "row_numbers")), !unlist( pmap( list(.data$replace, .data$remove, dplyr::row_number()), @@ -244,7 +286,7 @@ # and assigns them an sequential ID .number_footnotes <- function(x, type, start_with = 0L) { # if empty, return empty data frame - if (nrow(x$table_styling[[type]]) == 0L) { + if (nrow(type) == 0L) { return(dplyr::tibble( footnote_id = integer(), footnote = character(), column = character(), column_id = integer(), row_numbers = integer() @@ -256,7 +298,7 @@ x$table_styling$header |> select("column", column_id = "id") |> dplyr::filter(!is.na(.data$column_id)), - x$table_styling[[type]], + type, by = "column" ) |> dplyr::arrange(dplyr::pick(any_of(c("column_id", "row_numbers")))) |> diff --git a/R/utils-gtsummary_core.R b/R/utils-gtsummary_core.R index 1adecdf71..c1917cdf6 100644 --- a/R/utils-gtsummary_core.R +++ b/R/utils-gtsummary_core.R @@ -20,32 +20,50 @@ hide = TRUE, align = "center", interpret_label = "gt::md", - label = names(x$table_body), - interpret_spanning_header = "gt::md", - spanning_header = NA_character_ + label = names(x$table_body) ) %>% dplyr::mutate( hide = ifelse(.data$column %in% "label", FALSE, .data$hide), align = ifelse(.data$column %in% "label", "left", .data$align) ) + + x$table_styling$spanning_header <- + dplyr::tibble( + level = integer(), + column = character(), + spanning_header = character(), + text_interpret = character(), + remove = logical() + ) + x$table_styling$footnote_header <- dplyr::tibble( column = character(), footnote = character(), text_interpret = character(), replace = logical(), remove = logical() ) + x$table_styling$footnote_body <- dplyr::tibble( column = character(), rows = list(), footnote = character(), text_interpret = character(), replace = logical(), remove = logical() ) + + x$table_styling$footnote_spanning_header <- + dplyr::tibble( + column = character(), footnote = character(), + level = integer(), text_interpret = character(), + replace = logical(), remove = logical() + ) + x$table_styling$abbreviation <- dplyr::tibble( column = character(), abbreviation = character(), text_interpret = character() ) + x$table_styling$source_note <- dplyr::tibble( id = integer(), @@ -53,6 +71,7 @@ text_interpret = character(), remove = logical() ) + x$table_styling$text_format <- dplyr::tibble( column = character(), rows = list(), @@ -60,7 +79,7 @@ ) x$table_styling$indent <- - # if there is a label column, make it idnent 0 (which makes it easier to modify later) + # if there is a label column, make it indent 0 (which makes it easier to modify later) if ("label" %in% x$table_styling$header$column) { dplyr::tibble( column = "label", @@ -138,9 +157,7 @@ hide = TRUE, align = "center", interpret_label = "gt::md", - label = names(x$table_body), - interpret_spanning_header = "gt::md", - spanning_header = NA_character_ + label = names(x$table_body) ) %>% .rows_update_table_styling_header(x$table_styling$header) diff --git a/man/modify.Rd b/man/modify.Rd index 4c5b6d5a8..dcbf9d597 100644 --- a/man/modify.Rd +++ b/man/modify.Rd @@ -4,12 +4,22 @@ \alias{modify} \alias{modify_header} \alias{modify_spanning_header} +\alias{remove_spanning_header} \alias{show_header_names} \title{Modify column headers, footnotes, and spanning headers} \usage{ modify_header(x, ..., text_interpret = c("md", "html"), quiet, update) -modify_spanning_header(x, ..., text_interpret = c("md", "html"), quiet, update) +modify_spanning_header( + x, + ..., + text_interpret = c("md", "html"), + level = 1L, + quiet, + update +) + +remove_spanning_header(x, columns, level = 1L) show_header_names(x, include_example, quiet) } @@ -34,6 +44,12 @@ Applies to tables printed with \code{{gt}}.} \item{update, quiet}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}} +\item{level}{(\code{integer})\cr +An integer specifying which level to place the spanning header.} + +\item{columns}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr +Columns from which to remove spanning headers.} + \item{include_example}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}} } \value{ @@ -70,6 +86,7 @@ for the number of events (when applicable). } \examples{ +\dontshow{if ((identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true")) && gtsummary:::is_pkg_installed(c("cardx", "broom", "broom.helpers"))) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # create summary table tbl <- trial |> tbl_summary(by = trt, missing = "no", include = c("age", "grade", "trt")) |> @@ -88,12 +105,7 @@ tbl |> tbl |> modify_header(all_stat_cols() ~ "**{level}**, N = {n} ({style_percent(p)}\%)") |> modify_spanning_header(all_stat_cols() ~ "**Treatment Received**") - -# Example 3 ---------------------------------- -# updating an abbreviation in table footnote -glm(response ~ age + grade, trial, family = binomial) |> - tbl_regression(exponentiate = TRUE) |> - modify_abbreviation("CI = Credible Interval") +\dontshow{\}) # examplesIf} } \author{ Daniel D. Sjoberg diff --git a/man/modify_footnote2.Rd b/man/modify_footnote2.Rd index 6a1617e3c..a3b3127a3 100644 --- a/man/modify_footnote2.Rd +++ b/man/modify_footnote2.Rd @@ -4,8 +4,10 @@ \alias{modify_footnote2} \alias{modify_footnote_header} \alias{modify_footnote_body} +\alias{modify_footnote_spanning_header} \alias{remove_footnote_header} \alias{remove_footnote_body} +\alias{remove_footnote_spanning_header} \title{Modify Footnotes} \usage{ modify_footnote_header( @@ -25,9 +27,20 @@ modify_footnote_body( text_interpret = c("md", "html") ) +modify_footnote_spanning_header( + x, + footnote, + columns, + level = 1L, + replace = TRUE, + text_interpret = c("md", "html") +) + remove_footnote_header(x, columns) remove_footnote_body(x, columns, rows) + +remove_footnote_spanning_header(x, columns, level) } \arguments{ \item{x}{(\code{gtsummary})\cr @@ -37,7 +50,11 @@ A gtsummary object} a string} \item{columns}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr -columns to add footnote} +columns to add footnote. + +For \code{modify_footnote_spanning_header()}, pass a single column name where +the spanning header begins. If multiple column names are passed, only +the first is used.} \item{replace}{(scalar \code{logical})\cr Logical indicating whether to replace any existing footnotes in the specified @@ -53,6 +70,9 @@ Applies to tables printed with \code{{gt}}.} \item{rows}{(predicate \code{expression})\cr Predicate expression to select rows in \code{x$table_body}. Review \link[=rows_argument]{rows argument details}.} + +\item{level}{(\code{integer})\cr +An integer specifying which level to place the spanning header footnote.} } \value{ Updated gtsummary object diff --git a/tests/testthat/_snaps/modify_footnote_spanning_header.md b/tests/testthat/_snaps/modify_footnote_spanning_header.md new file mode 100644 index 000000000..46e452c24 --- /dev/null +++ b/tests/testthat/_snaps/modify_footnote_spanning_header.md @@ -0,0 +1,18 @@ +# modify_footnote_spanning_header() messaging + + Code + modify_footnote_spanning_header(base_tbl_summary, footnote = "Treatment as of June", + columns = all_stat_cols(), level = 0L) + Condition + Error in `modify_footnote_spanning_header()`: + ! The `level` argument must be a positive integer. + +--- + + Code + remove_footnote_spanning_header(base_tbl_summary, columns = all_stat_cols(), + level = 0L) + Condition + Error in `remove_footnote_spanning_header()`: + ! The `level` argument must be a positive integer. + diff --git a/tests/testthat/_snaps/modify_spanning_header.md b/tests/testthat/_snaps/modify_spanning_header.md index d193e320c..f0a4865a2 100644 --- a/tests/testthat/_snaps/modify_spanning_header.md +++ b/tests/testthat/_snaps/modify_spanning_header.md @@ -7,3 +7,31 @@ ! There was an error in the `glue::glue()` evaluation of "This is not a valid {element}." for column "label". i Run `gtsummary::show_header_names()` for information on values available for glue interpretation. +# modify_spanning_header() messaging with missing level + + Code + as_gt(remove_spanning_header(modify_spanning_header(modify_spanning_header( + tbl_summary(trial, by = trt, include = age), all_stat_cols() ~ + "**Treatments**", level = 1), all_stat_cols() ~ "**Treatments**", level = 2), + columns = everything(), level = 1)) + Condition + Error in `as_gt()`: + ! There is an error in the spanning headers structure. + ! Each spanning header level must be defined, that is, no levels may be skipped. + i The spanning header for level 1 is not present, but level 2 is present. + +--- + + Code + as_gt(remove_spanning_header(remove_spanning_header(modify_spanning_header( + modify_spanning_header(modify_spanning_header(tbl_summary(trial, by = trt, + include = age), all_stat_cols() ~ "**Treatments**", level = 1), + all_stat_cols() ~ "**Treatments**", level = 2), all_stat_cols() ~ + "**Treatments**", level = 3), columns = everything(), level = 1), columns = everything(), + level = 2)) + Condition + Error in `as_gt()`: + ! There is an error in the spanning headers structure. + ! Each spanning header level must be defined, that is, no levels may be skipped. + i The spanning headers for levels 1 and 2 are not present, but level 3 is present. + diff --git a/tests/testthat/test-as_flex_table.R b/tests/testthat/test-as_flex_table.R index 6fe690262..6697c8ad8 100644 --- a/tests/testthat/test-as_flex_table.R +++ b/tests/testthat/test-as_flex_table.R @@ -172,11 +172,30 @@ test_that("as_flex_table passes table header labels correctly", { # spanning header placed correctly vis_cols <- which(!my_spanning_tbl$table_styling$header$hide) expect_equal( - my_spanning_tbl$table_styling$header |> + my_spanning_tbl$table_styling$spanning_header |> dplyr::filter(!is.na(spanning_header)) |> dplyr::pull(column), which(nchar(apply(ft_spanning_tbl$header$content$data, c(1, 2), \(x) x[[1]]$txt[1])[1, ]) > 1) |> names() ) + + # checking the placement of a second spanning header + expect_silent( + tbl2 <- + my_spanning_tbl |> + modify_spanning_header(all_stat_cols() ~ "**Tumor Grade**", level = 2) |> + as_flex_table() + ) + + expect_equal( + tbl2$header$dataset, + data.frame( + stringsAsFactors = FALSE, + label = c(" ", " ", "label"), + stat_1 = c("**Tumor Grade**", "**Testing**", "stat_1"), + stat_2 = c("**Tumor Grade**", " ", "stat_2"), + stat_3 = c("**Tumor Grade**", "**Testing**", "stat_3") + ) + ) }) test_that("as_flex_table passes table column visibility correctly", { @@ -269,6 +288,44 @@ test_that("as_flex_table passes table footnotes & abbreviations correctly", { c(fn1[2], fn2[2]), # correct labels c("another new footnote", "replace old footnote") ) + + # footnotes in spanning headers + expect_equal( + my_spanning_tbl |> + modify_footnote_spanning_header( + footnote = "spanning footnote", + columns = stat_1 + ) |> + as_flex_table() |> + getElement("footer") |> + getElement("content") |> + getElement("data") %>% + `[`(1,) |> + getElement("label") |> + getElement("txt"), + c("1", "spanning footnote") + ) + expect_equal( + my_spanning_tbl |> + modify_spanning_header(stat_1 = "**2 levels**", level = 2L) |> + modify_footnote_spanning_header( + footnote = "spanning footnote", + columns = stat_1 + ) |> + modify_footnote_spanning_header( + footnote = "spanning footnote 2", + columns = stat_1, + level = 2 + ) |> + as_flex_table() |> + getElement("footer") |> + getElement("content") |> + getElement("data") %>% + `[`(1,) |> + getElement("label") |> + getElement("txt"), + c("1", "spanning footnote 2") + ) }) test_that("as_flex_table passes multiple table footnotes correctly", { diff --git a/tests/testthat/test-as_gt.R b/tests/testthat/test-as_gt.R index 91b12f6d7..1fbd42758 100644 --- a/tests/testthat/test-as_gt.R +++ b/tests/testthat/test-as_gt.R @@ -114,7 +114,7 @@ test_that("as_gt passes table header labels correctly", { # spanning_tbl - spanning header expect_equal( - my_spanning_tbl$table_styling$header |> + my_spanning_tbl$table_styling$spanning_header |> dplyr::filter(!is.na(spanning_header)) |> dplyr::pull(column), gt_spanning_tbl$`_spanners`$vars[[1]] @@ -185,9 +185,9 @@ test_that("as_gt passes table text interpreters correctly", { # spanning header expect_equal( sapply( - my_spanning_tbl$table_styling$header |> + my_spanning_tbl$table_styling$spanning_header |> dplyr::filter(!is.na(spanning_header)) |> - dplyr::pull(interpret_spanning_header), + dplyr::pull(text_interpret), \(x) do.call(eval(parse(text = x)), list("")) |> class() ), gt_spanning_tbl$`_spanners`$spanner_label[[1]] |> class() |> @@ -210,7 +210,30 @@ test_that("as_gt passes table text interpreters correctly", { ) # spanning header - expect_true(attr(gt_tbl$`_spanners`$spanner_label[[1]], "html")) + expect_true(attr(gt_tbl$`_spanners`$spanner_label[[2]], "html")) + + # checking the placement of a second spanning header + expect_silent( + tbl2 <- + my_spanning_tbl |> + modify_spanning_header(all_stat_cols() ~ "**Tumor Grade**", level = 2) |> + as_gt() + ) + + expect_equal( + tbl2$`_spanners` |> + dplyr::select(vars, spanner_label, spanner_level) |> + dplyr::mutate( + vars = map_chr(vars, ~paste(.x, collapse = ", ")), + spanner_label = map_chr(spanner_label, as.character) + ), + data.frame( + stringsAsFactors = FALSE, + vars = c("stat_1, stat_3", "stat_1, stat_2, stat_3"), + spanner_label = c("**Testing**", "**Tumor Grade**"), + spanner_level = c(1L, 2L) + ) + ) }) test_that("as_gt passes table footnotes & abbreviations correctly", { @@ -276,6 +299,34 @@ test_that("as_gt passes table footnotes & abbreviations correctly", { rownum = c(1, 2, 1) ) ) + + # footnotes in spanning headers + expect_equal( + my_spanning_tbl |> + modify_footnote_spanning_header( + footnote = "Testing 1 Footnote", + columns = stat_1 + ) |> + as_gt() |> + getElement("_footnotes") |> + dplyr::filter(footnotes == "Testing 1 Footnote") |> + dplyr::pull(locname), + "columns_groups" + ) + expect_equal( + my_spanning_tbl |> + modify_spanning_header(c(stat_1, stat_2) ~ "**Another Span**", level = 2L) |> + modify_footnote_spanning_header( + footnote = "Testing 1 Footnote", + columns = stat_1, + level = 2L + ) |> + as_gt() |> + getElement("_footnotes") |> + dplyr::filter(footnotes == "Testing 1 Footnote") |> + dplyr::pull(locname), + "columns_groups" + ) }) test_that("as_gt passes table indentation correctly", { diff --git a/tests/testthat/test-as_hux_table.R b/tests/testthat/test-as_hux_table.R index 8e94e9da5..73f5275d0 100644 --- a/tests/testthat/test-as_hux_table.R +++ b/tests/testthat/test-as_hux_table.R @@ -50,6 +50,31 @@ test_that("as_hux_table works with tbl_merge", { expect_snapshot(ht_merge) }) +test_that("as_hux_table checking the placement of a second spanning header", { + expect_silent( + tbl2 <- + trial |> + tbl_summary(by = grade, include = age) |> + modify_spanning_header(c(stat_1, stat_3) ~ "**Testing**") |> + modify_spanning_header(all_stat_cols() ~ "**Tumor Grade**", level = 2) |> + as_hux_table() + ) + + expect_equal( + map(tbl2, ~.x[1:3]) |> + dplyr::bind_cols() |> + as.data.frame(), + data.frame( + stringsAsFactors = FALSE, + label = c(NA, NA, "**Characteristic**"), + stat_1 = c("**Tumor Grade**", "**Testing**", "**I** \nN = 68"), + stat_2 = c("**Tumor Grade**", NA, "**II** \nN = 68"), + stat_3 = c("**Tumor Grade**", "**Testing**", "**III** \nN = 64") + ) + ) +}) + + test_that("as_hux_table works with tbl_stack", { t1 <- trial |> dplyr::filter(trt == "Drug A") |> @@ -146,6 +171,16 @@ test_that("as_hux_table passes table footnotes & abbreviations correctly", { ht[8:9, 1] |> unlist(use.names = FALSE), c("another new footnote", "replace old footnote") ) + + expect_equal( + my_tbl_summary |> + modify_footnote_spanning_header("footnote test", columns = all_stat_cols()) |> + as_hux_table() %>% + `[`(8,) |> + unlist() |> + unname(), + c("footnote test", "footnote test") + ) }) test_that("as_hux_table passes appended glance statistics correctly", { diff --git a/tests/testthat/test-as_kable_extra.R b/tests/testthat/test-as_kable_extra.R index 41857baaa..a6c5c5315 100644 --- a/tests/testthat/test-as_kable_extra.R +++ b/tests/testthat/test-as_kable_extra.R @@ -28,7 +28,8 @@ test_that("as_kable_extra(return_calls) works as expected", { expect_equal( names(kbl), c("tibble", "fmt", "cols_merge", "fmt_missing", "cols_hide", "remove_line_breaks", - "escape_table_body", "bold_italic", "kable", "add_indent", "source_note", "abbreviations", "footnote") + "escape_table_body", "bold_italic", "kable", "add_indent", + "add_header_above", "source_note", "abbreviations", "footnote") ) }) @@ -125,6 +126,22 @@ test_that("as_kable_extra works with tbl_stack", { expect_snapshot(kbl_stack) }) +test_that("as_kable_extra checking the placement of a second spanning header", { + expect_silent( + tbl2 <- + trial |> + tbl_summary(by = grade, include = age) |> + modify_spanning_header(c(stat_1, stat_3) ~ "**Testing**") |> + modify_spanning_header(all_stat_cols() ~ "**Tumor Grade**", level = 2) |> + as_kable_extra() + ) + + # this isn't a great test, but it's something! + expect_true(as.character(tbl2) |> str_detect("Testing")) + expect_true(as.character(tbl2) |> str_detect("Tumor Grade")) +}) + + test_that("as_kable_extra works with bold/italics", { tbl <- my_tbl_summary |> bold_labels() |> @@ -195,6 +212,13 @@ test_that("as_kable_extra passes table footnotes & abbreviations correctly", { expect_snapshot_value( strsplit(kbl[1], "* \n *")[[1]][10] ) + + expect_true( + my_tbl_summary |> + modify_footnote_spanning_header("footnote test", columns = all_stat_cols()) |> + as_kable_extra(format = "html") |> + str_detect("footnote test") + ) }) test_that("as_kable_extra passes appended glance statistics correctly", { diff --git a/tests/testthat/test-bold_italicize_labels_levels.R b/tests/testthat/test-bold_italicize_labels_levels.R index 720ba34d0..ab71dd94d 100644 --- a/tests/testthat/test-bold_italicize_labels_levels.R +++ b/tests/testthat/test-bold_italicize_labels_levels.R @@ -107,7 +107,7 @@ test_that("bold_labels.tbl_cross()", { expect_equal( tbl |> getElement("table_styling") |> - getElement("header") |> + getElement("spanning_header") |> dplyr::filter(startsWith(column, "stat_"), column != "stat_0") |> dplyr::pull(spanning_header) |> unique(), @@ -158,7 +158,7 @@ test_that("italicize_labels.tbl_cross()", { expect_equal( tbl |> getElement("table_styling") |> - getElement("header") |> + getElement("spanning_header") |> dplyr::filter(startsWith(column, "stat_"), column != "stat_0") |> dplyr::pull(spanning_header) |> unique(), diff --git a/tests/testthat/test-combine_terms.R b/tests/testthat/test-combine_terms.R index 13d66f185..9c423afdc 100644 --- a/tests/testthat/test-combine_terms.R +++ b/tests/testthat/test-combine_terms.R @@ -151,14 +151,13 @@ test_that("combine_terms works with GEE models", { test_that("combine_terms works when used in map/apply", { data <- data.frame(outcome = "marker", exp = FALSE, test = "F") - expect_no_error( + expect_silent( res <- data |> mutate( mod = map(outcome, ~ glm(as.formula(paste0(.x, " ~ age + stage")), data = trial, family = gaussian)), tbl = map2(mod, exp, ~ tbl_regression(.x, exponentiate = .y)), tbl2 = map2(tbl, test, ~ combine_terms(..1, formula_update = . ~ . - stage, test = ..2)) - ) |> - invisible() + ) ) }) diff --git a/tests/testthat/test-modify_footnote_spanning_header.R b/tests/testthat/test-modify_footnote_spanning_header.R new file mode 100644 index 000000000..89934c1e6 --- /dev/null +++ b/tests/testthat/test-modify_footnote_spanning_header.R @@ -0,0 +1,94 @@ +skip_on_cran() + +base_tbl_summary <- + tbl_summary(trial, by = trt, include = marker) |> + modify_spanning_header(all_stat_cols() ~ "**Treatment**") |> + modify_footnote_spanning_header( + "Randomized Treatment", + columns = all_stat_cols(), + level = 1, + ) + +test_that("modify_footnote_spanning_header(footnote)", { + # test we can easily replace an existing spanning header footnote + expect_silent( + tbl <- + base_tbl_summary |> + remove_footnote_spanning_header( + columns = all_stat_cols(), + level = 1L + ) |> + modify_footnote_spanning_header( + footnote = "Treatment Recieved", + columns = all_stat_cols() + ) + ) + expect_equal( + tbl$table_styling$footnote_spanning_header, + dplyr::tribble( + ~column, ~footnote, ~level, ~text_interpret, ~replace, ~remove, + "stat_1", "Randomized Treatment", 1L, "gt::md", TRUE, FALSE, + "stat_1", NA, 1L, "gt::md", TRUE, TRUE, + "stat_1", "Treatment Recieved", 1L, "gt::md", TRUE, FALSE + ) + ) + + # test that two footnotes can be placed in the same spanning header + expect_silent( + tbl <- + base_tbl_summary |> + modify_footnote_spanning_header( + footnote = "Treatment as of June", + columns = all_stat_cols(), + replace = FALSE + ) + ) + expect_equal( + tbl$table_styling$footnote_spanning_header, + dplyr::tribble( + ~column, ~footnote, ~level, ~text_interpret, ~replace, ~remove, + "stat_1", "Randomized Treatment", 1L, "gt::md", TRUE, FALSE, + "stat_1", "Treatment as of June", 1L, "gt::md", FALSE, FALSE + ) + ) + + # test that we can place footnotes on multiple spanning header levels + expect_silent( + tbl <- + base_tbl_summary |> + modify_spanning_header(all_stat_cols() ~ "**Treatment 2**", level = 2) |> + modify_footnote_spanning_header( + footnote = "Treatment as of June", + columns = all_stat_cols(), + level = 2 + ) + ) + expect_equal( + tbl$table_styling$footnote_spanning_header, + dplyr::tribble( + ~column, ~footnote, ~level, ~text_interpret, ~replace, ~remove, + "stat_1", "Randomized Treatment", 1L, "gt::md", TRUE, FALSE, + "stat_1", "Treatment as of June", 2L, "gt::md", TRUE, FALSE + ) + ) +}) + +test_that("modify_footnote_spanning_header() messaging", { + expect_snapshot( + error = TRUE, + base_tbl_summary |> + modify_footnote_spanning_header( + footnote = "Treatment as of June", + columns = all_stat_cols(), + level = 0L + ) + ) + expect_snapshot( + error = TRUE, + base_tbl_summary |> + remove_footnote_spanning_header( + columns = all_stat_cols(), + level = 0L + ) + ) +}) diff --git a/tests/testthat/test-modify_spanning_header.R b/tests/testthat/test-modify_spanning_header.R index f2344bdc4..d651dabbd 100644 --- a/tests/testthat/test-modify_spanning_header.R +++ b/tests/testthat/test-modify_spanning_header.R @@ -32,8 +32,7 @@ test_that("modify_spanning_header(...) works", { tbl |> modify_spanning_header(label = "Variable") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column %in% "label") |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), "Variable" ) @@ -42,8 +41,7 @@ test_that("modify_spanning_header(...) works", { tbl |> modify_spanning_header(label = "Variable", stat_0 = "Overall") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column %in% c("label", "stat_0")) |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), c("Variable", "Overall") ) @@ -52,8 +50,7 @@ test_that("modify_spanning_header(...) works", { tbl |> modify_spanning_header(c(label, stat_0) ~ "Variable") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column %in% c("label", "stat_0")) |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), c("Variable", "Variable") ) @@ -67,8 +64,7 @@ test_that("modify_spanning_header(...) dynamic headers work with `tbl_summary()` tbl |> modify_spanning_header(!!!list(label = "Variable", stat_0 = "Overall")) |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column %in% c("label", "stat_0")) |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), c("Variable", "Overall") ) @@ -78,8 +74,7 @@ test_that("modify_spanning_header(...) dynamic headers work with `tbl_summary()` tbl |> modify_spanning_header(stat_0 = "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column %in% "stat_0") |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), "Overall | N = 200 | n = 200 | p = 100%" ) @@ -88,8 +83,7 @@ test_that("modify_spanning_header(...) dynamic headers work with `tbl_summary()` tbl_summary(trial, by = trt, include = marker) |> modify_spanning_header(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(startsWith(column, "stat_")) |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), c("Drug A | N = 200 | n = 98 | p = 49%", "Drug B | N = 200 | n = 102 | p = 51%") @@ -100,8 +94,7 @@ test_that("modify_spanning_header(...) dynamic headers work with `tbl_summary()` add_overall() |> modify_spanning_header(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(startsWith(column, "stat_")) |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), c("Overall | N = 200 | n = 200 | p = 100%", "Drug A | N = 200 | n = 98 | p = 49%", @@ -120,9 +113,8 @@ test_that("modify_spanning_header(text_interpret) works", { tbl_summary(trial, include = marker) |> modify_spanning_header(label = "Variable", text_interpret = "html") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column %in% "label") |> - dplyr::pull(interpret_spanning_header), + getElement("spanning_header") |> + dplyr::pull(text_interpret), "gt::html" ) }) @@ -136,59 +128,135 @@ test_that("modify_spanning_header() works with tbl_svysummary()", { add_overall() |> modify_spanning_header(label = "Variable") |> getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column == "label") |> + getElement("spanning_header") |> dplyr::pull("spanning_header"), "Variable" ) }) test_that("modify_spanning_header() works with tbl_continuous()", { - expect_equal(tbl_continuous(data = trial, variable = age, by = trt, include = grade)|> - add_overall() |> - modify_spanning_header(all_stat_cols() ~ "Statistics") |> - getElement("table_styling") |> - getElement("header") |> - dplyr::filter(startsWith(column, "stat_")) |> - dplyr::pull("spanning_header"), - c("Statistics", "Statistics", "Statistics") + expect_equal( + tbl_continuous(data = trial, variable = age, by = trt, include = grade)|> + add_overall() |> + modify_spanning_header(all_stat_cols() ~ "Statistics") |> + getElement("table_styling") |> + getElement("spanning_header") |> + dplyr::pull("spanning_header"), + c("Statistics", "Statistics", "Statistics") ) }) test_that("modify_spanning_header() works with tbl_cross()", { - expect_equal(tbl_cross(data = trial, row = trt, col = response) |> - modify_spanning_header(stat_0 = "Total Response") |> - getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column == "stat_0") |> - dplyr::pull("spanning_header"), - c("Total Response") + expect_equal( + tbl_cross(data = trial, row = trt, col = response) |> + modify_spanning_header(stat_0 = "Total Response") |> + getElement("table_styling") |> + getElement("spanning_header") |> + dplyr::filter(column == "stat_0") |> + dplyr::pull("spanning_header"), + c("Total Response") ) }) test_that("modify_spanning_header() works with tbl_regression()", { skip_if_not(is_pkg_installed("broom.helpers")) - expect_equal(glm(response ~ age + grade, trial, family = binomial()) |> - tbl_regression(exponentiate = TRUE) |> - modify_spanning_header(estimate = "Estimate") |> - getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column == "estimate") |> - dplyr::pull("spanning_header"), - c("Estimate") + expect_equal( + glm(response ~ age + grade, trial, family = binomial()) |> + tbl_regression(exponentiate = TRUE) |> + modify_spanning_header(estimate = "Estimate") |> + getElement("table_styling") |> + getElement("spanning_header") |> + dplyr::pull("spanning_header"), + c("Estimate") ) }) test_that("modify_spanning_header() works with tbl_uvregression()", { - expect_equal(tbl_uvregression(trial, method = glm, y = response, method.args = list(family = binomial), - exponentiate = TRUE, include = c("age", "grade")) |> - modify_spanning_header(estimate = "Estimate") |> - getElement("table_styling") |> - getElement("header") |> - dplyr::filter(column == "estimate") |> - dplyr::pull("spanning_header"), - c("Estimate") + expect_equal( + tbl_uvregression(trial, method = glm, y = response, method.args = list(family = binomial), + exponentiate = TRUE, include = c("age", "grade")) |> + modify_spanning_header(estimate = "Estimate") |> + getElement("table_styling") |> + getElement("spanning_header") |> + dplyr::filter(column == "estimate") |> + dplyr::pull("spanning_header"), + c("Estimate") + ) +}) + +test_that("modify_spanning_header() works with multiple spanning headers", { + expect_silent( + tbl <- trial |> + tbl_summary( + by = trt, + include = age + ) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 1) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 2) + ) + + expect_equal( + tbl$table_styling$spanning_header$spanning_header, + rep_len("**Treatments**", 4L) + ) +}) + +test_that("remove_spanning_header() works", { + expect_silent( + tbl <- trial |> + tbl_summary( + by = trt, + include = age + ) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 1) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 2) |> + remove_spanning_header(columns = everything(), level = 2) + ) + + expect_true( + tbl$table_styling$spanning_header |> + dplyr::filter( + .by = c("level", "column"), + dplyr::n() == dplyr::row_number(), + level == 2L, + column %in% c("stat_1", "stat_2") + ) |> + dplyr::pull(spanning_header) |> + is.na() |> + all() + ) +}) + +test_that("modify_spanning_header() messaging with missing level", { + # one missing level + expect_snapshot( + error = TRUE, + trial |> + tbl_summary( + by = trt, + include = age + ) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 1) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 2) |> + remove_spanning_header(columns = everything(), level = 1) |> + as_gt() + ) + + # two missing level + expect_snapshot( + error = TRUE, + trial |> + tbl_summary( + by = trt, + include = age + ) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 1) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 2) |> + modify_spanning_header(all_stat_cols() ~ "**Treatments**", level = 3) |> + remove_spanning_header(columns = everything(), level = 1) |> + remove_spanning_header(columns = everything(), level = 2) |> + as_gt() ) }) diff --git a/tests/testthat/test-modify_table_styling.R b/tests/testthat/test-modify_table_styling.R index 9757b3830..52d0b2661 100644 --- a/tests/testthat/test-modify_table_styling.R +++ b/tests/testthat/test-modify_table_styling.R @@ -120,7 +120,7 @@ test_that("modify_table_styling(spanning_header)", { spanning_header = "**Treatment Assignment**" ) |> getElement("table_styling") |> - getElement("header") |> + getElement("spanning_header") |> dplyr::filter(startsWith(column, "stat_")) |> dplyr::pull(spanning_header), c("**Treatment Assignment**", "**Treatment Assignment**") @@ -307,7 +307,7 @@ test_that("modify_table_styling(text_interpret)", { text_interpret = "html" ) |> getElement("table_styling") |> - getElement("header") |> + getElement("spanning_header") |> dplyr::filter(startsWith(column, "stat_")) |> dplyr::pull(spanning_header), c("my big header", "my big header") diff --git a/tests/testthat/test-tbl_cross.R b/tests/testthat/test-tbl_cross.R index 8d69cb856..1c17fa601 100644 --- a/tests/testthat/test-tbl_cross.R +++ b/tests/testthat/test-tbl_cross.R @@ -41,7 +41,7 @@ test_that("tbl_cross(label) works", { ) ) expect_identical(out$table_body$var_label[1], "TRT") - expect_identical(unique(out$table_styling$header$spanning_header)[2], "STAGE") + expect_identical(unique(out$table_styling$spanning_header$spanning_header), "STAGE") }) test_that("tbl_cross(label) errors properly", { diff --git a/tests/testthat/test-tbl_merge.R b/tests/testthat/test-tbl_merge.R index 0e1455e09..f9191f54f 100644 --- a/tests/testthat/test-tbl_merge.R +++ b/tests/testthat/test-tbl_merge.R @@ -70,11 +70,11 @@ test_that("tbl_merge works with standard use", { # correct spanning headers expect_equal( - t4$table_styling$header |> - dplyr::filter(!hide) |> - dplyr::pull(spanning_header), - c(NA, - rep("UVA Tumor Response", 4), + t4$table_styling$spanning_header |> + dplyr::filter(column %in% t4$table_styling$header$column[!t4$table_styling$header$hide]) |> + dplyr::pull(spanning_header) |> + rev(), + c(rep("UVA Tumor Response", 4), rep("MVA Tumor Response", 3), rep("MVA Time to Death", 3), rep("TTD Adjusted for grade", 3)) @@ -104,7 +104,7 @@ test_that("tbl_merge works with no spanning header", { expect_silent(tbl <- tbl_merge(list(t0, t1, t2, t3), tab_spanner = FALSE)) expect_true( - tbl$table_styling$header$spanning_header |> is.na() |> all() + tbl$table_styling$spanning_header$spanning_header |> is_empty() ) }) @@ -132,8 +132,8 @@ test_that("tbl_merge works with a single table", { # correct spanning header expect_equal( - tbl$table_styling$header$spanning_header, - c(rep(NA, 4), rep("**Table 1**", 3)) + tbl$table_styling$spanning_header$spanning_header, + rep("**Table 1**", 3) ) }) diff --git a/tests/testthat/test-tbl_strata.R b/tests/testthat/test-tbl_strata.R index 631a0494a..83983c83a 100644 --- a/tests/testthat/test-tbl_strata.R +++ b/tests/testthat/test-tbl_strata.R @@ -201,7 +201,7 @@ test_that("tbl_strata(.combine_args) works as expected", { ) # no spanning header added - expect_true(all(is.na(tbl$table_styling$header$spanning_header))) + expect_true(tbl$table_styling$spanning_header$spanning_header |> is_empty()) }) test_that("tbl_strata2 works with standard use", { diff --git a/vignettes/gtsummary_definition.Rmd b/vignettes/gtsummary_definition.Rmd index 7ef9823ed..6a6fd1d9b 100644 --- a/vignettes/gtsummary_definition.Rmd +++ b/vignettes/gtsummary_definition.Rmd @@ -59,7 +59,7 @@ tbl_summary_ex$table_body #### table_styling The `.$table_styling` object is a list of data frames containing information about how `.$table_body` is printed, formatted, and styled. -The list contains the following data frames `header`, `footnote_header`, `footnote_body`, `abbreviation`, `source_note`, `fmt_fun`, `text_format`, `fmt_missing`, `cols_merge` and the following objects `caption` and `horizontal_line_above`. +The list contains the following data frames `header`, `footnote_header`, `footnote_body`, `footnote_spanning_header`, `abbreviation`, `source_note`, `fmt_fun`, `text_format`, `fmt_missing`, `cols_merge` and the following objects `caption` and `horizontal_line_above`. **`header`** @@ -74,8 +74,6 @@ dplyr::tribble( "align", "Specifies the alignment/justification of the column, e.g. 'center' or 'left'", "label", "Label that will be displayed (if column is displayed in output)", "interpret_label", "the {gt} function that is used to interpret the column label, `gt::md()` or `gt::html()`", - "spanning_header", "Includes text printed above columns as spanning headers.", - "interpret_spanning_header", "the {gt} function that is used to interpret the column spanning headers, `gt::md()` or `gt::html()`", "modify_stat_{*}", "any column beginning with `modify_stat_` is a statistic available to report in `modify_header()` (and others)", "modify_selector_{*}", "any column beginning with `modify_selector_` is a column that is scoped in `modify_header()` (and friends) to be used in a selecting environment" ) %>% @@ -147,6 +145,34 @@ dplyr::tribble( ) ``` +**`footnote_spanning_header`** + +Each {gtsummary} table may include footnotes in the spanning headers of the table. +Updates/changes to footnote are appended to the bottom of the tibble. + +```{r, echo=FALSE} +dplyr::tribble( + ~Column, ~Description, + "level", "Integer specifying the spanning header level, similar to `gt::tab_spanner(level)`", + "column", "Column name from `.$table_body`", + "footnote", "string containing footnote to add to column/row", + "text_interpret", "the {gt} function that is used to interpret the source note, `gt::md()` or `gt::html()`", + "replace", "logical indicating whether this footnote should replace any existing footnote in that header (TRUE) or be added to any existing (FALSE)", + "remove", "logical indicating whether to remove all footnotes in the column header", +) %>% + gt::gt() %>% + gt::fmt_markdown(columns = everything()) %>% + gt::tab_options( + table.font.size = "small", + data_row.padding = gt::px(1), + summary_row.padding = gt::px(1), + grand_summary_row.padding = gt::px(1), + footnotes.padding = gt::px(1), + source_notes.padding = gt::px(1), + row_group.padding = gt::px(1) + ) +``` + **`abbreviation`** Abbreviations are added one at a time, and at the time of table rendering, the are coalesced into a single source note.