diff --git a/DESCRIPTION b/DESCRIPTION index cdfc5aa6b..71141a7ac 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: gtsummary Title: Presentation-Ready Data Summary and Analytic Result Tables -Version: 2.0.4.9001 +Version: 2.0.4.9002 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 21100d773..edeb014b3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -153,6 +153,7 @@ export(label_style_ratio) export(label_style_sigfig) export(last_col) export(matches) +export(modify_abbreviation) export(modify_caption) export(modify_cols_merge) export(modify_column_alignment) @@ -162,6 +163,8 @@ export(modify_column_merge) export(modify_column_unhide) export(modify_fmt_fun) export(modify_footnote) +export(modify_footnote_body) +export(modify_footnote_header) export(modify_header) export(modify_source_note) export(modify_spanning_header) @@ -180,6 +183,9 @@ export(pkgdown_print.gtsummary) export(pool_and_tidy_mice) export(proportion_summary) export(ratio_summary) +export(remove_abbreviation) +export(remove_footnote_body) +export(remove_footnote_header) export(remove_row_type) export(remove_source_note) export(reset_gtsummary_theme) diff --git a/NEWS.md b/NEWS.md index 91bd04ba7..164449ed4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # gtsummary (development version) +* Updates to the handling of footnotes. Previously, header footnotes were handled with `modify_footnote()` and `modify_table_styling(footnote)`. It was possible to also include footnotes in the table body with `modify_table_styling(footnote)`, but this was largely a hidden feature. Also confusingly, a special abbreviation footnote was handled with `modify_footnote(abbreviation=TRUE)`. + + In this update, we now export separate user-facing functions for each of these with clearer names and scope: `modify_footnote_header()`, `modify_footnote_body()`, and `modify_abbreviation()`. As the names indicate, the `modify_footnote_header()` and `modify_footnote_body()` functions place footnotes in the header and table body. Abbreviations are now treated like source notes and do not have footnote markers associated with them. We also export functions `remove_footnote_header()`, `remove_footnote_body()`, and `remove_abbreviation()` to remove previously assigned footnotes and abbreviations. + + Also, multiple footnotes may now reference the same cell in the table or column header by utilizing the `modify_footnote_header(replace=FALSE)`, `modify_footnote_body(replace=FALSE)` argument. + +* 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. + # gtsummary 2.0.4 ### New Features and Functions diff --git a/R/add_ci.R b/R/add_ci.R index c12bd7be7..f5f7326db 100644 --- a/R/add_ci.R +++ b/R/add_ci.R @@ -59,7 +59,7 @@ #' include = c(response, grade) #' ) |> #' add_ci(pattern = "{stat} ({ci})") |> -#' modify_footnote(everything() ~ NA) +#' remove_footnote_header(everything()) NULL #' @rdname add_ci @@ -293,10 +293,7 @@ brdg_add_ci <- function(x, pattern, statistic, include, conf.level, updated_call modify_header( matches("^ci_stat_\\d+$") ~ paste0("**", conf.level * 100, "% ", translate_string("CI"), "**") ) |> - modify_footnote( - matches("^ci_stat_\\d+$") ~ translate_string("CI = Confidence Interval"), - abbreviation = TRUE - ) + modify_abbreviation(translate_string("CI = Confidence Interval")) } else { # get the stat column index numbers, eg get the 1 and 2 from stat_1 and stat_2 @@ -327,10 +324,7 @@ brdg_add_ci <- function(x, pattern, statistic, include, conf.level, updated_call x <- cols_merge_expr |> reduce(\(.x, .y) inject(!!.x %>% !!.y), .init = x) |> - modify_footnote( - all_stat_cols() ~ translate_string("CI = Confidence Interval"), - abbreviation = TRUE - ) + modify_abbreviation(translate_string("CI = Confidence Interval")) # updating header using `pattern=` argument x$table_styling$header <- diff --git a/R/add_overall.R b/R/add_overall.R index 4f146fec2..c2a8541b6 100644 --- a/R/add_overall.R +++ b/R/add_overall.R @@ -199,10 +199,16 @@ add_overall_merge <- function(x, tbl_overall, last, col_label, calling_fun) { } # updating table_style with footnote and column header - x$table_styling$footnote <- + x$table_styling$footnote_header <- dplyr::bind_rows( - x$table_styling$footnote, - tbl_overall$table_styling$footnote %>% + x$table_styling$footnote_header, + tbl_overall$table_styling$footnote_header %>% + dplyr::filter(.data$column %in% "stat_0") + ) + x$table_styling$footnote_body <- + dplyr::bind_rows( + x$table_styling$footnote_body, + tbl_overall$table_styling$footnote_body %>% dplyr::filter(.data$column %in% "stat_0") ) diff --git a/R/add_p.tbl_cross.R b/R/add_p.tbl_cross.R index 3dbe7bddd..6cbcd2f90 100644 --- a/R/add_p.tbl_cross.R +++ b/R/add_p.tbl_cross.R @@ -99,7 +99,7 @@ add_p.tbl_cross <- function(x, # report p-value as source note ---------------------------------------------- if (source_note == TRUE) { test_name <- - x$table_styling$footnote |> + x$table_styling$footnote_header |> dplyr::filter(.data$column %in% "p.value") |> dplyr::pull("footnote") diff --git a/R/add_significance_stars.R b/R/add_significance_stars.R index 64751626c..38714717c 100644 --- a/R/add_significance_stars.R +++ b/R/add_significance_stars.R @@ -39,7 +39,7 @@ #' hide_ci = TRUE, hide_se = TRUE #' ) |> #' modify_header(estimate = "**Beta (95% CI)**") |> -#' modify_footnote(estimate = "CI = Confidence Interval", abbreviation = TRUE) +#' modify_abbreviation("CI = Confidence Interval") #' #' # Example 3 ---------------------------------- #' # Use ' \n' to put a line break between beta and SE @@ -49,7 +49,7 @@ #' pattern = "{estimate}{stars} \n({std.error})" #' ) |> #' modify_header(estimate = "**Beta \n(SE)**") |> -#' modify_footnote(estimate = "SE = Standard Error", abbreviation = TRUE) |> +#' modify_abbreviation("SE = Standard Error") |> #' as_gt() |> #' gt::fmt_markdown(columns = everything()) |> #' gt::tab_style( @@ -118,7 +118,7 @@ add_significance_stars <- function(x, unlist() |> paste(collapse = "; ") - x <- modify_footnote(x, any_of(pattern_cols[1]) ~ p_footnote) + x <- modify_footnote_header(x, footnote = p_footnote, columns = any_of(pattern_cols[1])) # adding stars column -------------------------------------------------------- thresholds <- union(thresholds, 0L) diff --git a/R/add_stat.R b/R/add_stat.R index cb4f572b1..ac1b7c64e 100644 --- a/R/add_stat.R +++ b/R/add_stat.R @@ -43,7 +43,7 @@ #' #' - Use `modify_header()` to update the column headers #' - Use `modify_fmt_fun()` to update the functions that format the statistics -#' - Use `modify_footnote()` to add a explanatory footnote +#' - Use `modify_footnote_header()` to add a explanatory footnote #' #' If you return a tibble with column names `p.value` or `q.value`, default #' p-value formatting will be applied, and you may take advantage of subsequent diff --git a/R/as_flex_table.R b/R/as_flex_table.R index 05107f69d..fd6206527 100644 --- a/R/as_flex_table.R +++ b/R/as_flex_table.R @@ -206,47 +206,84 @@ table_styling_to_flextable_calls <- function(x, ...) { # autofit -------------------------------------------------------------------- flextable_calls[["autofit"]] <- expr(flextable::autofit()) - # footnote ------------------------------------------------------------------- + # footnote_header ------------------------------------------------------------ + df_footnote_header <- + .number_footnotes(x, "footnote_header") |> + tidyr::nest(df_location = c("column", "column_id")) |> + dplyr::mutate( + column_id = map(.data$df_location, ~ getElement(.x, "column_id")) + ) header_i_index <- ifelse(any_spanning_header == TRUE, 2L, 1L) - df_footnote <- - .number_footnotes(x) |> - dplyr::inner_join( - x$table_styling$header |> - dplyr::select("column", column_id = "id"), - by = "column" - ) |> - dplyr::mutate( - row_numbers = - ifelse(.data$tab_location == "header", - header_i_index, - .data$row_numbers + flextable_calls[["footnote_header"]] <- + map( + seq_len(nrow(df_footnote_header)), + ~ expr( + flextable::footnote( + i = !!header_i_index, + j = !!df_footnote_header$column_id[[.x]], + value = flextable::as_paragraph(!!df_footnote_header$footnote[[.x]]), + part = "header", + ref_symbols = !!df_footnote_header$footnote_id[[.x]] ) - ) |> - dplyr::select( - "footnote_id", "footnote", "tab_location", - "row_numbers", "column_id" - ) |> - tidyr::nest(location_ids = c("row_numbers", "column_id")) %>% + ) + ) + + # footnote_body -------------------------------------------------------------- + df_footnote_body <- + .number_footnotes(x, "footnote_body", start_with = nrow(df_footnote_header)) |> + tidyr::nest(df_location = c("column", "column_id", "row_numbers")) |> dplyr::mutate( - row_numbers = map(.data$location_ids, ~ getElement(.x, "row_numbers")), - column_id = map(.data$location_ids, ~ getElement(.x, "column_id")) + row_numbers = map(.data$df_location, ~ getElement(.x, "row_numbers")), + column_id = map(.data$df_location, ~ getElement(.x, "column_id")) ) - flextable_calls[["footnote"]] <- + flextable_calls[["footnote_body"]] <- map( - seq_len(nrow(df_footnote)), + seq_len(nrow(df_footnote_body)), ~ expr( flextable::footnote( - i = !!df_footnote$row_numbers[[.x]], - j = !!df_footnote$column_id[[.x]], - value = flextable::as_paragraph(!!df_footnote$footnote[[.x]]), - part = !!df_footnote$tab_location[[.x]], - ref_symbols = !!df_footnote$footnote_id[[.x]] + i = !!df_footnote_body$row_numbers[[.x]], + j = !!df_footnote_body$column_id[[.x]], + value = flextable::as_paragraph(!!df_footnote_body$footnote[[.x]]), + part = "body", + ref_symbols = !!df_footnote_body$footnote_id[[.x]] ) ) ) + + # abbreviation --------------------------------------------------------------- + flextable_calls[["abbreviations"]] <- + case_switch( + nrow(x$table_styling$abbreviation) > 0L ~ + expr( + flextable::add_footer_lines( + value = flextable::as_paragraph( + !!(x$table_styling$abbreviation$abbreviation |> + paste(collapse = ", ") %>% + paste0( + ifelse(nrow(x$table_styling$abbreviation) > 1L, "Abbreviations", "Abbreviation") |> translate_string(), + ": ", . + )) + ) + ) + ), + .default = list() + ) + + # source note ---------------------------------------------------------------- + # in flextable, this is just a footnote associated without column or symbol + flextable_calls[["source_note"]] <- + map( + seq_len(nrow(x$table_styling$source_note)), + \(i) { + expr( + flextable::add_footer_lines(value = flextable::as_paragraph(!!x$table_styling$source_note$source_note[i])) + ) + } + ) + # fmt_missing ---------------------------------------------------------------- df_fmt_missing <- x$table_styling$fmt_missing |> @@ -315,17 +352,7 @@ table_styling_to_flextable_calls <- function(x, ...) { )) ) - # source note ---------------------------------------------------------------- - # in flextable, this is just a footnote associated without column or symbol - flextable_calls[["source_note"]] <- - map( - seq_len(nrow(x$table_styling$source_note)), - \(i) { - expr( - flextable::add_footer_lines(value = flextable::as_paragraph(!!x$table_styling$source_note$source_note[i])) - ) - } - ) + # border --------------------------------------------------------------------- flextable_calls[["border"]] <- diff --git a/R/as_gt.R b/R/as_gt.R index faeb1a5af..17c318ce4 100644 --- a/R/as_gt.R +++ b/R/as_gt.R @@ -236,54 +236,42 @@ table_styling_to_gt_calls <- function(x, ...) { # tab_footnote --------------------------------------------------------------- - if (nrow(x$table_styling$footnote) == 0 && - nrow(x$table_styling$footnote_abbrev) == 0) { - gt_calls[["tab_footnote"]] <- list() - } else { - df_footnotes <- - dplyr::bind_rows( - x$table_styling$footnote, - x$table_styling$footnote_abbrev - ) |> - tidyr::nest(row_numbers = "row_numbers") %>% - dplyr::mutate( - # columns = .data$data %>% dplyr::pull("column") %>% list(), - rows = map(.data$row_numbers, \(x) unlist(x) |> unname()) - ) - df_footnotes$footnote_exp <- - map2( - df_footnotes$text_interpret, - df_footnotes$footnote, - ~ call2(parse_expr(.x), .y) - ) - - - gt_calls[["tab_footnote"]] <- - pmap( - list( - df_footnotes$tab_location, df_footnotes$footnote_exp, - df_footnotes$column, df_footnotes$rows - ), - function(tab_location, footnote, column, rows) { - if (tab_location == "header") { - return(expr( - gt::tab_footnote( - footnote = !!footnote, - locations = gt::cells_column_labels(columns = !!column) - ) - )) - } - if (tab_location == "body") { - return(expr( - gt::tab_footnote( - footnote = !!footnote, - locations = gt::cells_body(columns = !!column, rows = !!rows) - ) - )) - } + gt_calls[["tab_footnote"]] <- + c( + # header footnotes + map( + seq_len(nrow(x$table_styling$footnote_header)), + function(i) { + expr( + gt::tab_footnote( + footnote = + !!call2( + parse_expr(x$table_styling$footnote_header$text_interpret[i]), + x$table_styling$footnote_header$footnote[i] + ), + locations = gt::cells_column_labels(columns = !!x$table_styling$footnote_header$column[i]) + ) + ) + } + ), + # body footnotes + map( + seq_len(nrow(x$table_styling$footnote_body)), + function(i) { + expr( + gt::tab_footnote( + footnote = + !!call2( + parse_expr(x$table_styling$footnote_body$text_interpret[i]), + x$table_styling$footnote_body$footnote[i] + ), + locations = gt::cells_body(columns = !!x$table_styling$footnote_body$column[i], + rows = !!x$table_styling$footnote_body$row_numbers[i]) + ) + ) } ) - } + ) # spanning_header ------------------------------------------------------------ df_spanning_header <- @@ -321,9 +309,32 @@ table_styling_to_gt_calls <- function(x, ...) { ) } + # abbreviation -------------------------------------------------------------- + gt_calls[["abbreviations"]] <- + case_switch( + nrow(x$table_styling$abbreviation) > 0L ~ + expr( + gt::tab_source_note( + source_note = + !!call2( + parse_expr(dplyr::last(x$table_styling$abbreviation$text_interpret)), + x$table_styling$abbreviation$abbreviation |> + paste(collapse = ", ") %>% + paste0( + ifelse(nrow(x$table_styling$abbreviation) > 1L, "Abbreviations", "Abbreviation") |> translate_string(), + ": ", . + ) + ) + ) + ), + .default = list() + ) + + # tab_source_note ----------------------------------------------------------- # adding other calls from x$table_styling$source_note gt_calls[["tab_source_note"]] <- + # source notes map( seq_len(nrow(x$table_styling$source_note)), \(i) { @@ -335,7 +346,6 @@ table_styling_to_gt_calls <- function(x, ...) { } ) - # cols_hide ------------------------------------------------------------------ gt_calls[["cols_hide"]] <- names(x$table_body) %>% diff --git a/R/as_hux_table.R b/R/as_hux_table.R index 8af3841b2..c9e9cfe26 100644 --- a/R/as_hux_table.R +++ b/R/as_hux_table.R @@ -186,7 +186,10 @@ table_styling_to_huxtable_calls <- function(x, ...) { # footnote ------------------------------------------------------------------- vct_footnote <- - .number_footnotes(x) %>% + dplyr::bind_rows( + .number_footnotes(x, "footnote_header"), + .number_footnotes(x, "footnote_body") + ) |> dplyr::pull("footnote") %>% unique() border <- rep_len(0, length(vct_footnote)) @@ -205,6 +208,24 @@ table_styling_to_huxtable_calls <- function(x, ...) { ) } + # abbreviation --------------------------------------------------------------- + huxtable_calls[["abbreviations"]] <- + case_switch( + nrow(x$table_styling$abbreviation) > 0L ~ + expr( + huxtable::add_footnote( + text = + !!(x$table_styling$abbreviation$abbreviation |> + paste(collapse = ", ") %>% + paste0( + ifelse(nrow(x$table_styling$abbreviation) > 1L, "Abbreviations", "Abbreviation") |> translate_string(), + ": ", . + )) + ) + ), + .default = list() + ) + # source note ---------------------------------------------------------------- huxtable_calls[["source_note"]] <- map( diff --git a/R/as_kable_extra.R b/R/as_kable_extra.R index b476dd68b..19dfe55e1 100644 --- a/R/as_kable_extra.R +++ b/R/as_kable_extra.R @@ -347,10 +347,47 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... ) } + # source note ---------------------------------------------------------------- + kable_extra_calls[["source_note"]] <- + map( + seq_len(nrow(x$table_styling$source_note)), + \(i) { + expr( + kableExtra::footnote( + general = !!x$table_styling$source_note$source_note[i], + escape = !!escape, + general_title = "" + ) + ) + } + ) + + # abbreviation --------------------------------------------------------------- + kable_extra_calls[["abbreviations"]] <- + case_switch( + nrow(x$table_styling$abbreviation) > 0L ~ + expr( + kableExtra::footnote( + general = !!(x$table_styling$abbreviation$abbreviation |> + paste(collapse = ", ") %>% + paste0( + ifelse(nrow(x$table_styling$abbreviation) > 1L, "Abbreviations", "Abbreviation") |> translate_string(), + ": ", . + )), + escape = !!escape, + general_title = "" + ) + ), + .default = list() + ) + # footnote ------------------------------------------------------------------- vct_footnote <- - .number_footnotes(x) |> - dplyr::pull("footnote") |> + dplyr::bind_rows( + .number_footnotes(x, "footnote_header"), + .number_footnotes(x, "footnote_body") + ) |> + dplyr::pull("footnote") %>% unique() if (length(vct_footnote > 0)) { @@ -358,6 +395,7 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... expr(kableExtra::footnote(number = !!vct_footnote, escape = !!escape)) } + # return list of calls ------------------------------------------------------- kable_extra_calls } @@ -492,12 +530,20 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... ) # removing line breaks from footnotes - x$table_styling$footnote$footnote <- - gsub("\\n", " ", x$table_styling$footnote$footnote) %>% + x$table_styling$footnote_header$footnote <- + gsub("\\n", " ", x$table_styling$footnote_header$footnote) %>% + .escape_latex2(newlines = FALSE) %>% + .markdown_to_latex2() + x$table_styling$footnote_body$footnote <- + gsub("\\n", " ", x$table_styling$footnote_body$footnote) %>% .escape_latex2(newlines = FALSE) %>% .markdown_to_latex2() - x$table_styling$footnote_abbrev$footnote <- - gsub("\\n", " ", x$table_styling$footnote_abbrev$footnote) %>% + x$table_styling$abbreviation$abbreviation <- + gsub("\\n", " ", x$table_styling$abbreviation$abbreviation) %>% + .escape_latex2(newlines = FALSE) %>% + .markdown_to_latex2() + x$table_styling$source_note$source_note <- + gsub("\\n", " ", x$table_styling$source_note$source_note) %>% .escape_latex2(newlines = FALSE) %>% .markdown_to_latex2() @@ -523,11 +569,17 @@ table_styling_to_kable_extra_calls <- function(x, escape, format, addtl_fmt, ... .escape_html() # removing line breaks from footnotes - x$table_styling$footnote$footnote <- - gsub("\\n", " ", x$table_styling$footnote$footnote) %>% + x$table_styling$footnote_header$footnote <- + gsub("\\n", " ", x$table_styling$footnote_header$footnote) %>% + .escape_html() + x$table_styling$footnote_body$footnote <- + gsub("\\n", " ", x$table_styling$footnote_body$footnote) %>% + .escape_html() + x$table_styling$abbreviation$abbreviation <- + gsub("\\n", " ", x$table_styling$abbreviation$abbreviation) %>% .escape_html() - x$table_styling$footnote_abbrev$footnote <- - gsub("\\n", " ", x$table_styling$footnote_abbrev$footnote) %>% + x$table_styling$source_note$source_note <- + gsub("\\n", " ", x$table_styling$source_note$source_note) %>% .escape_html() if (!is.null(x$table_styling$caption)) { diff --git a/R/deprecated_modify_footnote.R b/R/deprecated_modify_footnote.R new file mode 100644 index 000000000..35bfe3187 --- /dev/null +++ b/R/deprecated_modify_footnote.R @@ -0,0 +1,69 @@ +#' DEPRECATED Footnote +#' +#' `r lifecycle::badge("deprecated")`\cr +#' Use [`modify_footnote_header()`] and [`modify_abbreviation()`] instead. +#' +#' @inheritParams modify +#' @param abbreviation (scalar `logical`)\cr +#' Logical indicating if an abbreviation is being updated. +#' @param ... [`dynamic-dots`][rlang::dyn-dots]\cr +#' Used to assign updates to footnotes. +#' Use `modify_footnote(colname='new footnote')` to update a single footnote. +#' @return Updated gtsummary object +#' @export +#' @keywords internal +#' @name deprecated_modify_footnote +#' +#' @examples +#' # Use `modify_footnote_header()`, `modify_footnote_body()`, `modify_abbreviation()` instead. +modify_footnote <- function(x, ..., abbreviation = FALSE, + text_interpret = c("md", "html"), + update, quiet) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(modify_footnote = match.call())) + + # checking inputs ------------------------------------------------------------ + check_class(x, "gtsummary") + text_interpret <- arg_match(text_interpret) + + # process inputs ------------------------------------------------------------- + dots <- rlang::dots_list(...) + dots <- .deprecate_modify_update_and_quiet_args(dots, update, quiet, calling_fun = "modify_footnote") + + # process arguments ---------------------------------------------------------- + text_interpret <- rlang::arg_match(text_interpret) + cards::process_formula_selectors(data = scope_header(x$table_body, x$table_styling$header), dots = dots) + cards::check_list_elements( + x = dots, + predicate = function(x) is_string(x) || is.na(x), + error_msg = + c("All values passed in {.arg ...} must be strings.", + "i" = "For example, {.code label='Results as of June 26, 2015'}" + ) + ) + + # evaluate the strings with glue + dots <- .evaluate_string_with_glue(x, dots) + + # updating footnotes --------------------------------------------------------- + x <- + if (!abbreviation) { + modify_table_styling( + x = x, + columns = names(dots), + footnote = unlist(dots), + text_interpret = text_interpret + ) + } else { + modify_table_styling( + x = x, + columns = names(dots), + footnote_abbrev = unlist(dots), + text_interpret = text_interpret + ) + } + + # returning gtsummary object ------------------------------------------------- + x$call_list <- updated_call_list + x +} diff --git a/R/modify.R b/R/modify.R index c42cdf133..37af7d3ed 100644 --- a/R/modify.R +++ b/R/modify.R @@ -4,7 +4,6 @@ #' These functions assist with modifying the aesthetics/style of a table. #' #' - `modify_header()` update column headers -#' - `modify_footnote()` update/add table footnotes #' - `modify_spanning_header()` update/add spanning headers #' #' The functions often require users to know the underlying column names. @@ -13,20 +12,18 @@ #' @param x (`gtsummary`)\cr #' A gtsummary object #' @param ... [`dynamic-dots`][rlang::dyn-dots]\cr -#' Used to assign updates to headers, -#' spanning headers, and footnotes. +#' Used to assign updates to headers and spanning headers. #' -#' Use `modify_*(colname='new header/footnote')` to update a single column. Using a +#' Use `modify_*(colname='new header')` to update a single column. Using a #' formula will invoke tidyselect, e.g. `modify_*(all_stat_cols() ~ "**{level}**")`. #' The dynamic dots allow syntax like `modify_header(x, !!!list(label = "Variable"))`. #' See examples below. #' #' Use the `show_header_names()` to see the column names that can be modified. -#' @param abbreviation (scalar `logical`)\cr -#' Logical indicating if an abbreviation is being updated. #' @param text_interpret (`string`)\cr #' 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 update,quiet `r lifecycle::badge("deprecated")` #' @param include_example `r lifecycle::badge("deprecated")` #' @@ -36,7 +33,7 @@ #' @name modify #' #' @section `tbl_summary()`, `tbl_svysummary()`, and `tbl_cross()`: -#' When assigning column headers, footnotes, and spanning headers, +#' When assigning column headers and spanning headers, #' you may use `{N}` to insert the number of observations. #' `tbl_svysummary` objects additionally have `{N_unweighted}` available. #' @@ -60,23 +57,21 @@ #' show_header_names(tbl) #' #' # Example 1 ---------------------------------- -#' # updating column headers and footnote +#' # updating column headers #' tbl |> -#' modify_header(label = "**Variable**", p.value = "**P**") |> -#' modify_footnote(all_stat_cols() ~ "median (IQR) for Age; n (%) for Grade") +#' modify_header(label = "**Variable**", p.value = "**P**") #' #' # Example 2 ---------------------------------- -#' # updating headers, remove all footnotes, add spanning header +#' # updating headers add spanning header #' tbl |> #' modify_header(all_stat_cols() ~ "**{level}**, N = {n} ({style_percent(p)}%)") |> -#' modify_footnote(everything() ~ NA) |> #' 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_footnote(conf.low = "CI = Credible Interval", abbreviation = TRUE) +#' modify_abbreviation("CI = Credible Interval") NULL #' @name modify @@ -126,60 +121,6 @@ modify_header <- function(x, ..., text_interpret = c("md", "html"), x } -#' @name modify -#' @export -modify_footnote <- function(x, ..., abbreviation = FALSE, - text_interpret = c("md", "html"), - update, quiet) { - set_cli_abort_call() - updated_call_list <- c(x$call_list, list(modify_footnote = match.call())) - - # checking inputs ------------------------------------------------------------ - check_class(x, "gtsummary") - text_interpret <- arg_match(text_interpret) - - # process inputs ------------------------------------------------------------- - dots <- rlang::dots_list(...) - dots <- .deprecate_modify_update_and_quiet_args(dots, update, quiet, calling_fun = "modify_footnote") - - # process arguments ---------------------------------------------------------- - text_interpret <- rlang::arg_match(text_interpret) - cards::process_formula_selectors(data = scope_header(x$table_body, x$table_styling$header), dots = dots) - cards::check_list_elements( - x = dots, - predicate = function(x) is_string(x) || is.na(x), - error_msg = - c("All values passed in {.arg ...} must be strings.", - "i" = "For example, {.code label='Results as of June 26, 2015'}" - ) - ) - - # evaluate the strings with glue - dots <- .evaluate_string_with_glue(x, dots) - - # updating footnotes --------------------------------------------------------- - x <- - if (!abbreviation) { - modify_table_styling( - x = x, - columns = names(dots), - footnote = unlist(dots), - text_interpret = text_interpret - ) - } else { - modify_table_styling( - x = x, - columns = names(dots), - footnote_abbrev = unlist(dots), - text_interpret = text_interpret - ) - } - - # returning gtsummary object ------------------------------------------------- - x$call_list <- updated_call_list - x -} - #' @name modify #' @export modify_spanning_header <- function(x, ..., text_interpret = c("md", "html"), diff --git a/R/modify_abbreviation.R b/R/modify_abbreviation.R new file mode 100644 index 000000000..f736adc9d --- /dev/null +++ b/R/modify_abbreviation.R @@ -0,0 +1,93 @@ +#' Modify Abbreviations +#' +#' All abbreviations will be coalesced when printing the final table into +#' a single source note. +#' +#' @inheritParams modify_footnote2 +#' @param abbreviation (`string`)\cr +#' a string +#' +#' @return Updated gtsummary object +#' @name modify_abbreviation +#' +#' @examplesIf (identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true")) && gtsummary:::is_pkg_installed(c("cardx", "broom", "broom.helpers")) +#' # Example 1 ---------------------------------- +#' tbl_summary( +#' trial, +#' by = trt, +#' include = age, +#' type = age ~ "continuous2" +#' ) |> +#' modify_table_body(~dplyr::mutate(.x, label = sub("Q1, Q3", "IQR", x = label))) |> +#' modify_abbreviation("IQR = Interquartile Range") +#' +#' # Example 2 ---------------------------------- +#' lm(marker ~ trt, trial) |> +#' tbl_regression() |> +#' remove_abbreviation("CI = Confidence Interval") +NULL + +#' @export +#' @rdname modify_abbreviation +modify_abbreviation <- function(x, abbreviation, 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(abbreviation) + text_interpret <- arg_match(text_interpret, error_call = get_cli_abort_call()) + + # add updates to `x$table_styling$abbreviation` ------------------------------ + x <- x |> + .modify_abbreviation(abbreviation = abbreviation, text_interpret = text_interpret) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + +#' @export +#' @rdname modify_abbreviation +remove_abbreviation <- function(x, abbreviation) { + 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(abbreviation) + if (nrow(x$table_styling$abbreviation) == 0L) { + cli::cli_abort("There are no abbreviations to remove.", call = get_cli_abort_call()) + } + if (!isTRUE(abbreviation %in% x$table_styling$abbreviation$abbreviation)) { + cli::cli_abort( + "The {.arg abbreviation} argument must be one of {.val {unique(x$table_styling$abbreviation$abbreviation)}}.", + call = get_cli_abort_call() + ) + } + + # remove abbreviation -------------------------------------------------------- + x$table_styling$abbreviation <- + x$table_styling$abbreviation |> + dplyr::filter(!.data$abbreviation %in% .env$abbreviation) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + +# column (`string`)\cr +# an optional column name from `x$table_body`. When supplied, the abbreviation +# is tied to a column and it only printed when the column appears in the +# final printed table. This is primarily used internally. +.modify_abbreviation <- function(x, abbreviation, text_interpret = "md", column = NA_character_) { + x$table_styling$abbreviation <- x$table_styling$abbreviation |> + dplyr::bind_rows( + dplyr::tibble( + column = column, + abbreviation = abbreviation, + text_interpret = paste0("gt::", text_interpret) + ) + ) + x +} diff --git a/R/modify_footnote.R b/R/modify_footnote.R new file mode 100644 index 000000000..1d4bd1e38 --- /dev/null +++ b/R/modify_footnote.R @@ -0,0 +1,213 @@ +#' Modify Footnotes +#' +#' @inheritParams modify +#' @param footnote (`string`)\cr +#' a string +#' @param columns ([`tidy-select`][dplyr::dplyr_tidy_select])\cr +#' columns to add footnote +#' @param rows (predicate `expression`)\cr +#' Predicate expression to select rows in `x$table_body`. +#' Review [rows argument details][rows_argument]. +#' @param replace (scalar `logical`)\cr +#' Logical indicating whether to replace any existing footnotes in the specified +#' 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. +#' +#' @return Updated gtsummary object +#' @name modify_footnote2 +#' +#' @examples +#' # TODO: Add examples +NULL + +#' @export +#' @rdname modify_footnote2 +modify_footnote_header <- function(x, footnote, columns, replace = TRUE, text_interpret = c("md", "html")) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(modify_footnote_header = match.call())) + + # check inputs --------------------------------------------------------------- + check_class(x, "gtsummary") + check_string(footnote) + 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 }} + ) + + # evaluate the strings with glue --------------------------------------------- + lst_footnotes <- .evaluate_string_with_glue(x, rep_named(columns, list(footnote))) + + # add updates to `x$table_styling$footnote_header` --------------------------- + x <- + .modify_footnote_header( + x, + lst_footnotes = lst_footnotes, + text_interpret = text_interpret, + replace = replace, + remove = FALSE + ) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + +#' @export +#' @rdname modify_footnote2 +modify_footnote_body <- function(x, footnote, columns, rows, 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_logical(replace) + text_interpret <- arg_match(text_interpret, error_call = get_cli_abort_call()) + .check_rows_input(x, {{ rows }}) + + # process columns ------------------------------------------------------------ + cards::process_selectors( + scope_header(x$table_body, x$table_styling$header), + columns = {{ columns }} + ) + + # evaluate the strings with glue --------------------------------------------- + lst_footnotes <- .evaluate_string_with_glue(x, rep_named(columns, list(footnote))) + + # add updates to `x$table_styling$footnote_body` ----------------------------- + x <- + .modify_footnote_body( + x, + lst_footnotes = lst_footnotes, + rows = {{ rows }}, + text_interpret = text_interpret, + replace = replace, + remove = FALSE + ) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + +#' @export +#' @rdname modify_footnote2 +remove_footnote_header <- function(x, columns) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(remove_footnote_header = match.call())) + + # check inputs --------------------------------------------------------------- + check_class(x, "gtsummary") + + # process columns ------------------------------------------------------------ + cards::process_selectors( + scope_header(x$table_body, x$table_styling$header), + columns = {{ columns }} + ) + + # add updates to `x$table_styling$footnote_header` --------------------------- + x <- + .modify_footnote_header( + x, + lst_footnotes = rep_named(columns, list(NA_character_)), + remove = TRUE + ) + + # update call list and return table ------------------------------------------ + x$call_list <- updated_call_list + x +} + +#' @export +#' @rdname modify_footnote2 +remove_footnote_body <- function(x, columns, rows) { + set_cli_abort_call() + updated_call_list <- c(x$call_list, list(remove_footnote_body = match.call())) + + # check inputs --------------------------------------------------------------- + check_class(x, "gtsummary") + .check_rows_input(x, {{ rows }}) + + # process columns ------------------------------------------------------------ + cards::process_selectors( + scope_header(x$table_body, x$table_styling$header), + columns = {{ columns }} + ) + + # add updates to `x$table_styling$footnote_body` ----------------------------- + x <- + .modify_footnote_body( + x, + lst_footnotes = rep_named(columns, list(NA_character_)), + rows = {{ rows }}, + 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) + # check rows evaluates to a logical + rows_eval_error <- + tryCatch( + eval_tidy(rows, data = x$table_body) %>% + {!is.logical(.)}, # styler: off + error = function(e) TRUE + ) + + if (rows_eval_error) { + cli::cli_abort( + "The {.arg rows} argument must be an expression that evaluates to a logical vector in {.code x$table_body}.", + call = get_cli_abort_call() + ) + } + + invisible() +} + +# modify_footnote_*() for internal use (no checking of inputs) ----------------- +.modify_footnote_header <- function(x, lst_footnotes, text_interpret = "md", + replace = TRUE, remove = FALSE) { + # add updates to `x$table_styling$footnote_header` --------------------------- + x$table_styling$footnote_header <- x$table_styling$footnote_header |> + dplyr::bind_rows( + dplyr::tibble( + column = names(lst_footnotes), + footnote = unlist(lst_footnotes) |> unname(), + text_interpret = paste0("gt::", text_interpret), + replace = replace, + remove = remove + ) + ) + + # return table --------------------------------------------------------------- + x +} + +.modify_footnote_body <- function(x, lst_footnotes, rows, text_interpret = "md", + replace = TRUE, remove = FALSE) { + # add updates to `x$table_styling$footnote_body` ----------------------------- + x$table_styling$footnote_body <- x$table_styling$footnote_body |> + dplyr::bind_rows( + dplyr::tibble( + column = names(lst_footnotes), + rows = list(enquo(rows)), + footnote = unlist(lst_footnotes) |> unname(), + text_interpret = paste0("gt::", text_interpret), + replace = replace, + remove = remove + ) + ) + + # return table --------------------------------------------------------------- + x +} diff --git a/R/modify_source_note.R b/R/modify_source_note.R index 05ed03c26..a48211ffb 100644 --- a/R/modify_source_note.R +++ b/R/modify_source_note.R @@ -22,7 +22,12 @@ #' @name modify_source_note #' #' @examplesIf identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true") +#' # Example 1 ---------------------------------- +#' tbl <- tbl_summary(trial, include = c(marker, grade), missing = "no") |> +#' modify_source_note("Results as of June 26, 2015") #' +#' # Example 2 ---------------------------------- +#' remove_source_note(tbl, source_note_id = 1) NULL #' @export diff --git a/R/modify_table_styling.R b/R/modify_table_styling.R index f30fdaf66..0baecd31d 100644 --- a/R/modify_table_styling.R +++ b/R/modify_table_styling.R @@ -1,10 +1,19 @@ #' Modify Table Styling #' +#' @description +#' This function is for developers. +#' If you are not a developer, it's recommended that you use the following +#' functions to make modifications to your table. [`modify_header()`], +#' [`modify_spanning_header()`], `[modify_column_hide()]`, [`modify_column_unhide()`], +#' [`modify_footnote_header()`], [`modify_footnote_body()`], [`modify_abbreviation()`], +#' [`modify_column_alignment()`], [`modify_fmt_fun()`], `[modify_column_indent()]`, +#' [`modify_column_merge()`]. +#' +#' #' This is a function meant for advanced users to gain #' more control over the characteristics of the resulting #' gtsummary table by directly modifying `.$table_styling`. -#' *This function is primarily used in the development of other gtsummary -#' functions, and very little checking of the passed arguments is performed.* +#' *This function has very little checking of the passed arguments.* #' #' Review the #' \href{https://www.danieldsjoberg.com/gtsummary/articles/gtsummary_definition.html}{gtsummary definition} @@ -101,7 +110,7 @@ modify_table_styling <- function(x, text_format = NULL, undo_text_format = NULL, indent = NULL, - text_interpret = c("md", "html"), + text_interpret = "md", cols_merge_pattern = NULL) { set_cli_abort_call() updated_call_list <- c(x$call_list, list(modify_table_styling = match.call())) @@ -175,8 +184,6 @@ modify_table_styling <- function(x, ) } - text_interpret <- paste0("gt::", arg_match(text_interpret)) - if (!is_empty(text_format)) { text_format <- arg_match(text_format, values = c("bold", "italic"), multiple = TRUE) } @@ -202,7 +209,7 @@ modify_table_styling <- function(x, x$table_styling$header <- x$table_styling$header %>% dplyr::rows_update( - dplyr::tibble(column = columns, interpret_label = text_interpret, label = label), + dplyr::tibble(column = columns, interpret_label = paste0("gt::", text_interpret), label = label), by = "column" ) } @@ -212,7 +219,7 @@ modify_table_styling <- function(x, x$table_styling$header <- x$table_styling$header %>% dplyr::rows_update( - dplyr::tibble(column = columns, interpret_spanning_header = text_interpret, spanning_header = spanning_header), + dplyr::tibble(column = columns, interpret_spanning_header = paste0("gt::", text_interpret), spanning_header = spanning_header), by = "column" ) } @@ -239,29 +246,38 @@ modify_table_styling <- function(x, # footnote ------------------------------------------------------------------- if (!is_empty(footnote)) { - x$table_styling$footnote <- - dplyr::bind_rows( - x$table_styling$footnote, - dplyr::tibble( - column = columns, - rows = list(rows), + # header footnotes + if (tryCatch(is.null(eval_tidy(rows)), error = \(x) FALSE)) { + x <- + .modify_footnote_header( + x = x, + lst_footnotes = + rep_named(columns, as.list(footnote)), text_interpret = text_interpret, - footnote = footnote + replace = TRUE, + remove = is.na(footnote) ) - ) + } + else { + x <- + .modify_footnote_body( + x = x, + lst_footnotes = rep_named(columns, as.list(footnote)), + rows = !!rows, + text_interpret = text_interpret, + replace = TRUE, + remove = is.na(footnote) + ) + } } # footnote_abbrev ------------------------------------------------------------ if (!is_empty(footnote_abbrev)) { - x$table_styling$footnote_abbrev <- - dplyr::bind_rows( - x$table_styling$footnote_abbrev, - dplyr::tibble( - column = columns, - rows = list(rows), - text_interpret = text_interpret, - footnote = footnote_abbrev - ) + x <- x |> + .modify_abbreviation( + abbreviation = footnote_abbrev, + text_interpret = text_interpret, + column = columns ) } diff --git a/R/rows_argument.R b/R/rows_argument.R new file mode 100644 index 000000000..8e4a7a1a0 --- /dev/null +++ b/R/rows_argument.R @@ -0,0 +1,19 @@ +#' @title `rows` argument +#' @keywords internal +#' @name rows_argument +#' +#' @description +#' The rows argument accepts a predicate expression that is used to specify +#' rows to apply formatting. The expression must evaluate to a logical when +#' evaluated in `x$table_body`. For example, to apply formatting to the age rows +#' pass `rows = variable == "age"`. A vector of row numbers is NOT acceptable. +#' +#' A couple of things to note when using the `rows` argument. +#' 1. You can use saved objects to create the predicate argument, e.g. +#' `rows = variable == letters[1]`. +#' 2. The saved object cannot share a name with a column in `x$table_body`. +#' The reason for this is that in `tbl_merge()` the columns are renamed, +#' and the renaming process cannot disambiguate the `variable` column from +#' an external object named `variable` in the following expression +#' `rows = .data$variable = .env$variable`. +NULL diff --git a/R/separate_p_footnotes.R b/R/separate_p_footnotes.R index cf5876242..8e9b9382b 100644 --- a/R/separate_p_footnotes.R +++ b/R/separate_p_footnotes.R @@ -34,7 +34,7 @@ separate_p_footnotes <- function(x) { } # remove p-value column footnote --------------------------------------------- - x <- modify_footnote(x, any_of(c("p.value", "estimate", "conf.low", "conf.high")) ~ NA_character_) + x <- remove_footnote_header(x, columns = any_of(c("p.value", "estimate", "conf.low", "conf.high"))) # extract footnote next for each variable ------------------------------------ calling_fun <- names(x$call_list) |> intersect(c("add_p", "add_difference")) diff --git a/R/tbl_ard_continuous.R b/R/tbl_ard_continuous.R index b5254d4f3..2883d952b 100644 --- a/R/tbl_ard_continuous.R +++ b/R/tbl_ard_continuous.R @@ -184,14 +184,14 @@ tbl_ard_continuous <- function(cards, variable, include, by = NULL, label = NULL modify_header(all_stat_cols() ~ "**{level}**") # prepend the footnote with information about the variable ------------------- - x$table_styling$footnote$footnote <- + x$table_styling$footnote_header$footnote <- paste0( cards |> dplyr::filter(.data$context == "attributes", .data$variable == .env$variable, .data$stat_name == "label") |> dplyr::pull("stat") |> unlist(), ": ", - x$table_styling$footnote$footnote + x$table_styling$footnote_header$footnote ) # add other information to the returned object diff --git a/R/tbl_continuous.R b/R/tbl_continuous.R index 46f4ff395..431f57842 100644 --- a/R/tbl_continuous.R +++ b/R/tbl_continuous.R @@ -20,7 +20,7 @@ #' @return a gtsummary table #' @export #' -#' @examples +#' @examplesIf identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true") #' # Example 1 ---------------------------------- #' tbl_continuous( #' data = trial, @@ -199,14 +199,14 @@ tbl_continuous <- function(data, ) # prepend the footnote with information about the variable ------------------- - x$table_styling$footnote$footnote <- + x$table_styling$footnote_header$footnote <- paste0( cards |> dplyr::filter(.data$context == "attributes", .data$variable == .env$variable, .data$stat_name == "label") |> dplyr::pull("stat") |> unlist(), ": ", - x$table_styling$footnote$footnote + x$table_styling$footnote_header$footnote ) x |> diff --git a/R/tbl_cross.R b/R/tbl_cross.R index d5f1dcd1a..7f9f43a9f 100644 --- a/R/tbl_cross.R +++ b/R/tbl_cross.R @@ -170,7 +170,7 @@ tbl_cross <- function(data, type = list(all_of(row) ~ "categorical", any_of("..total..") ~ "dichotomous") ) |> modify_header(all_stat_cols(FALSE) ~ "{level}", label = "") |> - modify_footnote(everything() ~ NA_character_) |> + remove_footnote_header(everything()) |> modify_spanning_header(all_stat_cols(FALSE) ~ label[[col]]) # adding column margin @@ -178,7 +178,7 @@ tbl_cross <- function(data, x <- x |> add_overall(last = TRUE) |> modify_header(stat_0 = margin_text) |> - modify_footnote(stat_0 = NA_character_) + remove_footnote_header("stat_0") } # returning results ---------------------------------------------------------- diff --git a/R/tbl_custom_summary.R b/R/tbl_custom_summary.R index 6d12b8839..a57872178 100644 --- a/R/tbl_custom_summary.R +++ b/R/tbl_custom_summary.R @@ -86,13 +86,13 @@ #' to the total number, number missing and number non missing observations #' in the denominator, not at each level of the categorical variable. #' -#' It is recommended to use [`modify_footnote()`] to properly describe the +#' It is recommended to use [`modify_footnote_header()`] to properly describe the #' displayed statistics (see examples). #' #' @section Caution: #' #' The returned table is compatible with all `gtsummary` features applicable -#' to a `tbl_summary` object, like [`add_overall()`], [`modify_footnote()`] or +#' to a `tbl_summary` object, like [`add_overall()`], [`modify_footnote_header()`] or #' [`bold_labels()`]. #' #' However, some of them could be inappropriate in such case. In particular, @@ -129,8 +129,9 @@ #' overall_row_label = "All stages & grades" #' ) |> #' add_overall(last = TRUE) |> -#' modify_footnote( -#' all_stat_cols() ~ "A: mean age - S: sum of marker" +#' modify_footnote_header( +#' footnote = "A: mean age - S: sum of marker", +#' columns = all_stat_cols() #' ) |> #' bold_labels() #' @@ -153,8 +154,9 @@ #' statistic = ~ "{mean} [{conf.low}; {conf.high}]" #' ) |> #' add_overall(last = TRUE) |> -#' modify_footnote( -#' all_stat_cols() ~ "mean [95% CI]" +#' modify_footnote_header( +#' footnote = "mean [95% CI]", +#' columns = all_stat_cols() #' ) #' #' # Example 3 ---------------------------------- diff --git a/R/tbl_merge.R b/R/tbl_merge.R index 8eb54b717..9712201cd 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", "footnote_abbrev", "fmt_fun", "indent", "text_format", "fmt_missing", "cols_merge")) { + for (style_type in c("footnote_header", "footnote_body", "abbreviation", "source_note", + "fmt_fun", "indent", "text_format", "fmt_missing", "cols_merge")) { x$table_styling[[style_type]] <- map( rev(seq_along(tbls)), @@ -244,13 +245,15 @@ tbl_merge <- function(tbls, tab_spanner = NULL) { } # renaming column variable - style_updated$column <- - ifelse( - style_updated$column %in% c("label", "variable", "var_label", "row_type"), - style_updated$column, - paste0(style_updated$column, "_", i) - ) %>% - as.character() + if ("column" %in% names(style_updated)) { + style_updated$column <- + ifelse( + style_updated$column %in% c("label", "variable", "var_label", "row_type") | is.na(style_updated$column), + style_updated$column, + paste0(style_updated$column, "_", i) + ) %>% + as.character() + } # updating column names in rows expr/quo if ("rows" %in% names(style_updated)) { @@ -277,7 +280,7 @@ tbl_merge <- function(tbls, tab_spanner = NULL) { } # take the first non-NULL element from tbls[[.]] - for (style_type in c("caption", "source_note")) { + for (style_type in c("caption")) { x$table_styling[[style_type]] <- map(seq_along(tbls), ~ getElement(tbls, .x) |> getElement("table_styling") |> getElement(style_type)) %>% reduce(.f = \(.x, .y) .x %||% .y) diff --git a/R/tbl_regression.R b/R/tbl_regression.R index b594d004d..ff327076e 100644 --- a/R/tbl_regression.R +++ b/R/tbl_regression.R @@ -69,7 +69,7 @@ #' @name tbl_regression #' @return A `tbl_regression` object #' -#' @examplesIf gtsummary:::is_pkg_installed(c("cardx", "broom", "broom.helpers")) +#' @examplesIf (identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true")) && gtsummary:::is_pkg_installed(c("cardx", "broom", "broom.helpers")) #' # Example 1 ---------------------------------- #' glm(response ~ age + grade, trial, family = binomial()) |> #' tbl_regression(exponentiate = TRUE) diff --git a/R/tbl_stack.R b/R/tbl_stack.R index 6e561eaa5..a556ad811 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", "footnote_abbrev", "fmt_fun", "text_format", "indent", "fmt_missing", "cols_merge")) { + for (style_type in c("footnote_header", "footnote_body", "abbreviation", "source_note", + "fmt_fun", "text_format", "indent", "fmt_missing", "cols_merge")) { results$table_styling[[style_type]] <- map( rev(seq_along(tbls)), @@ -142,7 +143,7 @@ tbl_stack <- function(tbls, group_header = NULL, quiet = FALSE) { } # take the first non-NULL element from tbls[[.]] - for (style_type in c("caption", "source_note", "horizontal_line_above")) { + for (style_type in c("caption", "horizontal_line_above")) { results$table_styling[[style_type]] <- map(seq_along(tbls), ~ tbls[[.x]][["table_styling"]][[style_type]]) |> reduce(.f = \(.x, .y) .x %||% .y) diff --git a/R/tbl_strata.R b/R/tbl_strata.R index 948be37c5..40e813426 100644 --- a/R/tbl_strata.R +++ b/R/tbl_strata.R @@ -82,7 +82,7 @@ #' ) |> #' add_ci(pattern = "{stat} ({ci})") |> #' modify_header(stat_0 = "**Rate (95% CI)**") |> -#' modify_footnote(stat_0 = NA), +#' remove_footnote_header(stat_0), #' .combine_with = "tbl_stack", #' .combine_args = list(group_header = NULL) #' ) |> diff --git a/R/tbl_summary.R b/R/tbl_summary.R index 971cc6c6b..ac0289602 100644 --- a/R/tbl_summary.R +++ b/R/tbl_summary.R @@ -611,7 +611,7 @@ tbl_summary <- function(data, c( "!" = "Column(s) {.val {haven_labelled_vars}} are class {.val haven_labelled}.", - "i" = "This is an intermediate datastructure not meant for analysis.", + "i" = "This is an intermediate data structure not meant for analysis.", "i" = "Convert columns with {.fun {cnvt_funs}}. Failure to convert may have unintended consequences or result in error.", paste0("{.url ", hyperlinks, "}") ) |> diff --git a/R/theme_gtsummary.R b/R/theme_gtsummary.R index 1db9f148c..824cce765 100644 --- a/R/theme_gtsummary.R +++ b/R/theme_gtsummary.R @@ -120,14 +120,6 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco "**)**" ) - # adding CI footnote to any existing abbreviation footnote, e.g. for OR, HR, etc. - estimate_footnote <- - x$table_styling$footnote_abbrev |> - dplyr::filter(.data$column %in% "estimate") |> - dplyr::filter(dplyr::row_number() == dplyr::n(), !is.na(.data$footnote)) |> - dplyr::pull("footnote") |> - c("CI = Confidence Interval") |> - paste(collapse = ", ") x %>% # merge estimate and CI into one cell modify_column_merge( @@ -136,9 +128,7 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco pattern = "{estimate} ({conf.low} to {conf.high})" ) |> # update column header - modify_header(estimate = new_header_text) |> - # add CI abbreviation footnote - modify_footnote(estimate = estimate_footnote, abbreviation = TRUE) + modify_header(estimate = new_header_text) }, error = function(e) x ) @@ -153,14 +143,6 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco " **(", style_number(x$inputs$conf.level, scale = 100), "% CI)**" ) - # adding CI footnote to any existing abbreviation footnote, e.g. for OR, HR, etc. - estimate_footnote <- - x$table_styling$footnote_abbrev |> - dplyr::filter(.data$column %in% "estimate") |> - dplyr::filter(dplyr::row_number() == dplyr::n(), !is.na(.data$footnote)) |> - dplyr::pull("footnote") |> - c("CI = Confidence Interval") |> - paste(collapse = ", ") x %>% # merge estimate and CI into one cell modify_column_merge( @@ -170,9 +152,7 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco pattern = "{estimate} ({conf.low} to {conf.high})" ) |> # update column header - modify_header(estimate = new_header_text) |> - # add CI abbreviation footnote - modify_footnote(estimate = estimate_footnote, abbreviation = TRUE) + modify_header(estimate = new_header_text) }, error = function(e) x ) @@ -190,8 +170,8 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco all_categorical() ~ "{n} ({p})"), "pkgwide-str:ci.sep" = " to ", "tbl_summary-fn:addnl-fn-to-run" = function(x) { - x$table_styling$footnote$footnote <- - gsub("Q1 \U2013 Q3", "IQR", x = x$table_styling$footnote$footnote) + x$table_styling$footnote_header$footnote <- + gsub("Q1 \U2013 Q3", "IQR", x = x$table_styling$footnote_header$footnote) x } ) @@ -258,8 +238,8 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco "style_number-arg:big.mark" = "\U2009", "pkgwide-str:ci.sep" = " to ", "tbl_summary-fn:addnl-fn-to-run" = function(x) { - x$table_styling$footnote$footnote <- - gsub("Q1 \U2013 Q3", "IQR", x = x$table_styling$footnote$footnote) + x$table_styling$footnote_header$footnote <- + gsub("Q1 \U2013 Q3", "IQR", x = x$table_styling$footnote_header$footnote) x } ) @@ -277,23 +257,13 @@ theme_gtsummary_journal <- function(journal = c("jama", "lancet", "nejm", "qjeco sep = " \n" ) - estimate_footnote <- - x$table_styling$footnote_abbrev |> - dplyr::filter(.data$column %in% "estimate") |> - dplyr::filter(dplyr::row_number() == dplyr::n(), !is.na(.data$footnote)) |> - dplyr::pull("footnote") |> - c("SE = Standard Error") |> - paste(collapse = ", ") - x |> add_significance_stars( pattern = "{estimate}{stars} \n({std.error})", hide_se = TRUE ) |> # update column header - modify_header(estimate = new_header_text) |> - # add SE abbreviation footnote - modify_footnote(estimate = estimate_footnote, abbreviation = TRUE) + modify_header(estimate = new_header_text) }, "as_gt-lst:addl_cmds" = list( tab_spanner = list( diff --git a/R/utils-as.R b/R/utils-as.R index eba2e73be..87a4b695e 100644 --- a/R/utils-as.R +++ b/R/utils-as.R @@ -127,13 +127,48 @@ dplyr::mutate(row_numbers = unlist(.data$row_numbers) %>% unname() %>% list()) %>% dplyr::ungroup() - # footnote ------------------------------------------------------------------- - x$table_styling$footnote <- - .table_styling_expr_to_row_number_footnote(x, "footnote") + # footnote_header ------------------------------------------------------------ + x$table_styling$footnote_header <- + x$table_styling$footnote_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, 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]) + + # footnote_body -------------------------------------------------------------- + x$table_styling$footnote_body <- + x$table_styling$footnote_body |> + dplyr::mutate( + remove = ifelse(is.na(.data$footnote), TRUE, .data$remove), # this is a hold-over from pre-v2.0.0 syntax where NA removed footnotes. + # convert rows predicate expression to row numbers + row_numbers = + map( + .data$rows, + \(rows) .rows_expr_to_row_numbers(x$table_body, rows) + ) + ) |> + tidyr::unnest(cols = "row_numbers") |> + # within a column/row, 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]) |> + 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_abbrev ------------------------------------------------------------ - x$table_styling$footnote_abbrev <- - .table_styling_expr_to_row_number_footnote(x, "footnote_abbrev") + # abbreviation --------------------------------------------------------------- + abbreviation_cols <- + x$table_styling$header$column[!x$table_styling$header$hide] |> + union(discard(x$table_styling$cols_merge$pattern, is.na) |> .extract_glue_elements()) |> + union(NA_character_) + x$table_styling$abbreviation <- + x$table_styling$abbreviation |> + dplyr::filter(.data$column %in% .env$abbreviation_cols) |> + dplyr::slice_tail(n = 1L, by = "abbreviation") |> + dplyr::arrange(.data$abbreviation) # fmt_fun -------------------------------------------------------------------- x$table_styling$fmt_fun <- @@ -179,84 +214,58 @@ x } -.table_styling_expr_to_row_number_footnote <- function(x, footnote_type) { - df_clean <- - x$table_styling[[footnote_type]] %>% - dplyr::filter(.data$column %in% .cols_to_show(x)) - if (nrow(df_clean) == 0) { - return(dplyr::tibble( - column = character(0), tab_location = character(0), row_numbers = logical(0), - text_interpret = character(0), footnote = character(0) - )) - } - df_clean <- - df_clean %>% - dplyr::rowwise() %>% - dplyr::mutate( - row_numbers = - switch(nrow(.) == 0, - integer(0) - ) %||% - .rows_expr_to_row_numbers(x$table_body, .data$rows) %>% list(), - tab_location = ifelse(identical(.data$row_numbers, NA), "header", "body") - ) %>% - dplyr::select(-"rows") %>% - tidyr::unnest(cols = "row_numbers") %>% - dplyr::group_by(.data$column, .data$tab_location, .data$row_numbers) %>% - dplyr::filter(dplyr::row_number() == dplyr::n()) %>% - # keeping the most recent addition - dplyr::filter(!is.na(.data$footnote)) # keep non-missing additions - - if (footnote_type == "footnote_abbrev") { - # order the footnotes by where they first appear in the table, - df_clean <- - df_clean %>% - dplyr::inner_join( - x$table_styling$header %>% - select("column") %>% - mutate(column_id = dplyr::row_number()), - by = "column" - ) %>% - dplyr::arrange(dplyr::desc(.data$tab_location), .data$column_id, .data$row_numbers) %>% - dplyr::ungroup() %>% - dplyr::mutate(footnote = paste(unique(.data$footnote), collapse = ", ")) - } +# this function processes the footnotes and removes footnotes that have +# later been replaced or removed from the table +.filter_row_with_subsequent_replace_or_removal <- function(x) { + if (nrow(x) == 0L) return(x) - df_clean %>% - dplyr::select(all_of(c("column", "tab_location", "row_numbers", "text_interpret", "footnote"))) + # 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")), + !unlist( + pmap( + list(.data$replace, .data$remove, dplyr::row_number()), + function(row_replace, row_remove, row_number) { + # if this is the last row in the group, there will be now removal indications below + if (row_number == dplyr::n()) return(FALSE) + # if a subsequent call to replace or remove a footnote appear below, + # then the current row can be deleted. + any(.data$replace[seq(row_number + 1L, dplyr::n())]) || + any(.data$remove[seq(row_number + 1L, dplyr::n())]) + } + ) + ) + ) } # this function orders the footnotes by where they first appear in the table, # and assigns them an sequential ID -.number_footnotes <- function(x) { - if (nrow(x$table_styling$footnote) == 0 && - nrow(x$table_styling$footnote_abbrev) == 0) { +.number_footnotes <- function(x, type, start_with = 0L) { + # if empty, return empty data frame + if (nrow(x$table_styling[[type]]) == 0L) { return(dplyr::tibble( footnote_id = integer(), footnote = character(), column = character(), - tab_location = character(), row_numbers = integer() + column_id = integer(), row_numbers = integer() )) } - dplyr::bind_rows( - x$table_styling$footnote, - x$table_styling$footnote_abbrev - ) %>% - dplyr::inner_join( - x$table_styling$header %>% - select("column") %>% - mutate(column_id = dplyr::row_number()), - by = "column" - ) %>% - dplyr::arrange(dplyr::desc(.data$tab_location), .data$column_id, .data$row_numbers) %>% - dplyr::group_by(.data$footnote) %>% - tidyr::nest() %>% - dplyr::ungroup() %>% - dplyr::mutate(footnote_id = dplyr::row_number()) %>% - tidyr::unnest(cols = "data") %>% - dplyr::select( - "footnote_id", "footnote", "column", - "tab_location", "row_numbers" - ) + + # adding the footnote number to assign to each of the footnotes + dplyr::inner_join( + x$table_styling$header |> + select("column", column_id = "id") |> + dplyr::filter(!is.na(.data$column_id)), + x$table_styling[[type]], + by = "column" + ) |> + dplyr::arrange(dplyr::pick(any_of(c("column_id", "row_numbers")))) |> + dplyr::group_by(.data$footnote) |> + tidyr::nest() |> + dplyr::ungroup() |> + dplyr::mutate(footnote_id = dplyr::row_number() + .env$start_with) |> + tidyr::unnest(cols = "data") |> + dplyr::select(any_of(c("footnote_id", "footnote", "column", "column_id", "row_numbers"))) } diff --git a/R/utils-gtsummary_core.R b/R/utils-gtsummary_core.R index e48a05468..f7b850f04 100644 --- a/R/utils-gtsummary_core.R +++ b/R/utils-gtsummary_core.R @@ -28,16 +28,23 @@ hide = ifelse(.data$column %in% "label", FALSE, .data$hide), align = ifelse(.data$column %in% "label", "left", .data$align) ) - - x$table_styling$footnote <- + x$table_styling$footnote_header <- dplyr::tibble( - column = character(), rows = list(), - text_interpret = character(), footnote = character() + column = character(), + footnote = character(), text_interpret = character(), + replace = logical(), remove = logical() ) - x$table_styling$footnote_abbrev <- + x$table_styling$footnote_body <- dplyr::tibble( column = character(), rows = list(), - text_interpret = character(), footnote = character() + footnote = character(), 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( @@ -79,64 +86,6 @@ structure(class = "gtsummary") } -# construct_initial_table_styling <- function(x) { -# # table_styling -------------------------------------------------------------- -# x$table_styling$header <- -# dplyr::tibble( -# column = names(x$table_body), -# hide = TRUE, -# align = "center", -# interpret_label = "gt::md", -# label = names(x$table_body), -# interpret_spanning_header = "gt::md", -# spanning_header = NA_character_ -# ) %>% -# dplyr::mutate( -# hide = ifelse(.data$column %in% "label", FALSE, .data$hide), -# align = ifelse(.data$column %in% "label", "left", .data$align) -# ) -# -# x$table_styling$footnote <- -# dplyr::tibble( -# column = character(), rows = list(), -# text_interpret = character(), footnote = character() -# ) -# x$table_styling$footnote_abbrev <- -# dplyr::tibble( -# column = character(), rows = list(), -# text_interpret = character(), footnote = character() -# ) -# x$table_styling$text_format <- -# dplyr::tibble( -# column = character(), rows = list(), -# format_type = character(), undo_text_format = logical() -# ) -# -# x$table_styling$indent <- -# # if there is a label column, make it -# if ("label" %in% x$table_styling$header$column) { -# dplyr::tibble( -# column = "label", -# rows = list(rlang::expr(TRUE)), -# n_spaces = 0L -# ) -# } else { -# dplyr::tibble(column = character(), rows = list(), n_spaces = integer()) -# } -# -# x$table_styling$fmt_missing <- -# dplyr::tibble(column = character(), rows = list(), symbol = character()) -# x$table_styling$fmt_fun <- -# dplyr::tibble(column = character(), rows = list(), fmt_fun = list()) -# x$table_styling$cols_merge <- -# dplyr::tibble(column = character(), rows = list(), pattern = character()) -# -# -# # returning gtsummary object ------------------------------------------------- -# x |> -# structure(class = "gtsummary") -# } - .purrr_when <- function(...) { lst_formulas <- rlang::dots_list(...) diff --git a/R/utils-tbl_custom_summary.R b/R/utils-tbl_custom_summary.R index 24550e3af..bf768d221 100644 --- a/R/utils-tbl_custom_summary.R +++ b/R/utils-tbl_custom_summary.R @@ -76,7 +76,7 @@ continuous_summary <- function(variable) { #' overall_row_label = "All stages & grades" #' ) |> #' bold_labels() |> -#' modify_footnote(all_stat_cols() ~ "Ratio [95% CI] (n/N)") +#' modify_footnote_header("Ratio [95% CI] (n/N)", columns = all_stat_cols()) ratio_summary <- function(numerator, denominator, na.rm = TRUE, conf.level = 0.95) { function(data, ...) { num <- sum(data[[numerator]], na.rm = na.rm) @@ -162,7 +162,7 @@ ratio_summary <- function(numerator, denominator, na.rm = TRUE, conf.level = 0.9 #' overall_row_last = TRUE #' ) |> #' bold_labels() |> -#' modify_footnote(all_stat_cols() ~ "Proportion (%) of survivors (n/N) [95% CI]") +#' modify_footnote_header("Proportion (%) of survivors (n/N) [95% CI]", columns = all_stat_cols()) proportion_summary <- function(variable, value, weights = NULL, na.rm = TRUE, conf.level = 0.95, method = c("wilson", "wilson.no.correct", "wald", "wald.no.correct", "exact", "agresti.coull", "jeffreys")) { # process arguments ---------------------------------------------------------- diff --git a/inst/WORDLIST b/inst/WORDLIST index 0655b37bf..d553a93aa 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -47,6 +47,7 @@ cli codebase coef conf +coxph customizability customizable der @@ -62,6 +63,7 @@ forcats ftExtra ggplot ggstats +glm hochberg holm hommel @@ -73,6 +75,7 @@ labelled lifecycle likert linebreaks +lm lme mL mis diff --git a/man/add_ci.Rd b/man/add_ci.Rd index 6d709779a..4d0607a1d 100644 --- a/man/add_ci.Rd +++ b/man/add_ci.Rd @@ -94,6 +94,6 @@ trial |> include = c(response, grade) ) |> add_ci(pattern = "{stat} ({ci})") |> - modify_footnote(everything() ~ NA) + remove_footnote_header(everything()) \dontshow{\}) # examplesIf} } diff --git a/man/add_significance_stars.Rd b/man/add_significance_stars.Rd index 2c08a78b8..8dcbf50aa 100644 --- a/man/add_significance_stars.Rd +++ b/man/add_significance_stars.Rd @@ -61,7 +61,7 @@ tbl |> hide_ci = TRUE, hide_se = TRUE ) |> modify_header(estimate = "**Beta (95\% CI)**") |> - modify_footnote(estimate = "CI = Confidence Interval", abbreviation = TRUE) + modify_abbreviation("CI = Confidence Interval") # Example 3 ---------------------------------- # Use ' \n' to put a line break between beta and SE @@ -71,7 +71,7 @@ tbl |> pattern = "{estimate}{stars} \n({std.error})" ) |> modify_header(estimate = "**Beta \n(SE)**") |> - modify_footnote(estimate = "SE = Standard Error", abbreviation = TRUE) |> + modify_abbreviation("SE = Standard Error") |> as_gt() |> gt::fmt_markdown(columns = everything()) |> gt::tab_style( diff --git a/man/add_stat.Rd b/man/add_stat.Rd index 56c670ecb..530dd540f 100644 --- a/man/add_stat.Rd +++ b/man/add_stat.Rd @@ -58,7 +58,7 @@ the user's function, e.g. \code{foo(data, variable, by, ...)} \itemize{ \item Use \code{modify_header()} to update the column headers \item Use \code{modify_fmt_fun()} to update the functions that format the statistics -\item Use \code{modify_footnote()} to add a explanatory footnote +\item Use \code{modify_footnote_header()} to add a explanatory footnote } If you return a tibble with column names \code{p.value} or \code{q.value}, default diff --git a/man/deprecated_modify_footnote.Rd b/man/deprecated_modify_footnote.Rd new file mode 100644 index 000000000..f650df236 --- /dev/null +++ b/man/deprecated_modify_footnote.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/deprecated_modify_footnote.R +\name{deprecated_modify_footnote} +\alias{deprecated_modify_footnote} +\alias{modify_footnote} +\title{DEPRECATED Footnote} +\usage{ +modify_footnote( + x, + ..., + abbreviation = FALSE, + text_interpret = c("md", "html"), + update, + quiet +) +} +\arguments{ +\item{x}{(\code{gtsummary})\cr +A gtsummary object} + +\item{...}{\code{\link[rlang:dyn-dots]{dynamic-dots}}\cr +Used to assign updates to footnotes. +Use \code{modify_footnote(colname='new footnote')} to update a single footnote.} + +\item{abbreviation}{(scalar \code{logical})\cr +Logical indicating if an abbreviation is being updated.} + +\item{text_interpret}{(\code{string})\cr +String indicates whether text will be interpreted with +\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}. +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]}}} +} +\value{ +Updated gtsummary object +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}\cr +Use \code{\link[=modify_footnote_header]{modify_footnote_header()}} and \code{\link[=modify_abbreviation]{modify_abbreviation()}} instead. +} +\examples{ +# Use `modify_footnote_header()`, `modify_footnote_body()`, `modify_abbreviation()` instead. +} +\keyword{internal} diff --git a/man/modify.Rd b/man/modify.Rd index d433c9038..4c5b6d5a8 100644 --- a/man/modify.Rd +++ b/man/modify.Rd @@ -3,22 +3,12 @@ \name{modify} \alias{modify} \alias{modify_header} -\alias{modify_footnote} \alias{modify_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_footnote( - x, - ..., - abbreviation = FALSE, - text_interpret = c("md", "html"), - update, - quiet -) - modify_spanning_header(x, ..., text_interpret = c("md", "html"), quiet, update) show_header_names(x, include_example, quiet) @@ -28,10 +18,9 @@ show_header_names(x, include_example, quiet) A gtsummary object} \item{...}{\code{\link[rlang:dyn-dots]{dynamic-dots}}\cr -Used to assign updates to headers, -spanning headers, and footnotes. +Used to assign updates to headers and spanning headers. -Use \code{modify_*(colname='new header/footnote')} to update a single column. Using a +Use \code{modify_*(colname='new header')} to update a single column. Using a formula will invoke tidyselect, e.g. \code{modify_*(all_stat_cols() ~ "**{level}**")}. The dynamic dots allow syntax like \code{modify_header(x, !!!list(label = "Variable"))}. See examples below. @@ -40,13 +29,11 @@ Use the \code{show_header_names()} to see the column names that can be modified. \item{text_interpret}{(\code{string})\cr String indicates whether text will be interpreted with -\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}.} +\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}. +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{abbreviation}{(scalar \code{logical})\cr -Logical indicating if an abbreviation is being updated.} - \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{ @@ -56,7 +43,6 @@ Updated gtsummary object These functions assist with modifying the aesthetics/style of a table. \itemize{ \item \code{modify_header()} update column headers -\item \code{modify_footnote()} update/add table footnotes \item \code{modify_spanning_header()} update/add spanning headers } @@ -65,7 +51,7 @@ Run \code{show_header_names()} to print the column names to the console. } \section{\code{tbl_summary()}, \code{tbl_svysummary()}, and \code{tbl_cross()}}{ -When assigning column headers, footnotes, and spanning headers, +When assigning column headers and spanning headers, you may use \code{{N}} to insert the number of observations. \code{tbl_svysummary} objects additionally have \code{{N_unweighted}} available. @@ -93,23 +79,21 @@ tbl <- trial |> show_header_names(tbl) # Example 1 ---------------------------------- -# updating column headers and footnote +# updating column headers tbl |> - modify_header(label = "**Variable**", p.value = "**P**") |> - modify_footnote(all_stat_cols() ~ "median (IQR) for Age; n (\%) for Grade") + modify_header(label = "**Variable**", p.value = "**P**") # Example 2 ---------------------------------- -# updating headers, remove all footnotes, add spanning header +# updating headers add spanning header tbl |> modify_header(all_stat_cols() ~ "**{level}**, N = {n} ({style_percent(p)}\%)") |> - modify_footnote(everything() ~ NA) |> 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_footnote(conf.low = "CI = Credible Interval", abbreviation = TRUE) + modify_abbreviation("CI = Credible Interval") } \author{ Daniel D. Sjoberg diff --git a/man/modify_abbreviation.Rd b/man/modify_abbreviation.Rd new file mode 100644 index 000000000..f0959b145 --- /dev/null +++ b/man/modify_abbreviation.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modify_abbreviation.R +\name{modify_abbreviation} +\alias{modify_abbreviation} +\alias{remove_abbreviation} +\title{Modify Abbreviations} +\usage{ +modify_abbreviation(x, abbreviation, text_interpret = c("md", "html")) + +remove_abbreviation(x, abbreviation) +} +\arguments{ +\item{x}{(\code{gtsummary})\cr +A gtsummary object} + +\item{abbreviation}{(\code{string})\cr +a string} + +\item{text_interpret}{(\code{string})\cr +String indicates whether text will be interpreted with +\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}. +Applies to tables printed with \code{{gt}}.} +} +\value{ +Updated gtsummary object +} +\description{ +All abbreviations will be coalesced when printing the final table into +a single source note. +} +\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} +# Example 1 ---------------------------------- +tbl_summary( + trial, + by = trt, + include = age, + type = age ~ "continuous2" +) |> + modify_table_body(~dplyr::mutate(.x, label = sub("Q1, Q3", "IQR", x = label))) |> + modify_abbreviation("IQR = Interquartile Range") + +# Example 2 ---------------------------------- +lm(marker ~ trt, trial) |> + tbl_regression() |> + remove_abbreviation("CI = Confidence Interval") +\dontshow{\}) # examplesIf} +} diff --git a/man/modify_caption.Rd b/man/modify_caption.Rd index 89da945f8..8addf3fed 100644 --- a/man/modify_caption.Rd +++ b/man/modify_caption.Rd @@ -15,7 +15,8 @@ A string for the table caption/title} \item{text_interpret}{(\code{string})\cr String indicates whether text will be interpreted with -\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}.} +\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}. +Applies to tables printed with \code{{gt}}.} } \value{ Updated gtsummary object diff --git a/man/modify_footnote2.Rd b/man/modify_footnote2.Rd new file mode 100644 index 000000000..4ca39532d --- /dev/null +++ b/man/modify_footnote2.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modify_footnote.R +\name{modify_footnote2} +\alias{modify_footnote2} +\alias{modify_footnote_header} +\alias{modify_footnote_body} +\alias{remove_footnote_header} +\alias{remove_footnote_body} +\title{Modify Footnotes} +\usage{ +modify_footnote_header( + x, + footnote, + columns, + replace = TRUE, + text_interpret = c("md", "html") +) + +modify_footnote_body( + x, + footnote, + columns, + rows, + replace = TRUE, + text_interpret = c("md", "html") +) + +remove_footnote_header(x, columns) + +remove_footnote_body(x, columns, rows) +} +\arguments{ +\item{x}{(\code{gtsummary})\cr +A gtsummary object} + +\item{footnote}{(\code{string})\cr +a string} + +\item{columns}{(\code{\link[dplyr:dplyr_tidy_select]{tidy-select}})\cr +columns to add footnote} + +\item{replace}{(scalar \code{logical})\cr +Logical indicating whether to replace any existing footnotes in the specified +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.} + +\item{text_interpret}{(\code{string})\cr +String indicates whether text will be interpreted with +\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}. +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}.} +} +\value{ +Updated gtsummary object +} +\description{ +Modify Footnotes +} +\examples{ +# TODO: Add examples +} diff --git a/man/modify_source_note.Rd b/man/modify_source_note.Rd index f390007a8..e771a01cf 100644 --- a/man/modify_source_note.Rd +++ b/man/modify_source_note.Rd @@ -18,7 +18,8 @@ A string to add as a source note.} \item{text_interpret}{(\code{string})\cr String indicates whether text will be interpreted with -\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}.} +\code{\link[gt:md]{gt::md()}} or \code{\link[gt:html]{gt::html()}}. Must be \code{"md"} (default) or \code{"html"}. +Applies to tables printed with \code{{gt}}.} \item{source_note_id}{(\code{integers})\cr Integers specifying the ID of the source note to remove. @@ -37,5 +38,11 @@ Source notes are not supported by \code{as_kable_extra()}. } \examples{ \dontshow{if (identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +# Example 1 ---------------------------------- +tbl <- tbl_summary(trial, include = c(marker, grade), missing = "no") |> + modify_source_note("Results as of June 26, 2015") + +# Example 2 ---------------------------------- +remove_source_note(tbl, source_note_id = 1) \dontshow{\}) # examplesIf} } diff --git a/man/modify_table_styling.Rd b/man/modify_table_styling.Rd index 76d5c233a..8f69cdb4e 100644 --- a/man/modify_table_styling.Rd +++ b/man/modify_table_styling.Rd @@ -19,7 +19,7 @@ modify_table_styling( text_format = NULL, undo_text_format = NULL, indent = NULL, - text_interpret = c("md", "html"), + text_interpret = "md", cols_merge_pattern = NULL ) } @@ -78,13 +78,19 @@ use \code{"{conf.low}, {conf.high}"}. The first column listed in the pattern string must match the single column name passed in \verb{columns=}.} } \description{ +This function is for developers. +If you are not a developer, it's recommended that you use the following +functions to make modifications to your table. \code{\link[=modify_header]{modify_header()}}, +\code{\link[=modify_spanning_header]{modify_spanning_header()}}, \verb{[modify_column_hide()]}, \code{\link[=modify_column_unhide]{modify_column_unhide()}}, +\code{\link[=modify_footnote_header]{modify_footnote_header()}}, \code{\link[=modify_footnote_body]{modify_footnote_body()}}, \code{\link[=modify_abbreviation]{modify_abbreviation()}}, +\code{\link[=modify_column_alignment]{modify_column_alignment()}}, \code{\link[=modify_fmt_fun]{modify_fmt_fun()}}, \verb{[modify_column_indent()]}, +\code{\link[=modify_column_merge]{modify_column_merge()}}. + This is a function meant for advanced users to gain more control over the characteristics of the resulting gtsummary table by directly modifying \code{.$table_styling}. -\emph{This function is primarily used in the development of other gtsummary -functions, and very little checking of the passed arguments is performed.} -} -\details{ +\emph{This function has very little checking of the passed arguments.} + Review the \href{https://www.danieldsjoberg.com/gtsummary/articles/gtsummary_definition.html}{gtsummary definition} vignette for information on \code{.$table_styling} objects. diff --git a/man/proportion_summary.Rd b/man/proportion_summary.Rd index 334b05e22..b1238c782 100644 --- a/man/proportion_summary.Rd +++ b/man/proportion_summary.Rd @@ -79,7 +79,7 @@ Titanic |> overall_row_last = TRUE ) |> bold_labels() |> - modify_footnote(all_stat_cols() ~ "Proportion (\%) of survivors (n/N) [95\% CI]") + modify_footnote_header("Proportion (\%) of survivors (n/N) [95\% CI]", columns = all_stat_cols()) } \author{ Joseph Larmarange diff --git a/man/ratio_summary.Rd b/man/ratio_summary.Rd index 1629eb62c..883cfe92d 100644 --- a/man/ratio_summary.Rd +++ b/man/ratio_summary.Rd @@ -54,7 +54,7 @@ trial |> overall_row_label = "All stages & grades" ) |> bold_labels() |> - modify_footnote(all_stat_cols() ~ "Ratio [95\% CI] (n/N)") + modify_footnote_header("Ratio [95\% CI] (n/N)", columns = all_stat_cols()) } \author{ Joseph Larmarange diff --git a/man/rows_argument.Rd b/man/rows_argument.Rd new file mode 100644 index 000000000..4182d2ec5 --- /dev/null +++ b/man/rows_argument.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rows_argument.R +\name{rows_argument} +\alias{rows_argument} +\title{\code{rows} argument} +\description{ +The rows argument accepts a predicate expression that is used to specify +rows to apply formatting. The expression must evaluate to a logical when +evaluated in \code{x$table_body}. For example, to apply formatting to the age rows +pass \code{rows = variable == "age"}. A vector of row numbers is NOT acceptable. + +A couple of things to note when using the \code{rows} argument. +\enumerate{ +\item You can use saved objects to create the predicate argument, e.g. +\code{rows = variable == letters[1]}. +\item The saved object cannot share a name with a column in \code{x$table_body}. +The reason for this is that in \code{tbl_merge()} the columns are renamed, +and the renaming process cannot disambiguate the \code{variable} column from +an external object named \code{variable} in the following expression +\code{rows = .data$variable = .env$variable}. +} +} +\keyword{internal} diff --git a/man/tbl_continuous.Rd b/man/tbl_continuous.Rd index 2b8bcca28..c35703ad4 100644 --- a/man/tbl_continuous.Rd +++ b/man/tbl_continuous.Rd @@ -54,6 +54,7 @@ a gtsummary table Summarize a continuous variable by one or more categorical variables } \examples{ +\dontshow{if (identical(Sys.getenv("NOT_CRAN"), "true") || identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # Example 1 ---------------------------------- tbl_continuous( data = trial, @@ -73,4 +74,5 @@ trial |> value = all_subjects ~ 1, label = list(all_subjects = "All Subjects") ) +\dontshow{\}) # examplesIf} } diff --git a/man/tbl_custom_summary.Rd b/man/tbl_custom_summary.Rd index faadb2829..d57f42c92 100644 --- a/man/tbl_custom_summary.Rd +++ b/man/tbl_custom_summary.Rd @@ -159,7 +159,7 @@ Note that for categorical variables, \code{{N_obs}}, \code{{N_miss}} and \code{{ to the total number, number missing and number non missing observations in the denominator, not at each level of the categorical variable. -It is recommended to use \code{\link[=modify_footnote]{modify_footnote()}} to properly describe the +It is recommended to use \code{\link[=modify_footnote_header]{modify_footnote_header()}} to properly describe the displayed statistics (see examples). } @@ -167,7 +167,7 @@ displayed statistics (see examples). The returned table is compatible with all \code{gtsummary} features applicable -to a \code{tbl_summary} object, like \code{\link[=add_overall]{add_overall()}}, \code{\link[=modify_footnote]{modify_footnote()}} or +to a \code{tbl_summary} object, like \code{\link[=add_overall]{add_overall()}}, \code{\link[=modify_footnote_header]{modify_footnote_header()}} or \code{\link[=bold_labels]{bold_labels()}}. However, some of them could be inappropriate in such case. In particular, @@ -201,8 +201,9 @@ trial |> overall_row_label = "All stages & grades" ) |> add_overall(last = TRUE) |> - modify_footnote( - all_stat_cols() ~ "A: mean age - S: sum of marker" + modify_footnote_header( + footnote = "A: mean age - S: sum of marker", + columns = all_stat_cols() ) |> bold_labels() @@ -225,8 +226,9 @@ trial |> statistic = ~ "{mean} [{conf.low}; {conf.high}]" ) |> add_overall(last = TRUE) |> - modify_footnote( - all_stat_cols() ~ "mean [95\% CI]" + modify_footnote_header( + footnote = "mean [95\% CI]", + columns = all_stat_cols() ) # Example 3 ---------------------------------- diff --git a/man/tbl_regression.Rd b/man/tbl_regression.Rd index 2529d4076..6bd43aa62 100644 --- a/man/tbl_regression.Rd +++ b/man/tbl_regression.Rd @@ -103,7 +103,7 @@ to print the random components. } \examples{ -\dontshow{if (gtsummary:::is_pkg_installed(c("cardx", "broom", "broom.helpers"))) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +\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} # Example 1 ---------------------------------- glm(response ~ age + grade, trial, family = binomial()) |> tbl_regression(exponentiate = TRUE) diff --git a/man/tbl_strata.Rd b/man/tbl_strata.Rd index be462fbda..3381734be 100644 --- a/man/tbl_strata.Rd +++ b/man/tbl_strata.Rd @@ -127,7 +127,7 @@ trial |> ) |> add_ci(pattern = "{stat} ({ci})") |> modify_header(stat_0 = "**Rate (95\% CI)**") |> - modify_footnote(stat_0 = NA), + remove_footnote_header(stat_0), .combine_with = "tbl_stack", .combine_args = list(group_header = NULL) ) |> diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index b195af0ad..5bbafe8d7 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -161,7 +161,9 @@ reference: - subtitle: Style Summary Tables - contents: - modify + - modify_footnote2 - modify_source_note + - modify_abbreviation - modify_caption - bold_italicize_labels_levels - bold_p diff --git a/tests/testthat/_snaps/as_hux_table.md b/tests/testthat/_snaps/as_hux_table.md index 3d8203f73..d6c2efb27 100644 --- a/tests/testthat/_snaps/as_hux_table.md +++ b/tests/testthat/_snaps/as_hux_table.md @@ -3,31 +3,23 @@ Code ht_merge Output - Tumor Response Time to Death - Characte OR 95% CI p-value HR 95% CI p-value - ristic - ──────────────────────────────────────────────────────────────────── - Chemothe - rapy - Treatmen - t - Drug A — — — — - Drug B 1.13 0.60, 0.7 1.30 0.88, 0.2 - 2.13 1.92 - Grade - I — — — — - II 0.85 0.39, 0.7 1.21 0.73, 0.5 - 1.85 1.99 - III 1.01 0.47, >0.9 1.79 1.12, 0.014 - 2.15 2.86 - Age 1.02 1.00, 0.10 1.01 0.99, 0.3 - 1.04 1.02 - ──────────────────────────────────────────────────────────────────── - OR = Odds Ratio, CI = Confidence Interval, HR - = Hazard Ratio + Tumor Response Time to Death + Characteristic OR 95% CI p-value HR 95% CI p-value + ────────────────────────────────────────────────────────────────────────────── + Chemotherapy + Treatment + Drug A — — — — + Drug B 1.13 0.60, 2.13 0.7 1.30 0.88, 1.92 0.2 + Grade + I — — — — + II 0.85 0.39, 1.85 0.7 1.21 0.73, 1.99 0.5 + III 1.01 0.47, 2.15 >0.9 1.79 1.12, 2.86 0.014 + Age 1.02 1.00, 1.04 0.10 1.01 0.99, 1.02 0.3 + ────────────────────────────────────────────────────────────────────────────── + Abbreviations: CI = Confidence Interval, HR = Hazard + Ratio, OR = Odds Ratio - Column names: label, estimate_1, conf.low_1, p.value_1, estimate_2, conf.low_2, - p.value_2 + Column names: label, estimate_1, conf.low_1, p.value_1, estimate_2, conf.low_2, p.value_2 # as_hux_table works with tbl_stack diff --git a/tests/testthat/_snaps/as_kable_extra.md b/tests/testthat/_snaps/as_kable_extra.md index 5e9606581..9e0d1f535 100644 --- a/tests/testthat/_snaps/as_kable_extra.md +++ b/tests/testthat/_snaps/as_kable_extra.md @@ -75,7 +75,7 @@ [9] "\n II \n 0.85 \n 0.39, 1.85 \n 0.7 \n 1.21 \n 0.73, 1.99 \n 0.5 \n " [10] "\n III \n 1.01 \n 0.47, 2.15 \n >0.9 \n 1.79 \n 1.12, 2.86 \n 0.014 \n " [11] "\n Age \n 1.02 \n 1.00, 1.04 \n 0.10 \n 1.01 \n 0.99, 1.02 \n 0.3 \n " - [12] "\n\n1 OR = Odds Ratio, CI = Confidence Interval, HR = Hazard Ratio\n" + [12] "\n\n Abbreviations: CI = Confidence Interval, HR = Hazard Ratio, OR = Odds Ratio\n" # as_kable_extra works with tbl_stack @@ -97,13 +97,13 @@ [13] "\n \n Patient Died \n 60 (59%) \n " [14] "\n\n1 Median (Q1, Q3); n (%)\n" -# as_kable_extra passes table footnotes & footnote abbreviations correctly +# as_kable_extra passes table footnotes & abbreviations correctly "<\/tbody>\n\n\n1<\/sup> n (%); Median (Q1, Q3)<\/td><\/tr>\n\n2<\/sup> test footnote<\/td><\/tr>\n<\/tfoot>\n<\/table>" --- - "<\/tbody>\n\n\n1<\/sup> n (%); Median (Q1, Q3)<\/td><\/tr>\n\n2<\/sup> N = number of observations<\/td><\/tr>\n\n3<\/sup> test footnote<\/td><\/tr>\n<\/tfoot>\n<\/table>" + "<\/tbody>\n\n<\/sup> Abbreviation: N = number of observations<\/td><\/tr><\/tfoot>\n\n\n1<\/sup> n (%); Median (Q1, Q3)<\/td><\/tr>\n\n2<\/sup> test footnote<\/td><\/tr>\n<\/tfoot>\n<\/table>" --- diff --git a/tests/testthat/_snaps/modify_abbreviation.md b/tests/testthat/_snaps/modify_abbreviation.md new file mode 100644 index 000000000..42853909d --- /dev/null +++ b/tests/testthat/_snaps/modify_abbreviation.md @@ -0,0 +1,17 @@ +# remove_abbreviation() messaging + + Code + remove_abbreviation(tbl_summary(trial, include = marker), "Q3 = Third Quartile") + Condition + Error in `remove_abbreviation()`: + ! There are no abbreviations to remove. + +--- + + Code + remove_abbreviation(modify_abbreviation(tbl_summary(trial, include = marker), + "Q1 = First Quartile"), "Q3 = Third Quartile") + Condition + Error in `remove_abbreviation()`: + ! The `abbreviation` argument must be one of "Q1 = First Quartile". + diff --git a/tests/testthat/_snaps/modify_footnote_body.md b/tests/testthat/_snaps/modify_footnote_body.md new file mode 100644 index 000000000..2eb825e93 --- /dev/null +++ b/tests/testthat/_snaps/modify_footnote_body.md @@ -0,0 +1,9 @@ +# modify_footnote_body(rows) messaging + + Code + modify_footnote_body(base_tbl_summary, footnote = "this will not appear", + columns = label, rows = not_a_predicate) + Condition + Error in `modify_footnote_body()`: + ! The `rows` argument must be an expression that evaluates to a logical vector in `x$table_body`. + diff --git a/tests/testthat/_snaps/separate_p_footnotes.md b/tests/testthat/_snaps/separate_p_footnotes.md index fe28646e9..b3c9ba694 100644 --- a/tests/testthat/_snaps/separate_p_footnotes.md +++ b/tests/testthat/_snaps/separate_p_footnotes.md @@ -1,39 +1,26 @@ # separate_p_footnotes() Code - as.data.frame(dplyr::mutate(dplyr::filter(getElement(getElement( - separate_p_footnotes(add_p(tbl, test = list(age = function(data, variable, by, - ...) broom::tidy(t.test(data[[variable]] ~ data[[by]]))))), "table_styling"), - "footnote"), dplyr::row_number() %in% c(dplyr::n(), dplyr::n() - 1L)), rows = map_chr( - rows, ~ expr_deparse(quo_squash(.x))))) + as.data.frame(dplyr::mutate(dplyr::filter(getElement(getElement(separate_p_footnotes(add_p(tbl, test = list(age = function(data, + variable, by, ...) broom::tidy(t.test(data[[variable]] ~ data[[by]]))))), "table_styling"), "footnote_body"), dplyr::row_number() %in% + c(dplyr::n(), dplyr::n() - 1L)), rows = map_chr(rows, ~ expr_deparse(quo_squash(.x))))) Output - column rows - 1 p.value .data$variable %in% "age" & .data$row_type %in% "label" - 2 p.value .data$variable %in% "grade" & .data$row_type %in% "label" - text_interpret footnote - 1 gt::md Welch Two Sample t-test - 2 gt::md Pearson's Chi-squared test + column rows footnote text_interpret replace remove + 1 p.value .data$variable %in% "age" & .data$row_type %in% "label" Welch Two Sample t-test gt::md TRUE FALSE + 2 p.value .data$variable %in% "grade" & .data$row_type %in% "label" Pearson's Chi-squared test gt::md TRUE FALSE --- Code - as.data.frame(dplyr::mutate(dplyr::filter(getElement(getElement( - separate_p_footnotes(add_difference(tbl)), "table_styling"), "footnote"), - dplyr::row_number() %in% seq(dplyr::n(), dplyr::n() - 4L)), rows = map_chr(rows, - ~ expr_deparse(quo_squash(.x))))) + as.data.frame(dplyr::mutate(dplyr::filter(getElement(getElement(separate_p_footnotes(add_difference(tbl)), "table_styling"), + "footnote_body"), dplyr::row_number() %in% seq(dplyr::n(), dplyr::n() - 4L)), rows = map_chr(rows, ~ expr_deparse(quo_squash(.x))))) Output - column rows - 1 estimate .data$variable %in% "age" & .data$row_type %in% "label" - 2 conf.low .data$variable %in% "age" & .data$row_type %in% "label" - 3 p.value .data$variable %in% "age" & .data$row_type %in% "label" - 4 estimate .data$variable %in% "grade" & .data$row_type %in% "label" - 5 conf.low .data$variable %in% "grade" & .data$row_type %in% "label" - text_interpret footnote - 1 gt::md Welch Two Sample t-test - 2 gt::md Welch Two Sample t-test - 3 gt::md Welch Two Sample t-test - 4 gt::md Standardized Mean Difference - 5 gt::md Standardized Mean Difference + column rows footnote text_interpret replace remove + 1 estimate .data$variable %in% "age" & .data$row_type %in% "label" Welch Two Sample t-test gt::md TRUE FALSE + 2 conf.low .data$variable %in% "age" & .data$row_type %in% "label" Welch Two Sample t-test gt::md TRUE FALSE + 3 p.value .data$variable %in% "age" & .data$row_type %in% "label" Welch Two Sample t-test gt::md TRUE FALSE + 4 estimate .data$variable %in% "grade" & .data$row_type %in% "label" Standardized Mean Difference gt::md TRUE FALSE + 5 conf.low .data$variable %in% "grade" & .data$row_type %in% "label" Standardized Mean Difference gt::md TRUE FALSE # separate_p_footnotes() messaging diff --git a/tests/testthat/test-as_flex_table.R b/tests/testthat/test-as_flex_table.R index 083a1bdec..6fe690262 100644 --- a/tests/testthat/test-as_flex_table.R +++ b/tests/testthat/test-as_flex_table.R @@ -222,9 +222,9 @@ test_that("as_flex_table passes table column alignment correctly", { ) }) -test_that("as_flex_table passes table footnotes & footnote abbreviations correctly", { +test_that("as_flex_table passes table footnotes & abbreviations correctly", { tbl_fn <- my_tbl_summary |> - modify_table_styling(columns = label, footnote = "test footnote", rows = variable == "age") + modify_footnote_body(columns = label, footnote = "test footnote", rows = variable == "age") ft_tbl_fn <- tbl_fn |> as_flex_table() # footnote @@ -234,12 +234,16 @@ test_that("as_flex_table passes table footnotes & footnote abbreviations correct expect_equal(nrow(ft_tbl_fn$footer$content$data), 2) # correct number of footnotes expect_equal(c(fn1[1], fn2[1]), c("1", "2")) # correct ordering expect_equal( - tbl_fn$table_styling$footnote$footnote, # correct labels - c(fn1[2], fn2[2]) + tbl_fn$table_styling$footnote_header$footnote, # correct labels + fn1[2] + ) + expect_equal( + tbl_fn$table_styling$footnote_body$footnote, # correct labels + fn2[2] ) tbl_fa <- tbl_fn |> - modify_footnote(stat_0 = "N = number of observations", abbreviation = TRUE) + modify_abbreviation("N = number of observations") ft_tbl_fa <- tbl_fa |> as_flex_table() # footnote_abbrev @@ -248,18 +252,12 @@ test_that("as_flex_table passes table footnotes & footnote abbreviations correct fn3 <- ft_tbl_fa$footer$content$data[3, ]$label$txt expect_equal(nrow(ft_tbl_fa$footer$content$data), 3) # correct number of footnotes - expect_equal(c(fn1[1], fn2[1], fn3[1]), c("1", "2", "3")) # correct ordering - expect_equal( - c("n (%); Median (Q1, Q3)", "N = number of observations", "test footnote"), # correct labels - c(fn1[2], fn2[2], fn3[2]) - ) + expect_equal(c(fn1[1], fn2[1], fn3[1]), c("1", "2", "Abbreviation: N = number of observations")) # correct ordering and label for abbreviation is correct # customized footnotes tbl <- my_tbl_summary |> - modify_footnote( - all_stat_cols() ~ "replace old footnote", - label = "another new footnote" - ) + modify_footnote_header("replace old footnote", columns = all_stat_cols()) |> + modify_footnote_header("another new footnote", columns = label) ft_tbl <- tbl |> as_flex_table() fn1 <- ft_tbl$footer$content$data[1, ]$label$txt @@ -276,8 +274,8 @@ test_that("as_flex_table passes table footnotes & footnote abbreviations correct test_that("as_flex_table passes multiple table footnotes correctly", { # testing one footnote passed to multiple columns and rows, addresses issue #2062 out <- my_tbl_summary |> - modify_footnote(stat_0 = NA) |> - modify_table_styling( + remove_footnote_header(stat_0) |> + modify_footnote_body( columns = c(label, stat_0), rows = (variable %in% "trt") & (row_type == "level"), footnote = "my footnote" @@ -312,17 +310,17 @@ test_that("as_flex_table passes multiple table footnotes correctly", { by = trt, include = grade ) |> - modify_table_styling( + modify_footnote_body( columns = stat_1, rows = (variable %in% "grade") & (row_type == "level"), footnote = "my footnote" ) |> - modify_table_styling( + modify_footnote_body( columns = label, rows = label == "grade", footnote = "my footnote" ) |> - modify_table_styling( + modify_footnote_body( columns = label, rows = label == "I", footnote = "my footnote" diff --git a/tests/testthat/test-as_gt.R b/tests/testthat/test-as_gt.R index 737eb791f..91b12f6d7 100644 --- a/tests/testthat/test-as_gt.R +++ b/tests/testthat/test-as_gt.R @@ -213,48 +213,44 @@ test_that("as_gt passes table text interpreters correctly", { expect_true(attr(gt_tbl$`_spanners`$spanner_label[[1]], "html")) }) -test_that("as_gt passes table footnotes & footnote abbreviations correctly", { +test_that("as_gt passes table footnotes & abbreviations correctly", { tbl_fn <- my_tbl_summary |> - modify_table_styling(columns = label, footnote = "test footnote", rows = variable == "age") + modify_footnote_body(footnote = "test footnote", columns = label,rows = variable == "age") gt_tbl_fn <- tbl_fn |> as_gt() # footnote expect_equal( - tbl_fn$table_styling$footnote$column, + tbl_fn$table_styling$footnote_header$column |> + append(tbl_fn$table_styling$footnote_body$column) |> + unique(), gt_tbl_fn$`_footnotes`$colname |> unique() ) expect_equal( - tbl_fn$table_styling$footnote$footnote, - gt_tbl_fn$`_footnotes`$footnotes |> unlist() |> unique() + tbl_fn$table_styling$footnote_header$footnote, + gt_tbl_fn$`_footnotes`$footnotes[[1]], + ignore_attr = TRUE + ) + expect_equal( + tbl_fn$table_styling$footnote_body$footnote, + gt_tbl_fn$`_footnotes`$footnotes[-1] |> unlist() |> unique(), + ignore_attr = TRUE ) tbl_fa <- tbl_fn |> - modify_footnote(stat_0 = "N = number of observations", abbreviation = TRUE) + modify_abbreviation("N = number of observations") gt_tbl_fa <- tbl_fa |> as_gt() - # footnote_abbrev + # abbreviation expect_equal( - gt_tbl_fa$`_footnotes` |> - dplyr::distinct(pick(!any_of("rownum"))) |> - dplyr::arrange(locnum) |> - dplyr::pull(colname), - c("stat_0", "stat_0", "label") - ) - expect_equal( - gt_tbl_fa$`_footnotes` |> - dplyr::distinct(pick(!any_of("rownum"))) |> - dplyr::arrange(locnum) |> - dplyr::pull(footnotes) |> + gt_tbl_fa$`_source_notes` |> unlist(), - c("n (%); Median (Q1, Q3)", "N = number of observations", "test footnote") + "Abbreviation: N = number of observations" ) # customized footnotes tbl <- my_tbl_summary |> - modify_footnote( - all_stat_cols() ~ "replace old footnote", - label = "another new footnote" - ) + modify_footnote_header("replace old footnote", columns = all_stat_cols()) |> + modify_footnote_header("another new footnote", columns = label) gt_tbl <- tbl |> as_gt() expect_equal( @@ -269,8 +265,8 @@ test_that("as_gt passes table footnotes & footnote abbreviations correctly", { # footnotes in the body of the table expect_equal( tbl_summary(trial, include = "age") |> - modify_table_styling(columns = label, rows = TRUE, footnote = "my footnote") |> - modify_table_styling(columns = stat_0, rows = row_type == "label", footnote = "my footnote") |> + modify_footnote_body(columns = label, rows = TRUE, footnote = "my footnote") |> + modify_footnote_body(columns = stat_0, rows = row_type == "label", footnote = "my footnote") |> as_gt() |> getElement("_footnotes") |> dplyr::filter(footnotes == "my footnote") |> @@ -318,7 +314,7 @@ test_that("as_gt passes appended glance statistics correctly", { ) expect_equal( tbl$table_styling$source_note$source_note, - gt_tbl$`_source_notes`[[1]], + gt_tbl$`_source_notes`[[2]], ignore_attr = "class" ) expect_equal( diff --git a/tests/testthat/test-as_hux_table.R b/tests/testthat/test-as_hux_table.R index 4e9014b80..8e94e9da5 100644 --- a/tests/testthat/test-as_hux_table.R +++ b/tests/testthat/test-as_hux_table.R @@ -1,5 +1,5 @@ skip_on_cran() -skip_if_not(is_pkg_installed("huxtable")) +skip_if_not(is_pkg_installed(c("huxtable", "withr"))) my_tbl_summary <- trial |> select(trt, age, death) |> @@ -25,13 +25,15 @@ test_that("as_hux_table(return_calls) works as expected", { # correct elements are returned expect_equal( names(ht), - c("tibble", "fmt", "cols_merge", "cols_hide", "huxtable", "set_left_padding", "add_footnote", "source_note", - "set_bold", "set_italic", "fmt_missing", "insert_row", "set_markdown", "align", "set_number_format") + c("tibble", "fmt", "cols_merge", "cols_hide", "huxtable", "set_left_padding", + "add_footnote", "abbreviations", "source_note", "set_bold", "set_italic", + "fmt_missing", "insert_row", "set_markdown", "align", "set_number_format") ) }) test_that("as_hux_table works with tbl_merge", { skip_if_not(is_pkg_installed("survival")) + withr::local_options(list(width = 120)) t1 <- glm(response ~ trt + grade + age, trial, family = binomial) |> tbl_regression(exponentiate = TRUE) @@ -108,9 +110,9 @@ test_that("as_hux_table passes table column alignment correctly", { ) }) -test_that("as_hux_table passes table footnotes & footnote abbreviations correctly", { +test_that("as_hux_table passes table footnotes & abbreviations correctly", { tbl_fn <- my_tbl_summary |> - modify_table_styling(columns = label, footnote = "test footnote", rows = variable == "age") + modify_footnote_body(columns = label, footnote = "test footnote", rows = variable == "age") ht_fn <- tbl_fn |> as_hux_table() # footnote @@ -124,21 +126,20 @@ test_that("as_hux_table passes table footnotes & footnote abbreviations correctl ) tbl_fa <- tbl_fn |> - modify_footnote(stat_0 = "N = number of observations", abbreviation = TRUE) + modify_abbreviation("N = number of observations") ht_fa <- tbl_fa |> as_hux_table() # footnote_abbrev expect_equal( - ht_fa[9, 1] |> unlist(use.names = FALSE), - "N = number of observations" + ht_fa[10, 1] |> unlist(use.names = FALSE), + "Abbreviation: N = number of observations" ) # customized footnotes tbl <- my_tbl_summary |> - modify_footnote( - all_stat_cols() ~ "replace old footnote", - label = "another new footnote" - ) + modify_footnote_header("replace old footnote", columns = all_stat_cols()) |> + modify_footnote_header("another new footnote", columns = label) + ht <- tbl |> as_hux_table() expect_equal( @@ -162,7 +163,7 @@ test_that("as_hux_table passes appended glance statistics correctly", { # correct row ordering expect_equal( ht$label, - c("**Characteristic**", "Age", "R²", "BIC", "CI = Confidence Interval", "R² = 0.000; BIC = 471") + c("**Characteristic**", "Age", "R²", "BIC", "Abbreviation: CI = Confidence Interval", "R² = 0.000; BIC = 471") ) }) diff --git a/tests/testthat/test-as_kable_extra.R b/tests/testthat/test-as_kable_extra.R index 9555eda0b..41857baaa 100644 --- a/tests/testthat/test-as_kable_extra.R +++ b/tests/testthat/test-as_kable_extra.R @@ -28,7 +28,7 @@ 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", "footnote") + "escape_table_body", "bold_italic", "kable", "add_indent", "source_note", "abbreviations", "footnote") ) }) @@ -167,9 +167,9 @@ test_that("as_kable_extra passes table column alignment correctly", { ) }) -test_that("as_kable_extra passes table footnotes & footnote abbreviations correctly", { +test_that("as_kable_extra passes table footnotes & abbreviations correctly", { tbl_fn <- my_tbl_summary |> - modify_table_styling(columns = label, footnote = "test footnote", rows = variable == "age") + modify_footnote_body(columns = label, footnote = "test footnote", rows = variable == "age") kbl_fn <- tbl_fn |> as_kable_extra() # footnote @@ -178,7 +178,7 @@ test_that("as_kable_extra passes table footnotes & footnote abbreviations correc ) tbl_fa <- tbl_fn |> - modify_footnote(stat_0 = "N = number of observations", abbreviation = TRUE) + modify_abbreviation("N = number of observations") kbl_fa <- tbl_fa |> as_kable_extra() # footnote_abbrev @@ -188,10 +188,8 @@ test_that("as_kable_extra passes table footnotes & footnote abbreviations correc # customized footnotes tbl <- my_tbl_summary |> - modify_footnote( - all_stat_cols() ~ "replace old footnote", - label = "another new footnote" - ) + modify_footnote_header("replace old footnote", columns = all_stat_cols()) |> + modify_footnote_header("another new footnote", columns = label) kbl <- tbl |> as_kable_extra() expect_snapshot_value( diff --git a/tests/testthat/test-modify_abbreviation.R b/tests/testthat/test-modify_abbreviation.R new file mode 100644 index 000000000..a3f528fa7 --- /dev/null +++ b/tests/testthat/test-modify_abbreviation.R @@ -0,0 +1,52 @@ +skip_on_cran() +skip_if_not(is_pkg_installed(c("cardx", "broom", "broom.helpers"))) + +test_that("modify_abbreviation()", { + expect_silent( + tbl <- + tbl_summary(trial, include = marker) |> + modify_abbreviation("Q1 = First Quartile") |> + modify_abbreviation("Q3 = Third Quartile") + ) + expect_equal( + tbl$table_styling$abbreviation, + dplyr::tribble( + ~column, ~abbreviation, ~text_interpret, + NA_character_, "Q1 = First Quartile", "gt::md", + NA_character_, "Q3 = Third Quartile", "gt::md" + ) + ) +}) + + +test_that("remove_abbreviation()", { + expect_silent( + tbl <- + tbl_summary(trial, include = marker) |> + modify_abbreviation("Q1 = First Quartile") |> + modify_abbreviation("Q3 = Third Quartile") |> + remove_abbreviation("Q3 = Third Quartile") + ) + expect_equal( + tbl$table_styling$abbreviation, + dplyr::tribble( + ~column, ~abbreviation, ~text_interpret, + NA_character_, "Q1 = First Quartile", "gt::md", + ) + ) +}) + +test_that("remove_abbreviation() messaging", { + expect_snapshot( + error = TRUE, + tbl_summary(trial, include = marker) |> + remove_abbreviation("Q3 = Third Quartile") + ) + + expect_snapshot( + error = TRUE, + tbl_summary(trial, include = marker) |> + modify_abbreviation("Q1 = First Quartile") |> + remove_abbreviation("Q3 = Third Quartile") + ) +}) diff --git a/tests/testthat/test-modify_footnote.R b/tests/testthat/test-modify_footnote.R index 0ae75c8c9..77f6cf7d1 100644 --- a/tests/testthat/test-modify_footnote.R +++ b/tests/testthat/test-modify_footnote.R @@ -33,7 +33,7 @@ test_that("modify_footnote(...) works", { tbl |> modify_footnote(label = "Variable") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::filter(column %in% "label") |> dplyr::pull("footnote"), "Variable", @@ -44,7 +44,7 @@ test_that("modify_footnote(...) works", { tbl |> modify_footnote(label = "Variable", stat_0 = "Overall") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column %in% c("label", "stat_0")) |> dplyr::pull("footnote"), @@ -56,10 +56,10 @@ test_that("modify_footnote(...) works", { tbl |> modify_footnote(label = "Variable", stat_0 = "Overall", abbreviation = TRUE) |> getElement("table_styling") |> - getElement("footnote_abbrev") |> + getElement("abbreviation") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column %in% c("label", "stat_0")) |> - dplyr::pull("footnote"), + dplyr::pull("abbreviation"), c("Variable", "Overall"), ignore_attr = TRUE ) @@ -73,7 +73,7 @@ test_that("modify_footnote(...) dynamic headers work with `tbl_summary()`", { tbl |> modify_footnote(!!!list(label = "Variable", stat_0 = "Overall")) |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column %in% c("label", "stat_0")) |> dplyr::pull("footnote"), @@ -86,7 +86,7 @@ test_that("modify_footnote(...) dynamic headers work with `tbl_summary()`", { tbl |> modify_footnote(stat_0 = "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column %in% "stat_0") |> dplyr::pull("footnote"), @@ -98,7 +98,7 @@ test_that("modify_footnote(...) dynamic headers work with `tbl_summary()`", { tbl_summary(trial, by = trt, include = marker) |> modify_footnote(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(startsWith(column, "stat_")) |> dplyr::pull("footnote"), @@ -112,7 +112,7 @@ test_that("modify_footnote(...) dynamic headers work with `tbl_summary()`", { add_overall() |> modify_footnote(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(startsWith(column, "stat_")) |> dplyr::pull("footnote"), @@ -134,7 +134,7 @@ test_that("modify_footnote(text_interpret) works", { tbl_summary(trial, include = marker) |> modify_footnote(label = "Variable", text_interpret = "html") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column %in% "label") |> dplyr::pull(text_interpret), @@ -147,7 +147,7 @@ test_that("modify_footnote() with tbl_svysummary()", { tbl_summary(trial, include = marker) |> modify_footnote(label = "Variable", text_interpret = "html") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column %in% "label") |> dplyr::pull(text_interpret), @@ -164,7 +164,7 @@ test_that("modify_footnote() works with tbl_svysummary()", { add_overall() |> modify_footnote(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(startsWith(column, "stat_")) |> dplyr::pull("footnote"), @@ -175,18 +175,20 @@ test_that("modify_footnote() works with tbl_svysummary()", { }) test_that("modify_footnote() works with tbl_continuous()", { - expect_equal(tbl_continuous(data = trial, variable = age, by = trt, include = grade)|> - add_overall() |> - modify_footnote(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> - getElement("table_styling") |> - getElement("footnote") |> - dplyr::slice_tail(by = "column", n = 1) |> - dplyr::filter(startsWith(column, "stat_")) |> - dplyr::pull("footnote"), - c("Drug A | N = 200 | n = 98 | p = 49%", - "Drug B | N = 200 | n = 102 | p = 51%", - "Overall | N = 200 | n = 200 | p = 100%"), - ignore_attr = TRUE) + expect_equal( + tbl_continuous(data = trial, variable = age, by = trt, include = grade) |> + add_overall() |> + modify_footnote(all_stat_cols() ~ "{level} | N = {N} | n = {n} | p = {style_percent(p)}%") |> + getElement("table_styling") |> + getElement("footnote_header") |> + dplyr::slice_tail(by = "column", n = 1) |> + dplyr::filter(startsWith(column, "stat_")) |> + dplyr::pull("footnote"), + c("Drug A | N = 200 | n = 98 | p = 49%", + "Drug B | N = 200 | n = 102 | p = 51%", + "Overall | N = 200 | n = 200 | p = 100%"), + ignore_attr = TRUE + ) }) @@ -194,7 +196,7 @@ test_that("modify_footnote() works with tbl_cross()", { expect_equal(tbl_cross(data = trial, row = trt, col = response) |> modify_footnote(stat_0 = "Total Response") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column == "stat_0") |> dplyr::pull("footnote"), @@ -209,7 +211,7 @@ test_that("modify_footnote() works with tbl_regression()", { tbl_regression(exponentiate = TRUE) |> modify_footnote(estimate = "Estimate") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column == "estimate") |> dplyr::pull("footnote"), @@ -222,7 +224,7 @@ test_that("modify_footnote() works with tbl_uvregression()", { exponentiate = TRUE, include = c("age", "grade")) |> modify_footnote(estimate = "Estimate") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::slice_tail(by = "column", n = 1) |> dplyr::filter(column == "estimate") |> dplyr::pull("footnote"), diff --git a/tests/testthat/test-modify_footnote_body.R b/tests/testthat/test-modify_footnote_body.R new file mode 100644 index 000000000..1f18e9465 --- /dev/null +++ b/tests/testthat/test-modify_footnote_body.R @@ -0,0 +1,68 @@ +skip_on_cran() + +base_tbl_summary <- + tbl_summary(trial, include = marker) |> + remove_footnote_header(columns = everything()) + +test_that("modify_footnote_body(footnote)", { + # test we can easily replace an existing header + expect_silent( + tbl <- base_tbl_summary |> + modify_footnote_body( + footnote = "this will not appear", + columns = label, + rows = row_type == "label" + ) |> + modify_footnote_body( + footnote = "this _will_ appear; N = {N}", + columns = label, + rows = row_type == "label" + ) + ) + expect_equal( + tbl$table_styling$footnote_body, + dplyr::tribble( + ~column, ~rows, ~footnote, ~text_interpret, ~replace, ~remove, + "label", ~row_type == "label", "this will not appear", "gt::md", TRUE, FALSE, + "label", ~row_type == "label", "this _will_ appear; N = 200", "gt::md", TRUE, FALSE + ), + ignore_attr = TRUE + ) +}) + +test_that("modify_footnote_body(rows) messaging", { + expect_snapshot( + error = TRUE, + base_tbl_summary |> + modify_footnote_body( + footnote = "this will not appear", + columns = label, + rows = not_a_predicate + ) + ) +}) + +test_that("remove_footnote_body(footnote)", { + # test we can remove footnotes from the cells + expect_silent( + tbl <- base_tbl_summary |> + modify_footnote_body( + footnote = "this will not appear", + columns = label, + rows = row_type == "label" + ) |> + remove_footnote_body( + columns = label, + rows = row_type == "label" + ) + ) + expect_equal( + tbl$table_styling$footnote_body, + dplyr::tribble( + ~column, ~rows, ~footnote, ~text_interpret, ~replace, ~remove, + "label", ~row_type == "label", "this will not appear", "gt::md", TRUE, FALSE, + "label", ~row_type == "label", NA, "gt::md", TRUE, TRUE + ), + ignore_attr = TRUE + ) +}) diff --git a/tests/testthat/test-modify_footnote_header.R b/tests/testthat/test-modify_footnote_header.R new file mode 100644 index 000000000..2d0e2acfa --- /dev/null +++ b/tests/testthat/test-modify_footnote_header.R @@ -0,0 +1,62 @@ +skip_on_cran() + +base_tbl_summary <- tbl_summary(trial, include = marker) +test_that("modify_footnote_header(footnote)", { + # test we can easily replace an existing header footnote + expect_equal(base_tbl_summary$table_styling$footnote_header$footnote, "Median (Q1, Q3)") + expect_silent( + tbl <- base_tbl_summary |> + modify_footnote_header( + footnote = "testing N={N}; n={n}; p={p}", + columns = all_stat_cols() + ) + ) + expect_equal( + tbl$table_styling$footnote_header, + dplyr::tribble( + ~column, ~footnote, ~text_interpret, ~replace, ~remove, + "stat_0", "Median (Q1, Q3)", "gt::md", TRUE, FALSE, + "stat_0", "testing N=200; n=200; p=1", "gt::md", TRUE, FALSE + ) + ) + + # test that two footnotes can be placed in the same header + expect_silent( + tbl <- base_tbl_summary |> + modify_footnote_header( + footnote = "testing N={N}; n={n}; p={p}", + columns = all_stat_cols(), + replace = FALSE + ) + ) + expect_equal( + tbl$table_styling$footnote_header, + dplyr::tribble( + ~column, ~footnote, ~text_interpret, ~replace, ~remove, + "stat_0", "Median (Q1, Q3)", "gt::md", TRUE, FALSE, + "stat_0", "testing N=200; n=200; p=1", "gt::md", FALSE, FALSE + ) + ) +}) + +test_that("remove_footnote_header(footnote)", { + # test we can remove footnotes from the headers + expect_silent( + tbl <- base_tbl_summary |> + modify_footnote_header( + footnote = "testing", + columns = all_stat_cols(), + replace = FALSE + ) |> + remove_footnote_header(columns = all_stat_cols()) + ) + expect_equal( + tbl$table_styling$footnote_header, + dplyr::tribble( + ~column, ~footnote, ~text_interpret, ~replace, ~remove, + "stat_0", "Median (Q1, Q3)", "gt::md", TRUE, FALSE, + "stat_0", "testing", "gt::md", FALSE, FALSE, + "stat_0", NA, "gt::md", TRUE, TRUE + ) + ) +}) diff --git a/tests/testthat/test-modify_table_styling.R b/tests/testthat/test-modify_table_styling.R index af591143e..9757b3830 100644 --- a/tests/testthat/test-modify_table_styling.R +++ b/tests/testthat/test-modify_table_styling.R @@ -152,7 +152,7 @@ test_that("modify_table_styling(footnote)", { footnote = "testing footnote" ) |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::filter(column == "stat_0") |> dplyr::slice_tail(n = 1L) |> dplyr::pull(footnote), @@ -169,10 +169,8 @@ test_that("modify_table_styling(footnote_abbrev)", { footnote_abbrev = "testing footnote_abbrev" ) |> getElement("table_styling") |> - getElement("footnote_abbrev") |> - dplyr::filter(column == "stat_0") |> - dplyr::slice_tail(n = 1L) |> - dplyr::pull(footnote), + getElement("abbreviation") |> + dplyr::pull(abbreviation), "testing footnote_abbrev" ) }) diff --git a/tests/testthat/test-separate_p_footnotes.R b/tests/testthat/test-separate_p_footnotes.R index 7072e8f3d..ecb63ed1f 100644 --- a/tests/testthat/test-separate_p_footnotes.R +++ b/tests/testthat/test-separate_p_footnotes.R @@ -1,7 +1,8 @@ skip_on_cran() -skip_if_not(is_pkg_installed(c("cardx", "broom", "smd"))) +skip_if_not(is_pkg_installed(c("cardx", "broom", "smd", "withr"))) test_that("separate_p_footnotes()", { + withr::local_options(list(width = 130)) tbl <- trial |> tbl_summary( by = trt, @@ -13,7 +14,7 @@ test_that("separate_p_footnotes()", { add_p(tbl, test = list(age = \(data, variable, by, ...) t.test(data[[variable]] ~ data[[by]]) |> broom::tidy())) |> separate_p_footnotes() |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_body") |> dplyr::filter(dplyr::row_number() %in% c(dplyr::n(), dplyr::n() - 1L)) |> dplyr::mutate(rows = map_chr(rows, ~quo_squash(.x) |> expr_deparse())) |> as.data.frame() @@ -23,7 +24,7 @@ test_that("separate_p_footnotes()", { add_difference(tbl) |> separate_p_footnotes() |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_body") |> dplyr::filter(dplyr::row_number() %in% seq(dplyr::n(), dplyr::n() - 4L)) |> dplyr::mutate(rows = map_chr(rows, ~quo_squash(.x) |> expr_deparse())) |> as.data.frame() diff --git a/tests/testthat/test-tbl_custom_summary.R b/tests/testthat/test-tbl_custom_summary.R index e7048cc00..530f3c8c8 100644 --- a/tests/testthat/test-tbl_custom_summary.R +++ b/tests/testthat/test-tbl_custom_summary.R @@ -16,7 +16,7 @@ test_that("tbl_custom_summary() basics", { digits = ~1 ) |> add_overall(last = TRUE) |> - modify_footnote(all_stat_cols() ~ "Mean age") |> + modify_footnote_header("Mean age", columns = all_stat_cols()) |> modify_column_unhide(everything()) |> as.data.frame(col_labels = FALSE), NA diff --git a/tests/testthat/test-theme_gtsummary.R b/tests/testthat/test-theme_gtsummary.R index 6ff9aedaf..f5e1d0fec 100644 --- a/tests/testthat/test-theme_gtsummary.R +++ b/tests/testthat/test-theme_gtsummary.R @@ -132,7 +132,7 @@ test_that("theme_gtsummary_journal('lancet') works", { expr = trial |> tbl_summary(by = trt, include = marker, label = marker ~ "marker", missing = "no") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> dplyr::filter(.by = "column", dplyr::n() == dplyr::row_number(), column %in% c("stat_1", "stat_2")) |> dplyr::pull("footnote") |> unique() @@ -174,7 +174,7 @@ test_that("theme_gtsummary_journal('nejm') works", { expr = trial |> tbl_summary(by = trt, include = age, label = age ~ "Age", missing = "no") |> getElement("table_styling") |> - getElement("footnote") |> + getElement("footnote_header") |> getElement("footnote") |> dplyr::last() ), @@ -215,7 +215,7 @@ test_that("theme_gtsummary_journal('jama') works", { # check that we get # - pvalues are rounded to 2 places # - CI separator is " to " - # - estimate and CI are in the same cell with approriate header + # - estimate and CI are in the same cell with appropriate header expect_snapshot( with_gtsummary_theme( theme_gtsummary_journal("jama"), diff --git a/vignettes/articles/gallery.Rmd b/vignettes/articles/gallery.Rmd index e1939f244..e24955e5d 100644 --- a/vignettes/articles/gallery.Rmd +++ b/vignettes/articles/gallery.Rmd @@ -275,7 +275,7 @@ trial |> missing = "no" ) |> modify_header(stat_0 = "**Mean (SD)**") |> - modify_footnote(stat_0 = NA) |> + remove_footnote_header(stat_0) |> add_ci() ``` @@ -341,7 +341,7 @@ gt_t1 <- trial |> tbl_summary(include = c(trt, grade), missing = "no") |> add_n() |> modify_header(stat_0 = "**n (%)**") |> - modify_footnote(stat_0 = NA_character_) + remove_footnote_header(stat_0) theme_gtsummary_compact() tbl_merge( @@ -387,7 +387,7 @@ trial |> ) |> modify_header(label = "**Model Outcome**", estimate = "**Treatment Coef.**") |> - modify_footnote(estimate = "Values larger than 0 indicate larger values in the Drug B group.") + modify_footnote_header("Values larger than 0 indicate larger values in the Drug B group.", columns = estimate) ```
diff --git a/vignettes/articles/tbl_regression.Rmd b/vignettes/articles/tbl_regression.Rmd index 3538acad9..ba70a176d 100644 --- a/vignettes/articles/tbl_regression.Rmd +++ b/vignettes/articles/tbl_regression.Rmd @@ -165,7 +165,8 @@ The {gtsummary} package comes with functions specifically made to modify and for dplyr::tribble( ~Function, ~Description, "`modify_header()`", "update column headers", - "`modify_footnote()`", "update column footnote", + "`modify_footnote_header()`", "update column header footnote", + "`modify_footnote_body()`", "update table body footnote", "`modify_spanning_header()`", "update spanning headers", "`modify_caption()`", "update table caption/title", "`bold_labels()`", "bold variable labels", diff --git a/vignettes/articles/tbl_summary.Rmd b/vignettes/articles/tbl_summary.Rmd index 915b4df29..b6762258c 100644 --- a/vignettes/articles/tbl_summary.Rmd +++ b/vignettes/articles/tbl_summary.Rmd @@ -214,7 +214,8 @@ The {gtsummary} package comes with functions specifically made to modify and for dplyr::tribble( ~Function, ~Description, "`modify_header()`", "update column headers", - "`modify_footnote()`", "update column footnote", + "`modify_footnote_header()`", "update column header footnote", + "`modify_footnote_body()`", "update table body footnote", "`modify_spanning_header()`", "update spanning headers", "`modify_caption()`", "update table caption/title", "`bold_labels()`", "bold variable labels", @@ -238,9 +239,7 @@ trial2 |> add_n() |> modify_header(label ~ "**Variable**") |> modify_spanning_header(c("stat_1", "stat_2") ~ "**Treatment Received**") |> - modify_footnote( - all_stat_cols() ~ "Median (IQR) or Frequency (%)" - ) |> + modify_footnote_header("Median (IQR) or Frequency (%)", columns = all_stat_cols()) |> modify_caption("**Table 1. Patient Characteristics**") |> bold_labels() ``` diff --git a/vignettes/gtsummary_definition.Rmd b/vignettes/gtsummary_definition.Rmd index 7a59048ab..5a7443a6e 100644 --- a/vignettes/gtsummary_definition.Rmd +++ b/vignettes/gtsummary_definition.Rmd @@ -27,14 +27,13 @@ Here, we review those characteristics, and provide instructions on how to constr library(gtsummary) tbl_regression_ex <- - lm(age ~ grade + marker, trial) %>% - tbl_regression() %>% + lm(age ~ grade + marker, trial) |> + tbl_regression() |> bold_p(t = 0.5) tbl_summary_ex <- - trial %>% - select(trt, age, grade, response) %>% - tbl_summary(by = trt) + trial |> + tbl_summary(by = trt, include = c(trt, age, grade, response)) ``` ## Structure of a {gtsummary} object @@ -58,7 +57,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`, `footnote_abbrev`, `fmt_fun`, `text_format`, `fmt_missing`, `cols_merge` and the following objects `source_note`, `caption`, `horizontal_line_above`. +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`. **`header`** @@ -91,19 +90,98 @@ dplyr::tribble( ) ``` -**`footnote` & `footnote_abbrev`** +**`footnote_header`** -Each {gtsummary} table may contain a single footnote per header and cell within the table. -Footnotes and footnote abbreviations are handled separately. +Each {gtsummary} table may contain footnotes in the column headers. +Updates/changes to footnote are appended to the bottom of the tibble. + +```{r, echo=FALSE} +dplyr::tribble( + ~Column, ~Description, + "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) + ) +``` + +**`footnote_body`** + +Each {gtsummary} table may include footnotes in the body of the table. Updates/changes to footnote are appended to the bottom of the tibble. -A footnote of `NA_character_` deletes an existing footnote. ```{r, echo=FALSE} dplyr::tribble( ~Column, ~Description, "column", "Column name from `.$table_body`", "rows", "expression selecting rows in `.$table_body`, `NA` indicates to add footnote to header", - "footnote", "string containing footnote to add to column/row" + "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. + +```{r, echo=FALSE} +dplyr::tribble( + ~Column, ~Description, + "column", "Optional column name from `.$table_body`. When present, the abbreviation is only printed when the column appears in the rendered table", + "abbreviation", "string containing the abbreviation to add", + "text_interpret", "the {gt} function that is used to interpret the source note, `gt::md()` or `gt::html()`", +) %>% + 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) + ) +``` + +**`source_note`** + +A tibble with the source notes to include. +Each source note is assigned an ID based on the order it is added to the table. +Source notes are added one at a time and present much like footnotes, but are not linked to a header or cell in the table. + +```{r, echo=FALSE} +dplyr::tribble( + ~Column, ~Description, + "id", "Integer idenitfying the source note", + "source_note", "string containing the abbreviation to add", + "text_interpret", "the {gt} function that is used to interpret the source note, `gt::md()` or `gt::html()`", + "remove", "logical indicating whether the source note should be included or removed from final table" ) %>% gt::gt() %>% gt::fmt_markdown(columns = everything()) %>% @@ -224,31 +302,6 @@ dplyr::tribble( ) ``` -**`source_note`** - -A tibble with the source notes to include. -Each source note is assigned an ID based on the order it is added to the table. - -```{r, echo=FALSE} -dplyr::tribble( - ~Column, ~Description, - "id", "ID of the source note", - "sounrce_note", "string containing the source note", - "text_interpret", "the {gt} function that is used to interpret the source note, `gt::md()` or `gt::html()`", - "remove", "logical indicating whether the source note should be removed from the table" -) %>% - 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) - ) -``` **`caption`** String that is made into the table caption.