From c11fa2a2fdaf73ddd210f6be8a65fae519c662fc Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 14:05:52 -0700 Subject: [PATCH 01/11] various fixes cherrypicked --- .pre-commit-config.yaml | 5 +- R/tree_accessors.R | 4 + R/tt_pos_and_access.R | 8 + inst/WORDLIST | 1 + .../guided_advanced_afuns_spl_context.Rmd | 264 ++++++++++++++++++ 5 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 vignettes/guided_advanced_afuns_spl_context.Rmd diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7253d67137..149cf22eca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,11 +6,11 @@ default_language_version: python: python3 repos: - repo: https://github.com/lorenzwalthert/precommit - rev: v0.4.3.9021 + rev: v0.4.3.9025 hooks: - id: roxygenize additional_dependencies: - - insightsengineering/formatters + - formatters - magrittr - methods - checkmate @@ -18,6 +18,7 @@ repos: - lifecycle - stats - stringi + - roxygen2 - id: use-tidy-description - id: spell-check exclude: > diff --git a/R/tree_accessors.R b/R/tree_accessors.R index 70efe6f2ad..aba45d0f31 100644 --- a/R/tree_accessors.R +++ b/R/tree_accessors.R @@ -1199,6 +1199,10 @@ setMethod("obj_na_str<-", "Split", function(obj, value) { #' @export setMethod("obj_na_str", "VTableNodeInfo", function(obj) obj@na_str) +#' @rdname int_methods +#' @export +setMethod("obj_na_str", "RowsVerticalSection", function(obj) attr(obj, "row_na_strs", exact = TRUE)) + #' @rdname formatters_methods #' @export setMethod("obj_na_str", "Split", function(obj) obj@split_na_str) diff --git a/R/tt_pos_and_access.R b/R/tt_pos_and_access.R index f79a45eb36..7aeec7e7c4 100644 --- a/R/tt_pos_and_access.R +++ b/R/tt_pos_and_access.R @@ -1584,6 +1584,14 @@ setMethod( } ) +#' @rdname int_methods +#' @keywords internal +#' @exportMethod cell_values +setMethod( + "cell_values", "RowsVerticalSection", + function(tt, rowpath, colpath = NULL, omit_labrows = TRUE) rawvalues(tt) +) + #' @rdname int_methods #' @keywords internal #' @exportMethod cell_values diff --git a/inst/WORDLIST b/inst/WORDLIST index 632e83e430..0c24671d1e 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -80,6 +80,7 @@ responder Resync reusability roadmap +RowsVerticalSection RStudio rtables Rua diff --git a/vignettes/guided_advanced_afuns_spl_context.Rmd b/vignettes/guided_advanced_afuns_spl_context.Rmd new file mode 100644 index 0000000000..bd892b50b1 --- /dev/null +++ b/vignettes/guided_advanced_afuns_spl_context.Rmd @@ -0,0 +1,264 @@ +--- +title: "Advanced rtables - Structure-Conditional Behavior In `afun`s With `.spl_context`" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Structure-Conditional Behavior In afuns With .spl_context} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +library(rtables) +``` + +# Split Context + +The split context (i.e., the optional `.spl_context` argument to a/c +functions) provides analysis functions the ability to know what +substructure of the table it is calculating cell values for, the data +(sub)set corresponding to that substructure, and the steps - both in +terms of faceting structure and corresponding data subsetting - taken +by the tabulation engine to arrive where it is now. + +This allows us to program custom analysis functions which have +behavior *conditional on which row or column facet they are currently +calculating cell values for*, as we will see further down in this +document. + +The *split context* is a `data.frame` with one row per faceting step +in row space up to and including the one the analysis function is +being called within, and the following columns which vary across +context rows: + +- **`split`** (`character`) - name of the split represented by each + row of the split context +- **`value`** (`character`) - string representation of the value of + the split for each row of the context +- **`full_parent_df`** (`list` of `data.frame`s) - the full data + (across all columns) corresponding to each row faceting step +- **`all_cols_n`** (`integer`) - the observation count for each row + faceting step (across all columns) +- **``** (one `list` column per column in the table) - + logical vectors corresponding to the subset of `full_parent_df` for + the named column for each faceting step. Named by + `names(col_exprs(tab))`. + +In addition, the context contains the following columns which are +constant across context rows: + +- **`cur_col_id`** (`character`) - identifier for the current column +- **`cur_col_expr`** (`list` of `expresssion` objects) - symbolic + expression for subset corresponding to current column. +- **`cur_col_n`** (`integer`) - column count for the current column +- **`cur_col_split`** (`list` of `character`) - vector of split names from the + path which resolves to the current column. +- **`cur_col_split_value`** (`list` of `character`) - vector of split + values from the path which resolves to the current column. + +Interleaving `cur_col_split` and `cur_col_split_value` will recreate +the full unique column path for the current column. + + +# Designing Conditional Behavior in `afun`s + +Recall that table contents are (typically) calculated by repeated +calling the analysis or content function for a given row facet - once +per individual column within the table structure. + +## Conditional-On-Column Behavior in `afun`s + +If we want our table to have different *types* of content in +different cells of the same row, we need an `afun` that + +1. can determine where in column space it is calculating cells for, and +2. implements two or more behaviors which it selects between based on (1) + +**Note** it is *mandatory* that the calls to our analysis or content +function result in the same number of rows within each column. This +can involve padding the results with blank cells in some columns. + +### Determining Column-Space Position Within An `afun` + +We can use the `cur_col_*` elements of the split context - all of +which are constant across rows - to determine where in the column +structure we are creating cells for, as we saw in the [Translating +Shells](./guided_intermediate_translating_Shells.html) portion of the +intermediate guided tour. In that function we used `cur_col_id` to indicate column, but using `cur_col_split` and/or `cur_col_split_value` is more robust, as follows: + +```{r} +in_risk_diff <- function(spl_context) { + any(grepl("Risk Differences", spl_context$cur_col_split_value[1])) +} +``` + +We can use the first element of the `cur_col_id` column of the split +context because as noted above, the column information columns are +constant across rows in the context. + +The `cur_col_id` value in the split context is currently computed by +pasting the split values of the column path to the current column. + +### General Template For Column Aware `afun` + +Assuming two desired behaviors depending on column position (e.g., +primary or risk difference column), a general template for a +conditional-on-column `afun` is: + + +```{r, eval = FALSE} +col_condition <- function(spl_context) { + ## return TRUE or FALSE +} + +col_cond_afun_template1 <- function(df, .var, ..., .spl_context) { + ## shared processing + + if (col_condition(.spl_context)) { + ## alternate behavior + + ## data processing + + ## value calculation + + ## determine cell formats, etc + } else { + ## primary behavior + + ## data processing + + ## value calculation + + ## determine cell formats, etc + } + + ## label calculation, etc if necessary + + in_rows(val_list, .labels = lbl_vector, .formats = format_vector) +} +``` + +Or, alternatively if we have two existing `afun`s that each fully +encapsulate the desired behavior for one of the conditions, + +```{r, eval = FALSE} +col_cond_afun_template2 <- function(df, .var, ..., .spl_context) { + if (col_condition(.spl_context)) { + alt_behavior_afun(df, .var, ..., .spl_context = .spl_context) + } else { + main_behavior_afun(df, .var, ..., .spl_context = .spl_context) + } +} +``` + +We note that both of the approaches above would be straightforward to +extend to more than two conditional behaviors by utilizing a condition +function which could return more than two values, and a `switch` call +or `if`/`ifelse`/`else` block. We leave this as an exercise for the +reader. + + +## Using Row Faceting Information Within `afun`s + +The split context is a data frame with a row for each preceding row +faceting (splitting) step; in particular, as described above, we have +access to the full data, split name, and split value for each of these +steps. + +Some illustrative examples of code extracting information from the +split context are: + +| code | context row | faceting step | what it extracts | +|+------------------------------------------------------+|+--------------+|+------------------+|+------------------------------------------------+| +| `.split_context$full_parent_df[[1]]` | first | root (no faceting) | full data (that was passed to `build_table` | +| `.split_context$split[NROW(.split_context)]` | last | current facet | name of the current split (typically a var name) | +| `.split_context$split_value[NROW(.split_context) - 1]` | second to last | parent facet | facet value of parent facet | + +We often want to retrieve reference group information for use in model +fitting or risk difference calculations. In practice this translates +to a different column facet's intersection with our current row facet +than the column we are currently operating within. + +Given a `ref_path`, which can be passed as an extra argument in the +`analyze` call if it's constant or set as an extra argument on each +split value by a custom split function if not (see `junco`'s +`grouped_cols_w_diffs` function for an example of this), we can +extract the relevant data. + +We will use the fact that the column subsetting vectors are included +in the split context by their "col ids", which currently are +constructed by pasting the split values (only) collapsed with ".": + +```{r} +basic_get_ref <- function(ref_path, spl_context) { + facet_dat <- spl_context$full_parent_df[[NROW(spl_context)]] + + ref_col_id <- paste(ref_path[seq(2, length(ref_path), by = 2)]) + + ref_subset_vec <- spl_context[[ref_col_id]][[NROW(spl_context)]] + + ref_dat <- facet_dat[ref_subset_vec, ] + + list(ref_group = ref_dat, in_ref_col = ref_col_id == spl_context$cur_col_id[[1]]) +} +``` + +We can see that this is working via a diagnostic table that shows us what is coming out of that function: + + + +```{r} +diag_afun <- function(df, .spl_context, ref_path) { + ref_info <- basic_get_ref(ref_path, .spl_context) + + in_rows( + data_dim = dim(df), + ref_dim = dim(ref_info$ref_group), + in_ref_col = ref_info$in_ref_col, + .formats = c( + data_dim = "xx, xx", + ref_dim = "xx, xx", + in_ref_col = "xx" + ) + ) +} + + +lyt <- basic_table() |> + split_cols_by("ARM") |> + split_rows_by("STRATA1") |> + split_rows_by("SEX", split_fun = keep_split_levels(c("F", "M"))) |> + analyze("AGE", diag_afun, extra_args = list(ref_path = c("ARM", "B: Placebo"))) + + +build_table(lyt, ex_adsl) +``` From 7bea7c601f5c7259e4b9f1acfb4c53225c7cbbcf Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 14:21:51 -0700 Subject: [PATCH 02/11] RowsVerticalSection and BBB vignettes plus needed fixes --- DESCRIPTION | 2 +- NAMESPACE | 1 + R/00tabletrees.R | 37 ++ R/make_split_fun.R | 2 +- R/tree_accessors.R | 26 +- inst/WORDLIST | 1 + man/c.RowsVerticalSection.Rd | 22 ++ man/formatters_methods.Rd | 6 + man/int_methods.Rd | 9 + man/row_accessors.Rd | 3 + man/rtables-package.Rd | 2 +- man/trim_levels_in_facets.Rd | 2 +- ...ded_advanced_afuns_rowsverticalsection.Rmd | 318 ++++++++++++++++++ .../guided_advanced_split_funs_new_bbbs.Rmd | 291 ++++++++++++++++ 14 files changed, 716 insertions(+), 6 deletions(-) create mode 100644 man/c.RowsVerticalSection.Rd create mode 100644 vignettes/guided_advanced_afuns_rowsverticalsection.Rmd create mode 100644 vignettes/guided_advanced_split_funs_new_bbbs.Rmd diff --git a/DESCRIPTION b/DESCRIPTION index ca7224b583..7be56a64fc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -67,7 +67,6 @@ Config/Needs/verdepcheck: insightsengineering/formatters, davidgohel/officer, Merck/r2rtf, rstudio/rmarkdown, therneau/survival, r-lib/testthat, tidyverse/tibble, tidyverse/tidyr, r-lib/withr, r-lib/xml2 -Config/roxygen2/version: 8.0.0 Encoding: UTF-8 Language: en-US LazyData: true @@ -105,3 +104,4 @@ Collate: 'tt_from_df.R' 'validate_table_struct.R' 'zzz_constants.R' +Config/roxygen2/version: 8.0.0 diff --git a/NAMESPACE b/NAMESPACE index f92c9b0742..a300067a10 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method(c,RowsVerticalSection) S3method(print,CellValue) S3method(print,RowsVerticalSection) export("cell_footnotes<-") diff --git a/R/00tabletrees.R b/R/00tabletrees.R index a3493ee5e6..3401d8745b 100644 --- a/R/00tabletrees.R +++ b/R/00tabletrees.R @@ -2182,6 +2182,43 @@ print.RowsVerticalSection <- function(x, ...) { invisible(x) } +#' Combine RowsVerticalSection objects +#' Combine two or more RowsVerticalSection objects (as returned +#' by [in_rows()]) into a single object +#' +#' @param ... RowsVerticalSection objects +#' @returns A single RowsVerticalSection object containing all +#' row sections from the objects passed to `...` +#' @export +c.RowsVerticalSection <- function(...) { + lst <- list(...) + if (!all(sapply(lst, function(x) inherits(x, "RowsVerticalSection")))) { + stop("Cannot use c() to combine RowsVerticalSection objects with objects of other clases") + } + + out <- NextMethod(generic = "c") + out <- RowsVerticalSection( + out, + names = comb_attr_w_dflt(lst, "row_names"), + labels = comb_attr_w_dflt(lst, "row_labels"), + indent_mods = comb_attr_w_dflt(lst, "indent_mods", 0L), + formats = comb_attr_w_dflt(lst, "row_formats", "xx"), + footnotes = comb_attr_w_dflt(lst, "row_footnotes"), + format_na_strs = comb_attr_w_dflt(lst, "row_na_strs", NA_character_) + ) + out +} + +comb_attr_w_dflt <- function(lst, attrname, dflt = NULL) { + unlist( + lapply(lst, function(x) { + attr(x, attrname, exact = TRUE) %||% rep(dflt, length(x)) + }), + recursive = FALSE, + use.names = FALSE + ) +} + #### Empty default objects to avoid repeated calls ## EmptyColInfo <- InstantiatedColumnInfo() ## EmptyElTable <- ElementaryTable() diff --git a/R/make_split_fun.R b/R/make_split_fun.R index bf7812facd..e2dea8b458 100644 --- a/R/make_split_fun.R +++ b/R/make_split_fun.R @@ -394,7 +394,7 @@ add_overall_facet <- function(name, label, extra = list()) { #' #' @param innervar (`character`)\cr the variable(s) to trim (remove unobserved levels) independently within each facet. #' -#' @return A function suitable for use in the `pre` (list) argument of `make_split_fun`. +#' @return A function suitable for use in the `post` (list) argument of `make_split_fun`. #' #' @seealso [make_split_fun()] #' diff --git a/R/tree_accessors.R b/R/tree_accessors.R index aba45d0f31..04a815a2d5 100644 --- a/R/tree_accessors.R +++ b/R/tree_accessors.R @@ -999,6 +999,10 @@ setGeneric("row_cells", function(obj) standardGeneric("row_cells")) #' @exportMethod row_cells setMethod("row_cells", "TableRow", function(obj) obj@leaf_value) +#' @rdname row_accessors +#' @exportMethod row_cells +setMethod("row_cells", "RowsVerticalSection", function(obj) as(obj, "list", strict = TRUE)) + #' @rdname row_accessors setGeneric("row_cells<-", function(obj, value) standardGeneric("row_cells<-")) @@ -1017,7 +1021,6 @@ setGeneric("row_values", function(obj) standardGeneric("row_values")) #' @exportMethod row_values setMethod("row_values", "TableRow", function(obj) rawvalues(obj@leaf_value)) - #' @rdname row_accessors #' @exportMethod row_values<- setGeneric("row_values<-", function(obj, value) standardGeneric("row_values<-")) @@ -1153,6 +1156,10 @@ setMethod("obj_format", "CellValue", function(obj) attr(obj, "format", exact = T #' @export setMethod("obj_format", "Split", function(obj) obj@split_format) +#' @rdname formatters_methods +#' @export +setMethod("obj_format", "RowsVerticalSection", function(obj) attr(obj, "row_formats", exact = TRUE)) + #' @rdname formatters_methods #' @export setMethod("obj_format<-", "VTableNodeInfo", function(obj, value) { @@ -1160,6 +1167,7 @@ setMethod("obj_format<-", "VTableNodeInfo", function(obj, value) { obj }) + #' @rdname formatters_methods #' @export setMethod("obj_format<-", "Split", function(obj, value) { @@ -1174,6 +1182,13 @@ setMethod("obj_format<-", "CellValue", function(obj, value) { obj }) +#' @rdname formatters_methods +#' @export +setMethod("obj_format<-", "RowsVerticalSection", function(obj, value) { + attr(obj, "row_formats") <- value + obj +}) + #' @rdname int_methods #' @export setMethod("obj_na_str<-", "CellValue", function(obj, value) { @@ -1181,6 +1196,13 @@ setMethod("obj_na_str<-", "CellValue", function(obj, value) { obj }) +#' @rdname int_methods +#' @export +setMethod("obj_na_str<-", "RowsVerticalSection", function(obj, value) { + attr(obj, "row_na_strs") <- value + obj +}) + #' @rdname int_methods #' @export setMethod("obj_na_str<-", "VTableNodeInfo", function(obj, value) { @@ -1682,7 +1704,7 @@ setMethod( "must have length 1 or the number of rows" ) } - attr(obj, "indent_mods") <- as.integer(value) + attr(obj, "indent_mods") <- rep(as.integer(value), length.out = length(obj)) obj ## obj@indent_mods <- value diff --git a/inst/WORDLIST b/inst/WORDLIST index 0c24671d1e..38801e409c 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -7,6 +7,7 @@ amongst ARD ard ARDs +BBBs biomarker BMEASIFL Bov diff --git a/man/c.RowsVerticalSection.Rd b/man/c.RowsVerticalSection.Rd new file mode 100644 index 0000000000..ffe5300033 --- /dev/null +++ b/man/c.RowsVerticalSection.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/00tabletrees.R +\name{c.RowsVerticalSection} +\alias{c.RowsVerticalSection} +\title{Combine RowsVerticalSection objects +Combine two or more RowsVerticalSection objects (as returned +by \code{\link[=in_rows]{in_rows()}}) into a single object} +\usage{ +\method{c}{RowsVerticalSection}(...) +} +\arguments{ +\item{...}{RowsVerticalSection objects} +} +\value{ +A single RowsVerticalSection object containing all +row sections from the objects passed to \code{...} +} +\description{ +Combine RowsVerticalSection objects +Combine two or more RowsVerticalSection objects (as returned +by \code{\link[=in_rows]{in_rows()}}) into a single object +} diff --git a/man/formatters_methods.Rd b/man/formatters_methods.Rd index e9829a39ed..9477c4da90 100644 --- a/man/formatters_methods.Rd +++ b/man/formatters_methods.Rd @@ -17,9 +17,11 @@ \alias{obj_format,VTableNodeInfo-method} \alias{obj_format,CellValue-method} \alias{obj_format,Split-method} +\alias{obj_format,RowsVerticalSection-method} \alias{obj_format<-,VTableNodeInfo-method} \alias{obj_format<-,Split-method} \alias{obj_format<-,CellValue-method} +\alias{obj_format<-,RowsVerticalSection-method} \alias{obj_na_str,Split-method} \alias{main_title,VTitleFooter-method} \alias{main_title<-,VTitleFooter-method} @@ -86,12 +88,16 @@ \S4method{obj_format}{Split}(obj) +\S4method{obj_format}{RowsVerticalSection}(obj) + \S4method{obj_format}{VTableNodeInfo}(obj) <- value \S4method{obj_format}{Split}(obj) <- value \S4method{obj_format}{CellValue}(obj) <- value +\S4method{obj_format}{RowsVerticalSection}(obj) <- value + \S4method{obj_na_str}{Split}(obj) \S4method{main_title}{VTitleFooter}(obj) diff --git a/man/int_methods.Rd b/man/int_methods.Rd index 31bad1aec2..0b13ebddfe 100644 --- a/man/int_methods.Rd +++ b/man/int_methods.Rd @@ -181,9 +181,11 @@ \alias{spanned_values<-,TableRow-method} \alias{spanned_values<-,LabelRow-method} \alias{obj_na_str<-,CellValue-method} +\alias{obj_na_str<-,RowsVerticalSection-method} \alias{obj_na_str<-,VTableNodeInfo-method} \alias{obj_na_str<-,Split-method} \alias{obj_na_str,VTableNodeInfo-method} +\alias{obj_na_str,RowsVerticalSection-method} \alias{set_format_recursive} \alias{set_format_recursive,TableRow-method} \alias{set_format_recursive,LabelRow-method} @@ -382,6 +384,7 @@ \alias{[,VTableTree,missing,numeric-method} \alias{[,VTableTree,numeric,numeric-method} \alias{cell_values,VTableTree-method} +\alias{cell_values,RowsVerticalSection-method} \alias{cell_values,TableRow-method} \alias{cell_values,LabelRow-method} \alias{value_at,TableRow-method} @@ -792,12 +795,16 @@ spanned_values(obj) <- value \S4method{obj_na_str}{CellValue}(obj) <- value +\S4method{obj_na_str}{RowsVerticalSection}(obj) <- value + \S4method{obj_na_str}{VTableNodeInfo}(obj) <- value \S4method{obj_na_str}{Split}(obj) <- value \S4method{obj_na_str}{VTableNodeInfo}(obj) +\S4method{obj_na_str}{RowsVerticalSection}(obj) + set_format_recursive(obj, format, na_str, override = FALSE) \S4method{set_format_recursive}{TableRow}(obj, format, na_str, override = FALSE) @@ -1194,6 +1201,8 @@ obj_stat_names(obj) <- value \S4method{cell_values}{VTableTree}(tt, rowpath = NULL, colpath = NULL, omit_labrows = TRUE) +\S4method{cell_values}{RowsVerticalSection}(tt, rowpath = NULL, colpath = NULL, omit_labrows = TRUE) + \S4method{cell_values}{TableRow}(tt, rowpath = NULL, colpath = NULL, omit_labrows = TRUE) \S4method{cell_values}{LabelRow}(tt, rowpath = NULL, colpath = NULL, omit_labrows = TRUE) diff --git a/man/row_accessors.Rd b/man/row_accessors.Rd index 479c192d35..08eef9db02 100644 --- a/man/row_accessors.Rd +++ b/man/row_accessors.Rd @@ -6,6 +6,7 @@ \alias{obj_avar,ElementaryTable-method} \alias{row_cells} \alias{row_cells,TableRow-method} +\alias{row_cells,RowsVerticalSection-method} \alias{row_cells<-} \alias{row_cells<-,TableRow-method} \alias{row_values} @@ -25,6 +26,8 @@ row_cells(obj) \S4method{row_cells}{TableRow}(obj) +\S4method{row_cells}{RowsVerticalSection}(obj) + row_cells(obj) <- value \S4method{row_cells}{TableRow}(obj) <- value diff --git a/man/rtables-package.Rd b/man/rtables-package.Rd index dd6327e07d..1b035fec9b 100644 --- a/man/rtables-package.Rd +++ b/man/rtables-package.Rd @@ -33,7 +33,7 @@ Authors: Other contributors: \itemize{ - \item Daniel Sabanés Bové \email{daniel.sabanes_bove@rconis.com} [contributor] + \item Daniel Sabans Bov \email{daniel.sabanes_bove@rconis.com} [contributor] \item Maximilian Mordig \email{maximilian_oliver.mordig@roche.com} [contributor] \item Abinaya Yogasekaram \email{ayogasek@gmail.com} (\href{https://orcid.org/0009-0005-2083-1105}{ORCID}) [contributor] \item F. Hoffmann-La Roche AG [copyright holder, funder] diff --git a/man/trim_levels_in_facets.Rd b/man/trim_levels_in_facets.Rd index 89e6fd342e..ba53da7edc 100644 --- a/man/trim_levels_in_facets.Rd +++ b/man/trim_levels_in_facets.Rd @@ -10,7 +10,7 @@ trim_levels_in_facets(innervar) \item{innervar}{(\code{character})\cr the variable(s) to trim (remove unobserved levels) independently within each facet.} } \value{ -A function suitable for use in the \code{pre} (list) argument of \code{make_split_fun}. +A function suitable for use in the \code{post} (list) argument of \code{make_split_fun}. } \description{ Trim levels of another variable from each facet (post-processing split step) diff --git a/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd b/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd new file mode 100644 index 0000000000..37b5622ddf --- /dev/null +++ b/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd @@ -0,0 +1,318 @@ +--- +title: "Advanced rtables - Calling Existing afuns Within Custom afuns" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Calling Existing afuns Within Custom afuns} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Analysis Function Return Values - `RowsVerticalSection`s + +Modern analysis functions return cell values via `in_rows`, which +constructs a `RowsVerticalSection` object. While the exact internal +implementation is not important, a `RowsVerticalSection` object +carries around cell values along with a number of *row*-level +formatting and rendering instructions; note that only the "first" (or +left-most) rows vertical section that includes cell values for a given +row will dictate row-level naming and rendering behavior. In +particular, `RowsVerticalSection` objects carry the following +instructions for rendering the resulting rows: + + +|name| accessor function|description| +|names |`value_names`| Names of the resulting rows| +|labels|`value_labels` | Labels of the resulting rows| +|indent mods| `indent_mod`, `indent_mod<-` | Indent modifiers for resulting rows| +|formats |`obj_format`, `obj_format<-` | **row level** formats| +|NA strings| `obj_na_str`, `obj_na_str<-`| **row level** NA strings| +|footnotes|`row_footnotes`, `row_footnotes<-` | **row level** footnotes| + + +Note arguments to `in_rows` which don't correspond to one of the +above, e.g., `.aligns`, `.cell_footnotes` and `.stat_names` represent +*cell* level information which is carried around by the cell objects +rather than the `RowsVerticalSection` object. The cells objects can be +extracted via `row_cells`. + +## Combining `RowsVerticalSection` Objects + +As of version `0.16.16`, `rtables` provides a `c` method for directly +combining `RowsVerticalSection` objects; prior to that this was fairly +straightforward to achieve, requiring developers to combine the values +of the objects as well as each of the above attributes and typically +passing these to the `RowsVerticalSection` constructor directly. For +those using earlier versions of `rtables` we provide example +`RowsVerticalSection` combination code in Appendix A. + +```{r} +library(rtables) +rvs1 <- in_rows(what = 17.123, .formats = c(what = "xx.x")) +rvs1 +``` + +```{r} +rvs2 <- in_rows(ok = "hi", + nah = "bye", + .indent_mods = c(ok = 1, nah = -1), .row_footnotes = list(nah = "I guess not ...")) +rvs2 +``` + +```{r} +c(rvs1, rvs2) +``` + +# Combining Existing Analysis Functions + +We assume here that all our analysis functions return their computed +cell values via calls to `in_rows`; this should be true of any +function written (or generated via a factor) specifically to be an +analysis function. + +There are (at least) three ways to combine existing analysis functions: + 1. conditionally call one or the other depending on, typically, where + in the column structure we are, + 2. call both functions and return a `RowsVerticalSection` + representing all rows generated by either function, or + 3. call one analysis function and then, conditional on row position, + call another and combine its results to the first. + +Either of (2) or (3) can also be combined with (1). + + +## Different Analysis Functions For Different Columns + +Our first form of combining existing analysis functions is to simply +selectively call one or the other depending on column position. We can +build a risk difference harness using this method: + + +```{r} +library(rtables) +placeholder_rd_afun <- function(df, .var, .spl_context, ref_path) { + val <- tail(.spl_context$cur_col_split_val[[1]], 1) + levs <- levels(df[[.var]]) + len <- length(levs) + + lst <- setNames(rep(val, len), levs) + in_rows(.list = lst, .formats = setNames(rep("xx", len), levs) ) +} + +comb_afun <- function(df, .var, .spl_context, ref_path) { + + if (grepl("difference", .spl_context$cur_col_id[[1]], ignore.case = TRUE)) { + ret <- placeholder_rd_afun(df, .var, .spl_context, ref_path) + } else { + ret <- simple_analysis(df[[.var]]) + } + ret +} + +adsl <- ex_adsl +adae <- ex_adae + +adsl$trt_span <- ifelse(adsl$ARM == "B: Placebo", " ", "Active Treatment") +adae$trt_span <- ifelse(adae$ARM == "B: Placebo", " ", "Active Treatment") +adsl$rr_header = "Risk Differences" +adae$rr_header = "Risk Differences" +adsl$rr_label <- paste(adsl$ARM, "vs B: Placebo") +adae$rr_label <- paste(adae$ARM, "vs B: Placebo") + +trtmap <- data.frame(rr_header = c("Active Treatment", "Active Treatment", " "), + ARM = c("A: Drug X", "C: Combination", "B: Placebo") +) + +lyt <- basic_table() |> + split_cols_by("trt_span", split_fun = trim_levels_in_group("ARM")) |> + split_cols_by("ARM") |> + split_cols_by("rr_header", nested = FALSE) |> + split_cols_by("rr_label", split_fun = remove_split_levels("B: Placebo vs B: Placebo")) |> + analyze("AEBODSYS", afun = comb_afun, extra_args = list(ref_path = c("ARM", "B: Placebo"))) + +build_table(lyt, adae, adsl) +``` + +Note: while we constructed the spanning variables, risk difference +labels, treatment map and column structure layout instructions +manually to avoid circular dependencies, we refer users to +`grouped_cols_w_diffs` in the `junco` package which encapsulates +creating this particular column structure. + +## Stacking Analysis Functions + +We can also 'stack' two existing analysis functions by creating new +function which calls each of them and combines the resulting +`RowsVerticalSection` objects. Note that while we will create two toy +example analysis functions to stack here, this approach only really +makes sense when at least one analysis function is pre-existing, such +as those provided by `tern` and `junco`. + +```{r} +afun_1 <- function(df, .var) { + dat_vec <- df[[.var]] + in_rows("Total Events" = sum(!is.na(dat_vec))) +} + +afun_2 <- function(df, .var, .N_col, id) { + non_na <- !is.na(df[[.var]]) + count <- length(unique(df[[id]])) + in_rows("Unique Patients" = count * c(1, 1/.N_col), .formats = c("Unique Patients" = "xx (xx.x%)")) +} + +stacked_afun <- function(df, .var, .N_col, id) { + events_rvs <- afun_1(df, .var) + pats_rvs <- afun_2(df, .var, .N_col, id) + c(events_rvs, pats_rvs) +} + +``` + +```{r} +lyt <- basic_table() |> + split_cols_by("ARM") |> + split_rows_by("AEBODSYS", split_fun = trim_levels_in_group("AEDECOD")) |> + split_rows_by("AEDECOD") |> + analyze("STUDYID", afun = stacked_afun, extra_args = list(id = "USUBJID")) + +build_table(lyt, ex_adae, ex_adsl) +``` + +Note, some care is required, for example + +- If any of the stacked afuns accept `df` as their first argument, the + combining function must do so as well + - the data vector can be constructed to pass to any that accept `x`, + if necessary +- The combining function must accept the union of additional arguments + (both `rtables` populated and extra) accepted by the functions being + stacked. + +## Conditional Stacking + +In some cases we want to to add additional analysis rows for only some +values or within only some row facets (recall, all column facets must +have the same number of rows across each row facet, independently). + +For example, a simplified version of a disposition table can display +counts for each final study status (`EOSSTT`), and then provide detailed +counts for each reason for discontinuation (`DCSREAS`) under only the +`"DISCONTINUED"` value. + +```{r} +afun_count_lbl <- function(df, .var, lbl) { + in_rows(sum(!is.na(df[[.var]])), .names = lbl) +} +basic_two_tier <- function(df, .var, .spl_context, detail_var, detail_level) { + + values <- lapply(levels(df[[.var]]), + function(lvl) { + dat <- df[df[[.var]] == lvl,] + rvs_out <- afun_count_lbl(dat, .var, lvl) + if (lvl %in% detail_level) { + det_rvs <- simple_analysis(dat[[detail_var]]) + indent_mod(det_rvs) <- 1 + rvs_out <- c(rvs_out, det_rvs) + } + rvs_out + } + ) + ret <- do.call(c, values) + ret +} +``` + +Here we use a simple counting function separately for each level in +`.var` (`EOSSTT` in this case), and then - only for the `DISCONTINUED` +level, stack the result of `simple_analysis` for our detail variable +(`DCSREAS` for our table). + +```{r} + +lyt <- basic_table() |> + split_cols_by("ARM") |> + analyze("EOSSTT", afun = basic_two_tier, extra_args = list(detail_var = "DCSREAS", detail_level = "DISCONTINUED")) + +build_table(lyt, ex_adsl) +``` + +Note that for production usage, `junco` provides `a_two_tier` for this +purpose which is preferred to creating our own combination afun from +scratch in most cases. + +We leave it as an exercise to use the methods in the +[./guided_advanced_afuns_spl_context.html](Split Context) portion of +this guide to reformulate this so that we split on `EOSSTT` and then +use an afun with behavior conditional on the current row facet. + + +# Appendix A - Code For `c.RowsVerticalSection` + +The following is a copy of a development version of +`c.RowsVerticalSection` at the time of writing this vignette; this +code is not guaranteed to be kept in sync with `rtables`' exported +version of `c.RowsVerticalSection` and should be used for illustrative +and back-porting purposes only. + +```{r, eval = FALSE} +c.RowsVerticalSection <- function(...) { + lst <- list(...) + if (!all(sapply(lst, function(x) inherits(x, "RowsVerticalSection")))) { + stop("Cannot use c() to combine RowsVerticalSection objects with objects of other clases") + } + + out <- NextMethod(generic = "c") + out <- RowsVerticalSection( + out, + names = comb_attr_w_dflt(lst, "row_names"), + labels = comb_attr_w_dflt(lst, "row_labels"), + indent_mods = comb_attr_w_dflt(lst, "indent_mods", 0L), + formats = comb_attr_w_dflt(lst, "row_formats", "xx"), + footnotes = comb_attr_w_dflt(lst, "row_footnotes"), + format_na_strs = comb_attr_w_dflt(lst, "row_na_strs", NA_character_) + ) + out +} + +comb_attr_w_dflt <- function(lst, attrname, dflt = NULL) { + unlist( + lapply(lst, function(x) { + attr(x, attrname, exact = TRUE) %||% rep(dflt, length(x)) + }), + recursive = FALSE, + use.names = FALSE + ) +} +``` diff --git a/vignettes/guided_advanced_split_funs_new_bbbs.Rmd b/vignettes/guided_advanced_split_funs_new_bbbs.Rmd new file mode 100644 index 0000000000..7dcb18ba3b --- /dev/null +++ b/vignettes/guided_advanced_split_funs_new_bbbs.Rmd @@ -0,0 +1,291 @@ +--- +title: "Advanced rtables - Writing Reusable Behavior Building Blocks" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Writing Reusable Behavior Building Blocks} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Split Function Behavioral Building Blocks + +We call functions that can used as either pre- or post processing +functions in `make_split_fun` (via entry into the lists passed to +`pre` and `post`, respectively) *behavioral building blocks* +(BBBs). Behavioral building blocks are modular, typically atomic (as +in they only do one narrow thing to the data or split result) +functions which we can mix and match to construct complex custom +behaviors in our split function. + +There are two types of behavioral building blocks: + +- *pre*-processing - accept, modify, and return the incoming parent data, and +- *post*-processing - accept, modify, and return a split result object + representing facets generated by the core splitting machinery + +## Existing BBBs provided by `rtables` + +`rtables` provides a small number of behavioral building blocks that +are ready for use: + + +|behavior | function | fun factory |usage notes | BBB type| +|---------|----------|-------------|---------| +|drop specific facets | `restrict_facets`| yes | `op = "drop"`| post| +|keep only specific facets | `restrict_facets` | yes | `op = "keep"` | post| +|reorder facets | `restrict_facets` | yes |`op = keep`, `reorder = TRUE`, pass all existing facet names| post| +|add combination facet | `add_combo_facet` | yes |create single combo facet, can be called repeatedly | post | +| add overall/total facet | `add_overall_facet` | yes | | post| | +| Trim levels of another var in each facet | `trim_levels_in_facet` | yes | equivalent to `trim_levels_in_group` | post| +| exclude facets for unobserved variable levels | `drop_facet_levels` | no | | pre | + + +We refer to the help for each of these functions for examples of their +usage and will not recreate single examples here. + + +# Creating Post Processing BBBs + +Post processing behavioral building blocks are functions which accept: + +- `ret` - split result object returned by the core splitting machinery + *or* a previously applied post processing BBB +- `spl` - Split object +- `fulldf` - incoming full data which was split in this faceting step +- `.spl_context` - *optional* split context object + +and returns `ret` modified typically in one of four ways: + +1. using `add_to_split_result` on `ret` to add facets manually to it, +2. manually removing facets from it (or wrapping logic around a + `restrict_facets` call), +3. reordering the existing facets, or, rarely, +4. calling `make_split_result` to construct an entirely new split result. + +## Illustrative And/Or Useful Examples +Here we implement a number of simple behavioral building blocks that +both illustrate how to create our own and may be useful in some +circumstances. + +### Placing Specific Facets First And/Or Last + + +Here we want to specify certain facets (in practice, often combination +facets whose order can't be controlled via variable re-leveling) to +appear first and or last *amongst their direct siblings*. + +Recognizing that this is ultimately a reordering behavior, we can wrap +a call to `restrict_facets` with `reorder = TRUE` after calculating +the desired full ordering: + + +```{r} +library(rtables) + +put_facets_first_last <- function(first = NULL, last = NULL) { + if (is.null(first) && is.null(last)) { + stop("must speficify at least one facet to be placed first or last") + } + function(ret, spl, fulldf) { + fac_names <- names(ret$values) + all_speced <- c(first, last) + + if (!all(all_speced %in% fac_names)) + stop("Facet(s) []", paste(setdiff(all_speced, fac_names), collapse = ", "), "] not found in incoming split result.") + tmpfun <- restrict_facets(c(first, setdiff(fac_names, all_speced), last), op = "keep", reorder = TRUE) + tmpfun(ret, spl, fulldf) + } +} +``` + + +While this could be achieved by variable re-leveling, we show that this +works by forcing the `U` and `UNDIFFERENTIATED` levels of `SEX` to be +first and last, respectively: + +```{r} +fl_splfun <- make_split_fun( + post = list( + put_facets_first_last(first = "U", last = "UNDIFFERENTIATED") + )) +``` + +We can then compare two similar (column) layouts to see the effect of our BBB + +```{r} +lyt_basic <- basic_table() |> + split_cols_by("SEX") + +build_table(lyt_basic, ex_adsl) +``` + +```{r} +lyt_fl <- basic_table() |> + split_cols_by("SEX", split_fun = fl_splfun) + +build_table(lyt_fl, ex_adsl) +``` +### Pre-sorting Or Pre-pruning Facets Based On Data Sparsity + +Here we want to either reorder our facets or remove some facets based +on how much data they represent. + +```{r} +presort_facets <- function(ret, spl, fulldf) { + fac_names <- names(ret$values) + fac_ns <- vapply(ret$datasplit, NROW, 1L) + ord <- order(fac_ns, decreasing = TRUE) + tmpfun <- restrict_facets(fac_names[ord], op = "keep", reorder = TRUE) + tmpfun(ret, spl, fulldf) +} +``` + +Here we can see that using this building block gives our desired behavior: + + +```{r} + +presort_splfun <- make_split_fun(post = list(presort_facets)) + +lyt_presort <- basic_table(show_colcounts = TRUE) |> + split_cols_by("STRATA1", split_fun = presort_splfun) + +build_table(lyt_presort, ex_adsl) +``` + +And similarly here: +```{r} + +drop_sparse_facets <- function(ncutoff = 5) { + function(ret, spl, fulldf) { + fac_names <- names(ret$values) + fac_ns <- vapply(ret$datasplit, NROW, 1L) + keep_inds <- which(fac_ns >= ncutoff) + tmpfun <- restrict_facets(fac_names[keep_inds], op = "keep", reorder = FALSE) + tmpfun(ret, spl, fulldf) + + } +} + +lyt_preprune1 <- basic_table(show_colcounts = TRUE) |> + split_cols_by("SEX") + +build_table(lyt_preprune1, ex_adsl) + +preprune_splfun2 <- make_split_fun(post = list(drop_sparse_facets())) +lyt_preprune2 <- basic_table(show_colcounts = TRUE) |> + split_cols_by("SEX", split_fun = preprune_splfun2) + +build_table(lyt_preprune2, ex_adsl) + +preprune_splfun3 <- make_split_fun(post = list(drop_sparse_facets(10))) +lyt_preprune3 <- basic_table(show_colcounts = TRUE) |> + split_cols_by("SEX", split_fun = preprune_splfun3) + +build_table(lyt_preprune3, ex_adsl) +``` + +# Creating Preprocessing BBBs + +Custom pre-processing BBB requirements are rarer than post-processing +ones, as most things are simpler to do over a small set of facets +rather than a large set of incoming data. Furthermore, most things +that could be done via a pre-processing BBB can also be done via a +post-processing BBB. + +That said, for illustrative purposes, we can recreate part of +functionality of `trim_levels_to_map` in a preprocessing BBB (the +restriction of data based on inner variable values), like so: + + +```{r} +trim_facets_to_map <- function(map = NULL) { + function(df, spl, vals, labels, .spl_context) { + if(is.null(map)) + return(df) # do nothing + cur_outer_val <- tail(.spl_context$value, 1) + inner_var <- names(map)[2] + inner_vec <- df[[inner_var]] + inner_keep <- map[map[[1]] == cur_outer_val, inner_var, drop = TRUE] + df_out <- df[inner_vec %in% inner_keep, ] + df_out[[inner_var]] <- factor(df_out[[inner_var]], levels = intersect(levels(inner_vec), inner_keep)) + df_out + } +} +``` + +We use `spl_variable` to retrieve the variable name for the split, +determine the levels of the inner variable to keep based on the map +and the current level of the split context, restrict the data to rows +where the inner variable is the desired value(s), and recreate the +inner variable factor to drop unwanted levels. + +Because we are doing this as factor re-leveling *before* the core +splitting machinery is invoked, we will use this as a pre-processing +BBB ***on the inner variable split***; for a post-processing BBB we +would do it on the split data of the outer variable split. + +Note: If our map does not include at least one entry for each factor +level defined by the incoming data, we need to restrict those at the +previous split; `trim_levels_to_map` combines this behavior. + + +```{r} +map <- data.frame(ARM = c("A: Drug X", "B: Placebo"), +STRATA1 = c("B", "A")) + +map_splfun <- make_split_fun(pre = list(trim_facets_to_map(map))) + +outer_splfun <- make_split_fun(post = list(restrict_facets("C: Combination", op = "exclude"))) + +lyt <- basic_table() |> + split_cols_by("ARM", split_fun = outer_splfun) |> + split_cols_by("STRATA1", split_fun = map_splfun) + +build_table(lyt, ex_adsl) +``` + +This matches the core behavior of `trim_levels_to_map`: + + +```{r} + +lyt <- basic_table() |> + split_cols_by("ARM", split_fun = trim_levels_to_map(map)) |> + split_cols_by("STRATA1") + +build_table(lyt, ex_adsl) +``` From 9ca6053c936dc93821d859056cc2a89560655fe0 Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 14:23:26 -0700 Subject: [PATCH 03/11] [no spell] update pkgdown file --- _pkgdown.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 3191ece3d2..1dba072709 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -323,3 +323,4 @@ reference: - length,CellValue-method - names,VTableNodeInfo-method - insert_rrow + - c.RowsVerticalSection From d76b1678443518e5bd9c6f3d2d29be034a2aa3b5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:27:33 +0000 Subject: [PATCH 04/11] [skip style] [skip vbump] Restyle files --- R/00tabletrees.R | 2 +- ...ded_advanced_afuns_rowsverticalsection.Rmd | 28 +++++++++---------- .../guided_advanced_split_funs_new_bbbs.Rmd | 21 +++++++------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/R/00tabletrees.R b/R/00tabletrees.R index 3401d8745b..3ff504db52 100644 --- a/R/00tabletrees.R +++ b/R/00tabletrees.R @@ -2185,7 +2185,7 @@ print.RowsVerticalSection <- function(x, ...) { #' Combine RowsVerticalSection objects #' Combine two or more RowsVerticalSection objects (as returned #' by [in_rows()]) into a single object -#' +#' #' @param ... RowsVerticalSection objects #' @returns A single RowsVerticalSection object containing all #' row sections from the objects passed to `...` diff --git a/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd b/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd index 37b5622ddf..ac34b6586c 100644 --- a/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd +++ b/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd @@ -85,9 +85,11 @@ rvs1 ``` ```{r} -rvs2 <- in_rows(ok = "hi", +rvs2 <- in_rows( + ok = "hi", nah = "bye", - .indent_mods = c(ok = 1, nah = -1), .row_footnotes = list(nah = "I guess not ...")) + .indent_mods = c(ok = 1, nah = -1), .row_footnotes = list(nah = "I guess not ...") +) rvs2 ``` @@ -122,17 +124,16 @@ build a risk difference harness using this method: ```{r} library(rtables) -placeholder_rd_afun <- function(df, .var, .spl_context, ref_path) { +placeholder_rd_afun <- function(df, .var, .spl_context, ref_path) { val <- tail(.spl_context$cur_col_split_val[[1]], 1) levs <- levels(df[[.var]]) len <- length(levs) lst <- setNames(rep(val, len), levs) - in_rows(.list = lst, .formats = setNames(rep("xx", len), levs) ) + in_rows(.list = lst, .formats = setNames(rep("xx", len), levs)) } comb_afun <- function(df, .var, .spl_context, ref_path) { - if (grepl("difference", .spl_context$cur_col_id[[1]], ignore.case = TRUE)) { ret <- placeholder_rd_afun(df, .var, .spl_context, ref_path) } else { @@ -146,12 +147,13 @@ adae <- ex_adae adsl$trt_span <- ifelse(adsl$ARM == "B: Placebo", " ", "Active Treatment") adae$trt_span <- ifelse(adae$ARM == "B: Placebo", " ", "Active Treatment") -adsl$rr_header = "Risk Differences" -adae$rr_header = "Risk Differences" +adsl$rr_header <- "Risk Differences" +adae$rr_header <- "Risk Differences" adsl$rr_label <- paste(adsl$ARM, "vs B: Placebo") adae$rr_label <- paste(adae$ARM, "vs B: Placebo") -trtmap <- data.frame(rr_header = c("Active Treatment", "Active Treatment", " "), +trtmap <- data.frame( + rr_header = c("Active Treatment", "Active Treatment", " "), ARM = c("A: Drug X", "C: Combination", "B: Placebo") ) @@ -189,7 +191,7 @@ afun_1 <- function(df, .var) { afun_2 <- function(df, .var, .N_col, id) { non_na <- !is.na(df[[.var]]) count <- length(unique(df[[id]])) - in_rows("Unique Patients" = count * c(1, 1/.N_col), .formats = c("Unique Patients" = "xx (xx.x%)")) + in_rows("Unique Patients" = count * c(1, 1 / .N_col), .formats = c("Unique Patients" = "xx (xx.x%)")) } stacked_afun <- function(df, .var, .N_col, id) { @@ -197,7 +199,6 @@ stacked_afun <- function(df, .var, .N_col, id) { pats_rvs <- afun_2(df, .var, .N_col, id) c(events_rvs, pats_rvs) } - ``` ```{r} @@ -236,10 +237,10 @@ afun_count_lbl <- function(df, .var, lbl) { in_rows(sum(!is.na(df[[.var]])), .names = lbl) } basic_two_tier <- function(df, .var, .spl_context, detail_var, detail_level) { - - values <- lapply(levels(df[[.var]]), + values <- lapply( + levels(df[[.var]]), function(lvl) { - dat <- df[df[[.var]] == lvl,] + dat <- df[df[[.var]] == lvl, ] rvs_out <- afun_count_lbl(dat, .var, lvl) if (lvl %in% detail_level) { det_rvs <- simple_analysis(dat[[detail_var]]) @@ -260,7 +261,6 @@ level, stack the result of `simple_analysis` for our detail variable (`DCSREAS` for our table). ```{r} - lyt <- basic_table() |> split_cols_by("ARM") |> analyze("EOSSTT", afun = basic_two_tier, extra_args = list(detail_var = "DCSREAS", detail_level = "DISCONTINUED")) diff --git a/vignettes/guided_advanced_split_funs_new_bbbs.Rmd b/vignettes/guided_advanced_split_funs_new_bbbs.Rmd index 7dcb18ba3b..bc60e10ea4 100644 --- a/vignettes/guided_advanced_split_funs_new_bbbs.Rmd +++ b/vignettes/guided_advanced_split_funs_new_bbbs.Rmd @@ -123,8 +123,9 @@ put_facets_first_last <- function(first = NULL, last = NULL) { fac_names <- names(ret$values) all_speced <- c(first, last) - if (!all(all_speced %in% fac_names)) + if (!all(all_speced %in% fac_names)) { stop("Facet(s) []", paste(setdiff(all_speced, fac_names), collapse = ", "), "] not found in incoming split result.") + } tmpfun <- restrict_facets(c(first, setdiff(fac_names, all_speced), last), op = "keep", reorder = TRUE) tmpfun(ret, spl, fulldf) } @@ -140,7 +141,8 @@ first and last, respectively: fl_splfun <- make_split_fun( post = list( put_facets_first_last(first = "U", last = "UNDIFFERENTIATED") - )) + ) +) ``` We can then compare two similar (column) layouts to see the effect of our BBB @@ -177,7 +179,6 @@ Here we can see that using this building block gives our desired behavior: ```{r} - presort_splfun <- make_split_fun(post = list(presort_facets)) lyt_presort <- basic_table(show_colcounts = TRUE) |> @@ -188,7 +189,6 @@ build_table(lyt_presort, ex_adsl) And similarly here: ```{r} - drop_sparse_facets <- function(ncutoff = 5) { function(ret, spl, fulldf) { fac_names <- names(ret$values) @@ -196,7 +196,6 @@ drop_sparse_facets <- function(ncutoff = 5) { keep_inds <- which(fac_ns >= ncutoff) tmpfun <- restrict_facets(fac_names[keep_inds], op = "keep", reorder = FALSE) tmpfun(ret, spl, fulldf) - } } @@ -234,8 +233,9 @@ restriction of data based on inner variable values), like so: ```{r} trim_facets_to_map <- function(map = NULL) { function(df, spl, vals, labels, .spl_context) { - if(is.null(map)) - return(df) # do nothing + if (is.null(map)) { + return(df) + } # do nothing cur_outer_val <- tail(.spl_context$value, 1) inner_var <- names(map)[2] inner_vec <- df[[inner_var]] @@ -264,8 +264,10 @@ previous split; `trim_levels_to_map` combines this behavior. ```{r} -map <- data.frame(ARM = c("A: Drug X", "B: Placebo"), -STRATA1 = c("B", "A")) +map <- data.frame( + ARM = c("A: Drug X", "B: Placebo"), + STRATA1 = c("B", "A") +) map_splfun <- make_split_fun(pre = list(trim_facets_to_map(map))) @@ -282,7 +284,6 @@ This matches the core behavior of `trim_levels_to_map`: ```{r} - lyt <- basic_table() |> split_cols_by("ARM", split_fun = trim_levels_to_map(map)) |> split_cols_by("STRATA1") From e58e59b9f1bbc3bfe1a0c23c232db3485b35b1d6 Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 14:28:00 -0700 Subject: [PATCH 05/11] reapply suggested vapply tweak --- R/00tabletrees.R | 4 ++-- vignettes/guided_advanced_afuns_rowsverticalsection.Rmd | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/00tabletrees.R b/R/00tabletrees.R index 3401d8745b..fee61167ef 100644 --- a/R/00tabletrees.R +++ b/R/00tabletrees.R @@ -2192,8 +2192,8 @@ print.RowsVerticalSection <- function(x, ...) { #' @export c.RowsVerticalSection <- function(...) { lst <- list(...) - if (!all(sapply(lst, function(x) inherits(x, "RowsVerticalSection")))) { - stop("Cannot use c() to combine RowsVerticalSection objects with objects of other clases") + if (!all(vapply(lst, function(x) inherits(x, "RowsVerticalSection"), TRUE))) { + stop("Cannot use c() to combine RowsVerticalSection objects with objects of other classes") } out <- NextMethod(generic = "c") diff --git a/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd b/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd index 37b5622ddf..cc1fdcdb1f 100644 --- a/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd +++ b/vignettes/guided_advanced_afuns_rowsverticalsection.Rmd @@ -289,8 +289,8 @@ and back-porting purposes only. ```{r, eval = FALSE} c.RowsVerticalSection <- function(...) { lst <- list(...) - if (!all(sapply(lst, function(x) inherits(x, "RowsVerticalSection")))) { - stop("Cannot use c() to combine RowsVerticalSection objects with objects of other clases") + if (!all(vapply(lst, function(x) inherits(x, "RowsVerticalSection"), TRUE))) { + stop("Cannot use c() to combine RowsVerticalSection objects with objects of other classes") } out <- NextMethod(generic = "c") From 8654a7a99d357834e5bbc513c8ad684ab8d5b598 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:32:15 +0000 Subject: [PATCH 06/11] [skip roxygen] [skip vbump] Roxygen Man Pages Auto Update --- man/rtables-package.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/rtables-package.Rd b/man/rtables-package.Rd index 1b035fec9b..dd6327e07d 100644 --- a/man/rtables-package.Rd +++ b/man/rtables-package.Rd @@ -33,7 +33,7 @@ Authors: Other contributors: \itemize{ - \item Daniel Sabans Bov \email{daniel.sabanes_bove@rconis.com} [contributor] + \item Daniel Sabanés Bové \email{daniel.sabanes_bove@rconis.com} [contributor] \item Maximilian Mordig \email{maximilian_oliver.mordig@roche.com} [contributor] \item Abinaya Yogasekaram \email{ayogasek@gmail.com} (\href{https://orcid.org/0009-0005-2083-1105}{ORCID}) [contributor] \item F. Hoffmann-La Roche AG [copyright holder, funder] From b6a59a35f0e4f9f739c4c668c98ad45da0886638 Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 15:23:51 -0700 Subject: [PATCH 07/11] Add vignette stubs for remaining advanced tour --- vignettes/analysis_basics_a.png | Bin 0 -> 48829 bytes vignettes/guided_advanced.Rmd | 108 ++++++++++ vignettes/guided_advanced_afuns.Rmd | 195 ++++++++++++++++++ .../guided_advanced_afuns_building_blocks.Rmd | 43 ++++ vignettes/guided_advanced_split_funs.Rmd | 58 ++++++ vignettes/guided_advanced_split_funs_bbbs.Rmd | 43 ++++ ...ded_advanced_split_funs_make_split_fun.Rmd | 43 ++++ .../guided_advanced_split_funs_worked_ex.Rmd | 43 ++++ vignettes/guided_advanced_tt.Rmd | 56 +++++ vignettes/guided_advanced_tt_access.Rmd | 43 ++++ vignettes/guided_advanced_tt_prune_funs.Rmd | 43 ++++ vignettes/guided_advanced_tt_score_funs.Rmd | 43 ++++ 12 files changed, 718 insertions(+) create mode 100644 vignettes/analysis_basics_a.png create mode 100644 vignettes/guided_advanced.Rmd create mode 100644 vignettes/guided_advanced_afuns.Rmd create mode 100644 vignettes/guided_advanced_afuns_building_blocks.Rmd create mode 100644 vignettes/guided_advanced_split_funs.Rmd create mode 100644 vignettes/guided_advanced_split_funs_bbbs.Rmd create mode 100644 vignettes/guided_advanced_split_funs_make_split_fun.Rmd create mode 100644 vignettes/guided_advanced_split_funs_worked_ex.Rmd create mode 100644 vignettes/guided_advanced_tt.Rmd create mode 100644 vignettes/guided_advanced_tt_access.Rmd create mode 100644 vignettes/guided_advanced_tt_prune_funs.Rmd create mode 100644 vignettes/guided_advanced_tt_score_funs.Rmd diff --git a/vignettes/analysis_basics_a.png b/vignettes/analysis_basics_a.png new file mode 100644 index 0000000000000000000000000000000000000000..fe225a376ab1cf4da736566e493882a253e02481 GIT binary patch literal 48829 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 literal 0 HcmV?d00001 diff --git a/vignettes/guided_advanced.Rmd b/vignettes/guided_advanced.Rmd new file mode 100644 index 0000000000..86b1423a84 --- /dev/null +++ b/vignettes/guided_advanced.Rmd @@ -0,0 +1,108 @@ +--- +title: "A Guided Tour of rtables - Advanced" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-12-27" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true +vignette: > + %\VignetteIndexEntry{A Guided Tour of rtables - Advanced} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + + +# Scope and Audience + + +We saw in the previous [intermediate](./guided_intermediate.html) +portion of this tour that a well engineered library of analysis, group +summary, and split functions can combine to support a massive array of +different individual tables. Whether we are tasked with maintaining +and extending those libraries or simply with creating custom tables +outside of that supported space, we sometimes need to write new custom +functions. By the end of this portion of the tour we will have the +tools necessary to do that. While gaining those tools, we will also +become more familiar with the structure of `TableTree` objects (how +`rtables` models tables) and how to interact with them once they are +created. + +Upon learning the material in this portion of the training, users will +be able to fully exploit the flexibility and power of the `rtables` +layout and table engines to create virtually any desired table in +cases when their existing function library falls short. + + +# Chapters + +- [Custom Analysis and Group Summary + Functions](./guided_advanced_afuns.html) Core concepts for creating + custom analysis and group summary functions + - [Structure-Conditional Behavior In `afun`s With `.spl_context`](./guided_advanced_afuns_spl_context.html) + Creating `afun`/`cfun` behavior conditional on location within the + table structure using `.spl_context` and other optional arguments. + - [Calling Existing `afun`s Within Custom `afun`s](./guided_advanced_afuns_rowsverticalsection.html) Details + about what `in_rows` returns and how we can use that to wrap or + combine existing `afun`s or `cfun`s + - [Useful Behavioral Building Blocks For Complex Custom + `afun`s](./guided_advanced_afuns_building_blocks.html) Examples of + prototypical behaviors which can be reused and combined when + writing custom `afun`s +- [Custom Split Functions](./guided_advanced_split_funs.html) Core + concepts for creating custom split functions + - [Using `make_split_fun` Effectively](./guided_advanced_split_funs_make_split_fun.html) `make_split_fun` and + recognizing when to specify `pre`, `core`, and `post` behavior + customizations + - [Using And Combining Provided Behavior Building + Blocks](./guided_advanced_split_funs_bbbs.html) The split function + behavior building blocks provided by `rtables` and how to use and + combine them + - [Writing Reusable Behavior Building + Blocks](./guided_advanced_split_funs_new_bbbs.html) Writing new + custom split function behaviors so that they are reusable + - [Some Complex Worked + Examples](./guided_advanced_split_funs_worked_ex.html) Combining + these topics to create complex custom split functions +- [Understanding and Interacting With `TableTree` + Objects](./guided_advanced_tt.html) Understanding how `rtables` + models tables and how to interact with them after creation + - [Table Structure and Pathing](./pathing.html) Table structure and + describing locations within a table via pathing (existing + vignette) + - [Accessing Values Within A + Table](./guided_advanced_tt_access.html) Retrieving values + from a table + - [Writing Custom Scoring Functions For + Sorting](./guided_advanced_tt_score_funs.html) Writing custom + scoring functions for use with `sort_at_path` + - [Writing Custom Pruning + Functions](./guided_advanced_tt_prune_funs.html) Writing custom + pruning functions for use with `prune_table` diff --git a/vignettes/guided_advanced_afuns.Rmd b/vignettes/guided_advanced_afuns.Rmd new file mode 100644 index 0000000000..2e276d23ab --- /dev/null +++ b/vignettes/guided_advanced_afuns.Rmd @@ -0,0 +1,195 @@ +--- +title: "Advanced rtables - Custom Analysis and Group Summary Functions" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Custom Analysis and Group Summary Functions} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Analysis and Group Summary Function Review + +During table creation, `rtables` calculates the contents for rows in +normal and marginal summary rows by calling analysis an group summary +functions, respectively, on the relevant facet data. Thus, while the +`split_row_by*` and `split_cols_by*` functions allow us to declare the +*structure* of our desired table, the `analyze` and +`summarize_row_groups` functions, via arguments `afun` and `cfun`, +respectively, allow us to declare the contents to appear in each +structural facet of our desired table. + +![](./analysis_basics_a.png) + +Key points to recall about `a/cfun`s: + +- First argument must be `x` or `df` + - `x` will be passed a *facet data vector* for the variable (column) being analyzed/summarized + - `df` will be passed the full *facet data frame* containing all columns of the data subset for the facet +- Can accept optional *special* arguments which will be populated by `rtables` during tabulation + - Values of these arguments cannot currently be overridden by the user +- Can accept additional (non-*special*) arguments as desired + - These can be passed via the `extra_args` argument to `analyze`/`summarize_row_groups` +- Should return the result of calling `in_rows` (a `RowsVerticalSection` object) +- Only difference between `afun`s and `cfun`s is that the latter must accept `labelstr` as the second argument + - `labelstr` will be automatically populated with the label for the + facet being summarized for a `cfun` and not passed to functions + used as an `afun` + + +# General Analysis/Summary Function Structure + +Due to the last key point listed above, we can template a function +that can be used as both an analysis and summary function: + +```{r} +library(rtables) + +template_acfun <- function(x, + labelstr = NULL, + ## , + ## , + ...) { + if (is.null(labelstr)) { + ## 'calculate' label(s) for afun-usage case + lbl <- "cool label, bro" + } else { + ## calculate label(s) from labelstr for cfun-usage case + lbl <- labelstr + } + + ## whatever calculations we want + out <- rcell(sample(c("what?", "huh?", "eh?"), 1), format = "xx") + + ## return our value(s) via in_rows + in_rows(.list = list(ok = out), .labels = c(ok = lbl)) +} +``` + +We can then use this function in either capacity: + +```{r} +lyt <- basic_table() |> + split_cols_by("ARM") |> + split_rows_by("STRATA1", split_fun = keep_split_levels(c("A", "B"))) |> + summarize_row_groups("STRATA1", cfun = template_acfun) |> + split_rows_by("SEX", split_fun = keep_split_levels(c("F", "M"))) |> + summarize_row_groups("SEX", cfun = template_acfun) |> + analyze("AGE", afun = template_acfun) + +build_table(lyt, ex_adsl) +``` + +In light of the above, we will - without loss of generality - discuss +analysis functions exclusively for the remainder of this guide with +the exception of any situation where the difference is specifically +relevant. + +# Arguments To Analysis Functions + +Beyond `.spl_context`, which is covered in detail on its own in the next section of this guide, the special arguments (again: those that `rtables` will populate itself during tabulation) can be categorized into three rough, somewhat overlapping groups: + +- Marginal Counts +- Facet Data +- Reference Group Information + +## `afun` Special Arguments: Marginal Counts + +Among special `afun` arguments supported by `rtables`, those which +supply marginal counts are the most straightforward. That said, some +care is warranted to ensure we understand the values our function will +receive, particularly in the cases of `.N_row` and `.N_total`, as we +will see. + +#### Marginal Column and Row Counts (`.N_col` and `.N_row`) + +`.N_col` will receive the column count - as understood by the +`rtables` machinery - for the individual column our analysis function +is currently being applied within. `.N_row` meanwhile, will receive a +row count of the facet data for the (full) row facet our function is +being applied to. + +When an `alt_counts_df` is provided in the call to `build_table` +`.N_col` will receive a count calculated based on that data frame, the +same as the column counts which can be optionally displayed when +rendering our tables. + +Unlike `.N_col`, however, `.N_row` will ***always receive a count +based on the primary data (`df`) passed to `build_table`***. Thus in +the common case of `df` being e.g., an `ADAE` dataset representing +individual events while `alt_counts_df` is the corresponding `ADSL` +dataset corresponding to subjects/patients, `.N_row` will receive a +count of *events*, while `.N_col` will receive a count of +*subjects*. This is due to the fact that `alt_counts_df` is required +to contain the variables necessary for all column splitting in our +layout, it *is not* required to contain all variables necessary for +the row splitting. + + +#### Other Marginal Count Special Argument + +`.all_col_counts` will receive the full vector of individual column +counts regardless of which column our `afun` is operating within. Like +`.N_col`, these counts will be based on `alt_counts_df` when it is +specified within the call to `build_table`. + +***It is not advised to use `N_total`.*** Its current implementation +effectively returns the sum of all column counts; while this will be +correct for tables with simple column structure, it does not take into +account partially or fully overlapping columns and will be incorrect +when those are present in the table structure. In the next chapter of +this guide we will use the split context (`.spl_context`) to derive a +robust equivalent to `.N_total` as a way of illustrating some of the +information the split context provides. + + + + + + + +# Further Topics On Creating Custom Analysis Functions + + - [Structure-Conditional Behavior In `afun`s With + `.spl_context`](./guided_advanced_afuns_spl_context.html) Creating + `afun`/`cfun` behavior conditional on location within the table + structure using `.spl_context` and other optional arguments. + - [Calling Existing `afun`s Within Custom `afun`s](./guided_advanced_afuns_rowsverticalsection.html) Details + about what `in_rows` returns and how we can use that to wrap or + combine existing `afun`s or `cfun`s + - [Useful Behavioral Building Blocks For Complex Custom + `afun`s](./guided_advanced_afuns_building_blocks.html) Examples of + prototypical behaviors which can be reused and combined when + writing custom `afun`s diff --git a/vignettes/guided_advanced_afuns_building_blocks.Rmd b/vignettes/guided_advanced_afuns_building_blocks.Rmd new file mode 100644 index 0000000000..5c58be0e5b --- /dev/null +++ b/vignettes/guided_advanced_afuns_building_blocks.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Useful Behavioral Building Blocks For Complex Custom afuns" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Useful Behavioral Building Blocks For Complex Custom afuns} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon diff --git a/vignettes/guided_advanced_split_funs.Rmd b/vignettes/guided_advanced_split_funs.Rmd new file mode 100644 index 0000000000..c7aed6eeb1 --- /dev/null +++ b/vignettes/guided_advanced_split_funs.Rmd @@ -0,0 +1,58 @@ +--- +title: "Advanced rtables - Custom Split Functions" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Custom Split Functions} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon + + - [`make_split_fun` and Its Effective + Use](./guided_advanced_split_funs_make_split_fun.html) `make_split_fun` and + recognizing when to specify `pre`, `core`, and `post` behavior + customizations + - [Using And Combining Provided Behavior Building + Blocks](./guided_advanced_split_funs_bbbs.html) The split function + behavior building blocks provided by `rtables` and how to use and + combine them + - [Writing Reusable Behavior Building + Blocks](./guided_advanced_split_funs_new_bbbs.html) Writing new + custom split function behaviors so that they are reusable + - [Some Complex Worked + Examples](./guided_advanced_split_funs_worked_ex.html) Combining + these topics to create complex custom split functions diff --git a/vignettes/guided_advanced_split_funs_bbbs.Rmd b/vignettes/guided_advanced_split_funs_bbbs.Rmd new file mode 100644 index 0000000000..0cc57ed33b --- /dev/null +++ b/vignettes/guided_advanced_split_funs_bbbs.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Using and Combining Provided Behavior Building Blocks" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Using and Combining Provided Behavior Building Blocks} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon diff --git a/vignettes/guided_advanced_split_funs_make_split_fun.Rmd b/vignettes/guided_advanced_split_funs_make_split_fun.Rmd new file mode 100644 index 0000000000..58630a3c45 --- /dev/null +++ b/vignettes/guided_advanced_split_funs_make_split_fun.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Using `make_split_fun` Effectively" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Using make_split_fun Effectively} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon diff --git a/vignettes/guided_advanced_split_funs_worked_ex.Rmd b/vignettes/guided_advanced_split_funs_worked_ex.Rmd new file mode 100644 index 0000000000..a9ade6f811 --- /dev/null +++ b/vignettes/guided_advanced_split_funs_worked_ex.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Some Complex Split Function Worked Examples" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Some Complex Split Function Worked Examples} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon diff --git a/vignettes/guided_advanced_tt.Rmd b/vignettes/guided_advanced_tt.Rmd new file mode 100644 index 0000000000..47261841aa --- /dev/null +++ b/vignettes/guided_advanced_tt.Rmd @@ -0,0 +1,56 @@ +--- +title: "Advanced rtables - Understanding and Interacting With `TableTree` Objects" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Understanding and Interacting With TableTree Objects} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon + + - [Table Structure and Pathing](./pathing.html) Table structure and + describing locations within a table via pathing (existing + vignette) + - [Accessing Values Within A + Table](./guided_advanced_tt_access.html) Retrieving values + from a table + - [Writing Custom Scoring Functions For + Sorting](./guided_advanced_tt_score_funs.html) Writing custom + scoring functions for use with `sort_at_path` + - [Writing Custom Pruning + Functions](./guided_advanced_tt_prune_funs.html) Writing custom + pruning functions for use with `prune_table` diff --git a/vignettes/guided_advanced_tt_access.Rmd b/vignettes/guided_advanced_tt_access.Rmd new file mode 100644 index 0000000000..042357ef17 --- /dev/null +++ b/vignettes/guided_advanced_tt_access.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Accessing Values Within a Table" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Accessing Values Within a Table} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon diff --git a/vignettes/guided_advanced_tt_prune_funs.Rmd b/vignettes/guided_advanced_tt_prune_funs.Rmd new file mode 100644 index 0000000000..f2a0eccb2d --- /dev/null +++ b/vignettes/guided_advanced_tt_prune_funs.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Writing Custom Pruning Functions" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Writing Custom Pruning Functions} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon diff --git a/vignettes/guided_advanced_tt_score_funs.Rmd b/vignettes/guided_advanced_tt_score_funs.Rmd new file mode 100644 index 0000000000..32f37337ff --- /dev/null +++ b/vignettes/guided_advanced_tt_score_funs.Rmd @@ -0,0 +1,43 @@ +--- +title: "Advanced rtables - Writing Custom Scoring Functions For Sorting" +subtitle: Contributed by Johnson & Johnson Innovative Medicine +date: "2025-10-22" +author: +- Gabriel Becker +- Dan Hofstaedter +output: + rmarkdown::html_document: + theme: "spacelab" + highlight: "kate" + toc: true + toc_float: true + code_folding: show +vignette: > + %\VignetteIndexEntry{Advanced rtables - Writing Custom Scoring Functions For Sorting} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 + chunk_output_type: console +--- + +```{r, include = FALSE} +suggested_dependent_pkgs <- c("dplyr") +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = all(vapply( + suggested_dependent_pkgs, + requireNamespace, + logical(1), + quietly = TRUE + )) +) +``` + +```{r, echo=FALSE} +knitr::opts_chunk$set(comment = "#") +``` + +# Coming Soon From f59700e6914e52f51d95a3d4934d284c77116c22 Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 15:57:33 -0700 Subject: [PATCH 08/11] collapse bbb vignettes into one --- vignettes/guided_advanced_split_funs.Rmd | 11 ++--- vignettes/guided_advanced_split_funs_bbbs.Rmd | 43 ------------------- 2 files changed, 4 insertions(+), 50 deletions(-) delete mode 100644 vignettes/guided_advanced_split_funs_bbbs.Rmd diff --git a/vignettes/guided_advanced_split_funs.Rmd b/vignettes/guided_advanced_split_funs.Rmd index c7aed6eeb1..f209626a90 100644 --- a/vignettes/guided_advanced_split_funs.Rmd +++ b/vignettes/guided_advanced_split_funs.Rmd @@ -46,13 +46,10 @@ knitr::opts_chunk$set(comment = "#") Use](./guided_advanced_split_funs_make_split_fun.html) `make_split_fun` and recognizing when to specify `pre`, `core`, and `post` behavior customizations - - [Using And Combining Provided Behavior Building - Blocks](./guided_advanced_split_funs_bbbs.html) The split function - behavior building blocks provided by `rtables` and how to use and - combine them - - [Writing Reusable Behavior Building - Blocks](./guided_advanced_split_funs_new_bbbs.html) Writing new - custom split function behaviors so that they are reusable + - [Combining And Creating Behavior Building + Blocks](./guided_advanced_split_funs_new_bbbs.html) The split function + behavior building blocks provided by `rtables`, how to use and + combine them, and how to create your own - [Some Complex Worked Examples](./guided_advanced_split_funs_worked_ex.html) Combining these topics to create complex custom split functions diff --git a/vignettes/guided_advanced_split_funs_bbbs.Rmd b/vignettes/guided_advanced_split_funs_bbbs.Rmd deleted file mode 100644 index 0cc57ed33b..0000000000 --- a/vignettes/guided_advanced_split_funs_bbbs.Rmd +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: "Advanced rtables - Using and Combining Provided Behavior Building Blocks" -subtitle: Contributed by Johnson & Johnson Innovative Medicine -date: "2025-10-22" -author: -- Gabriel Becker -- Dan Hofstaedter -output: - rmarkdown::html_document: - theme: "spacelab" - highlight: "kate" - toc: true - toc_float: true - code_folding: show -vignette: > - %\VignetteIndexEntry{Advanced rtables - Using and Combining Provided Behavior Building Blocks} - %\VignetteEncoding{UTF-8} - %\VignetteEngine{knitr::rmarkdown} -editor_options: - markdown: - wrap: 72 - chunk_output_type: console ---- - -```{r, include = FALSE} -suggested_dependent_pkgs <- c("dplyr") -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>", - eval = all(vapply( - suggested_dependent_pkgs, - requireNamespace, - logical(1), - quietly = TRUE - )) -) -``` - -```{r, echo=FALSE} -knitr::opts_chunk$set(comment = "#") -``` - -# Coming Soon From eb24bd09697160426e9a8437b5611fadf6aa4b79 Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 15:58:25 -0700 Subject: [PATCH 09/11] update pkgdown [no spell] --- _pkgdown.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 1dba072709..640e872e7b 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -68,6 +68,19 @@ articles: - guided_intermediate_translating_shells - guided_intermediate_afun_reqs - guided_intermediate_split_reqs + - guided_advanced + - guided_advanced_afuns + - guided_advanced_afuns_spl_context + - guided_advanced_afuns_rowsverticalsection + - guided_advanced_afuns_building_blocks + - guided_advanced_split_funs + - guided_advanced_split_funs_make_split_fun + - guided_advanced_split_funs_new_bbbs + - guided_advanced_split_funs_worked_ex + - guided_advanced_tt + - guided_advanced_tt_access + - guided_advanced_tt_score_funs + - guided_advanced_tt_prune_funs - title: Clinical Trials navbar: Clinical Trials From 4093d030e108b76c0d1ab7aa294a150e773554ac Mon Sep 17 00:00:00 2001 From: Gabe Becker Date: Thu, 4 Jun 2026 16:14:19 -0700 Subject: [PATCH 10/11] update NEWS.md --- NEWS.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 405f057316..f8d2a1f9a9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,26 @@ ## rtables 0.6.16.9001 ### New Features - * Added `restrict_facets` function factory for use with `make_split_fun` - * Exportd previously internal `make_subset_expr` for use when constructing custom splitting behavior + * Added `restrict_facets` function factory for use with `make_split_fun` @gmbecker + * Exported previously internal `make_subset_expr` for use when constructing custom splitting behavior + * Added accessor methods for RowsVerticalSection objects: `row_cells`, `obj_format`, `obj_format<-`, `obj_na_str`, `obj_na_str<-`, `cell_values` + * Added `c` method for directly combining `RowsVerticalSection` objects + * Added vignette: Guided Tour (Advanced) @gmbecker + * Added vignette: Guided Tour (Advanced) - Custom Analysis And Summary Functions @gmbecker + * Added vignette: Guided Tour (Advanced) - Analysis Functions - Split Context @gmbecker + * Added vignette: Guided Tour (Advanced) - Analysis Functions - Combining Existing `afun`s @gmbecker + * Added vignette: Guided Tour (Advanced) - Analysis Functions - (stub) Useful Building Blocks @gmbecker + * Added vignette: Guided Tour (Advanced) - Custom Split Functions @gmbecker + * Added vignette: Guided Tour (Advanced) - Custom Split Functions - (stub) `make_split_fun` @gmbecker + * Added vignette: Guided Tour (Advanced) - Custom Split Functions - Behavioral Building Block @gmbecker + * Added vignette: Guided Tour (Advanced) - Custom Split Functions - (stub) Worked Examples @gmbecker + * Added vignette: Guided Tour (Advanced) - (stub) `TableTree` Objects @gmbecker + * Added vignette: Guided Tour (Advanced) - (stub) `TableTree` Objects - (stub) Accessing Table Values @gmbecker + * Added vignette: Guided Tour (Advanced) - (stub) `TableTree` Objects - (stub) Custom Scoring Functions For Sorting @gmbecker + * Added vignette: Guided Tour (Advanced) - (stub) `TableTree` Objects - (stub) Custom Pruning Functions @gmbecker + +### Bug Fixes + * `obj_na_str<-` RowsVerticalSection method now correctly recycles length 1 values @gmbecker ## rtables 0.6.15 From 1abc2708a56bd9454cd745646485bb24f9c4efd8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:17:12 +0000 Subject: [PATCH 11/11] [skip style] [skip vbump] Restyle files --- vignettes/guided_advanced_afuns.Rmd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vignettes/guided_advanced_afuns.Rmd b/vignettes/guided_advanced_afuns.Rmd index 2e276d23ab..df2dbc2d7c 100644 --- a/vignettes/guided_advanced_afuns.Rmd +++ b/vignettes/guided_advanced_afuns.Rmd @@ -78,13 +78,13 @@ that can be used as both an analysis and summary function: library(rtables) template_acfun <- function(x, - labelstr = NULL, - ## , - ## , - ...) { + labelstr = NULL, + ## , + ## , + ...) { if (is.null(labelstr)) { ## 'calculate' label(s) for afun-usage case - lbl <- "cool label, bro" + lbl <- "cool label, bro" } else { ## calculate label(s) from labelstr for cfun-usage case lbl <- labelstr