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] "