Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
0c9712f
fix: decouple visual styling from tbl_hierarchical_rate_by_grade to p…
Melkiades May 6, 2026
2a4a0fa
fix: decouple visual styling from tbl_hierarchical_rate_by_grade to p…
Melkiades May 6, 2026
6f13108
fix: add idempotency guard, robust metadata lookup, and double-call test
Melkiades May 6, 2026
450e0dd
fix: add idempotency guard, robust metadata lookup, and double-call test
Melkiades May 6, 2026
8becdc8
docs: update @details to match Find() lookup
Melkiades May 6, 2026
9c30ff7
docs: update @details to match Find() lookup
Melkiades May 6, 2026
fc7795e
docs and md
Melkiades May 6, 2026
02b4a8e
docs and md
Melkiades May 6, 2026
b23ab6d
snapshot
Melkiades May 6, 2026
ed63de6
snapshot
Melkiades May 6, 2026
74c8419
fix: wrap bare string vars with all_of() in tbl_mmrm to silence tidys…
Melkiades May 6, 2026
c2d8a7e
fix: wrap bare string vars with all_of() in tbl_mmrm to silence tidys…
Melkiades May 6, 2026
76f4c0b
fix: wrap bare string vars with all_of() in tbl_mmrm to silence tidys…
jszczypinski May 6, 2026
a71cc92
fix: wrap bare string vars with all_of() in tbl_mmrm to silence tidys…
jszczypinski May 6, 2026
1bd231c
refactor: move add_grade_column() into tbl_hierarchical_rate_by_grade.R
jszczypinski May 6, 2026
3b65517
refactor: move add_grade_column() into tbl_hierarchical_rate_by_grade.R
jszczypinski May 6, 2026
cc636c2
style: place @rdname directly above @export for add_grade_column
jszczypinski May 6, 2026
a8ca796
style: place @rdname directly above @export for add_grade_column
jszczypinski May 6, 2026
25d76ee
Merge branch 'js/fix-cartesian-join-explosion' of github.com:insights…
Melkiades May 6, 2026
a594068
Merge branch 'js/fix-cartesian-join-explosion' of github.com:insights…
Melkiades May 6, 2026
8307e4b
fix: wrap bare string vars with all_of() in tbl_mmrm to silence tidys…
Melkiades May 6, 2026
fd5912f
fix: wrap bare string vars with all_of() in tbl_mmrm to silence tidys…
Melkiades May 6, 2026
6ebd3f1
refactor: move add_grade_column() into tbl_hierarchical_rate_by_grade.R
Melkiades May 6, 2026
7f07717
refactor: move add_grade_column() into tbl_hierarchical_rate_by_grade.R
Melkiades May 6, 2026
0985155
style: place @rdname directly above @export for add_grade_column
Melkiades May 6, 2026
83afb76
style: place @rdname directly above @export for add_grade_column
Melkiades May 6, 2026
f6f6561
fix
Melkiades May 6, 2026
a74067c
fix
Melkiades May 6, 2026
061b591
Merge branch 'js/fix-cartesian-join-explosion' of github.com:insights…
Melkiades May 6, 2026
6f583c9
Merge branch 'js/fix-cartesian-join-explosion' of github.com:insights…
Melkiades May 6, 2026
280fe3e
test: cover character grade with missing grade group levels (lines 16…
jszczypinski May 6, 2026
48990aa
test: cover character grade with missing grade group levels (lines 16…
jszczypinski May 6, 2026
0af0743
fix
Melkiades May 6, 2026
004db2b
fix
Melkiades May 6, 2026
9dbcb27
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 8, 2026
773da96
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 8, 2026
4d37232
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 8, 2026
eb1dc20
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 8, 2026
a0ebb77
Apply suggestion from @Melkiades
Melkiades May 8, 2026
d4b5b40
Apply suggestion from the reviewer
Melkiades May 8, 2026
e8125a5
Merge branch 'js/fix-cartesian-join-explosion' of github.com:insights…
Melkiades May 8, 2026
3d5c5cc
Merge branch 'js/fix-cartesian-join-explosion' of github.com:insights…
Melkiades May 8, 2026
937eca4
fix: use .data$label to resolve R CMD check NOTE
Melkiades May 8, 2026
1e233a4
fix: use .data$label to resolve R CMD check NOTE
Melkiades May 8, 2026
0a33d5f
fix snaps on local
Melkiades May 8, 2026
f5cc741
fix snaps on local
Melkiades May 8, 2026
7122153
test: tighten assertions and add local_wide_snapshot() helper
Melkiades May 8, 2026
12600ad
test: tighten assertions and add local_wide_snapshot() helper
Melkiades May 8, 2026
359feaa
fix tests
Melkiades May 10, 2026
649ca75
fix tests
Melkiades May 10, 2026
f98eccb
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 18, 2026
d05454a
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 18, 2026
f42c7e2
update tbl_hierarchical_rate_by_grade snapshots after main merge
Melkiades May 20, 2026
92af30f
update tbl_hierarchical_rate_by_grade snapshots after main merge
Melkiades May 20, 2026
da0f144
add NEWS entry for add_grade_column() (#226)
Melkiades May 20, 2026
8c7f6de
add NEWS entry for add_grade_column() (#226)
Melkiades May 20, 2026
ecd2958
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 20, 2026
a6da1a5
snaps
Melkiades May 20, 2026
f45e9b3
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 20, 2026
939e500
Merge branch 'main' into js/fix-cartesian-join-explosion
Melkiades May 20, 2026
76ef8f4
merge
Melkiades May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export("%>%")
export(add_blank_rows)
export(add_difference_row)
export(add_forest)
export(add_grade_column)
export(add_hierarchical_count_row)
export(add_overall)
export(adjust_stat_columns_wrap)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

* `tbl_coxph()` now accepts a data.frame created by `get_cox_pairwise_df()` as input. (#207)

* Added `add_grade_column()` to inject a grade-label column into `tbl_hierarchical_rate_by_grade()` output. Decoupled from the table builder to prevent Cartesian join explosion in `tbl_merge()`. (#226)

### Other Updates

* `tbl_hierarchical_rate_and_count()` now emits zero-rows for unobserved factor levels in the first hierarchical variable. (#233)
Expand Down
200 changes: 144 additions & 56 deletions R/tbl_hierarchical_rate_by_grade.R
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
#' A gtsummary table of class `'tbl_hierarchical_rate_by_grade'`.
#'
#' @details
#' This function returns a structurally pristine table where the `label` column retains unique grade text
#' (e.g., "1", "2", "Grade 1-2"). This preserves row uniqueness required by [gtsummary::tbl_merge()] and
#' [tbl_with_pools()]. To apply visual formatting (grade column, label blanking, header styling), pipe the
#' result through [add_grade_column()] **after** any merging operations.
#'
#' When using the `filter` argument, the filter will be applied to the second variable from `variables`, i.e. the
#' adverse event terms variable. If an AE does not meet the filtering criteria, the AE overall row as well as all grade
#' and grade group rows within an AE section will be excluded from the table. Filtering out AEs does not exclude the
Expand Down Expand Up @@ -81,7 +86,8 @@
#' ),
#' grade_groups = grade_groups,
#' grades_exclude = "5"
#' )
#' ) |>
#' add_grade_column()
#'
#' # Example 2 ----------------------------------
#' # Filter: Keep AEs with an overall prevalence of greater than 10%
Expand All @@ -93,7 +99,8 @@
#' grade_groups = list("Grades 1-2" = c("1", "2"), "Grades 3-5" = c("3", "4", "5")),
#' filter = sum(n) / sum(N) > 0.10
#' ) |>
#' add_overall(last = TRUE)
#' add_overall(last = TRUE) |>
#' add_grade_column()
NULL

#' @export
Expand Down Expand Up @@ -365,6 +372,7 @@ tbl_hierarchical_rate_by_grade <- function(data,
}
)

# structural cleanup: remove duplicate/empty rows but preserve label uniqueness
tbl_final <- tbl_final |>
gtsummary::modify_table_body(
\(table_body) {
Expand All @@ -374,70 +382,27 @@ tbl_hierarchical_rate_by_grade <- function(data,
# remove soc summary rows if all sub-rows filtered out
dplyr::filter(!(.data$variable == soc & dplyr::lead(.data$variable) %in% c(soc, NA))) |>
# remove rows for grades in grades_exclude
dplyr::filter(!.data$label %in% grades_exclude) |>
dplyr::rowwise() |>
# add label_grade column to display grade labels
dplyr::mutate(
label_grade = dplyr::case_when(
.data$variable == grade ~ label,
.data$variable == ae | label == "- Any adverse events -" ~ "- Any Grade -",
.default = ""
),
.after = "label"
) |>
# remove grade levels from label column
dplyr::mutate(label = if (.data$variable == grade) "" else label) |>
# remove statistics from summary rows if not an overall row
dplyr::mutate(
across(
gtsummary::all_stat_cols(),
~ if (
.data$variable %in% c(ae, "..ard_hierarchical_overall..") |
.data$label_grade %in% c(lvls, names(grade_groups)) |
.data$label == "- Any adverse events -"
) {
.
} else {
NA
}
)
) |>
dplyr::ungroup()
dplyr::filter(!.data$label %in% grades_exclude)
}
) |>
# show label_grade column
gtsummary::modify_column_unhide("label_grade") |>
gtsummary::modify_column_alignment("label_grade", align = "left") |>
# remove default footnote
gtsummary::remove_footnote_header(columns = everything()) |>
# convert "0 (0.0%)" to "0"
gtsummary::modify_post_fmt_fun(
fmt_fun = ~ ifelse(. %in% c("0 (0.0%)", "0 (NA%)"), "0", .),
columns = gtsummary::all_stat_cols()
) |>
# update header label
gtsummary::modify_header(
label ~ paste0(label[[soc]], " \n", paste0(rep("\U00A0", 4L), collapse = ""), label[[ae]]),
label_grade ~ label[[grade]],
gtsummary::all_stat_cols() ~ "{level} \n(N = {n})"
)

# indent grade level labels within grade groups
if (!is_empty(grade_groups)) {
tbl_final <- tbl_final |>
gtsummary::modify_indent(
columns = "label_grade", rows = .data$variable == grade & .data$label_grade %in% unlist(grade_groups),
indent = 4L
)
}

# return final table ---------------------------------------------------------
tbl_final$call_list <- list(tbl_hierarchical_rate_by_grade = match.call())
tbl_final$cards <- list(
tbl_hierarchical_rate_by_grade = list(tbl_hierarchical = tbl_final$cards$tbl_ard_hierarchical)
)
tbl_final$inputs <- tbl_hierarchical_rate_by_grade_inputs

# inject metadata for downstream post-processing by add_grade_column()
tbl_final$custom_info <- list(
soc = soc,
ae = ae,
grade = grade,
grade_groups = grade_groups,
lvls = lvls,
label_list = label
)

tbl_final |>
structure(class = c("tbl_hierarchical_rate_by_grade", "gtsummary"))
}
Expand Down Expand Up @@ -508,3 +473,126 @@ tbl_hierarchical_rate_by_grade <- function(data,
#' @rdname tbl_hierarchical_rate_by_grade
#' @export
add_overall.tbl_hierarchical_rate_by_grade <- asNamespace("gtsummary")[["add_overall.tbl_hierarchical"]]

#' @param x (`gtsummary`)\cr
#' A gtsummary table produced by [tbl_hierarchical_rate_by_grade()], or a merged table
#' (e.g., from [tbl_with_pools()]) where the underlying tables were produced by
#' [tbl_hierarchical_rate_by_grade()].
#'
#' @details
#' ## `add_grade_column()`
#'
#' Post-processing function that applies visual formatting to tables generated by
#' [tbl_hierarchical_rate_by_grade()]. Must be called **after** any merging
#' (e.g., via [tbl_with_pools()]) to avoid Cartesian join explosions caused by blanking
#' the `label` column prior to merge.
#'
#' The function extracts metadata injected by [tbl_hierarchical_rate_by_grade()] via
#' `x$custom_info` (standalone tables) or the first sub-table's `custom_info` (merged tables).
#' If no metadata is found, the function aborts with an informative error.
#'
#' @rdname tbl_hierarchical_rate_by_grade
#' @export
add_grade_column <- function(x) {
Comment on lines +495 to +496
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is added with tbl_hierarchical_rate_by_grade() if it must be called also after any merging?

set_cli_abort_call()

if (!inherits(x, "gtsummary")) {
cli::cli_abort(
"{.arg x} must be a {.cls gtsummary} object.",
call = get_cli_abort_call()
)
}

# idempotency guard: skip if already applied
if ("label_grade" %in% names(x$table_body)) {
return(x)
}

# extract metadata: standalone vs merged table
info <- x$custom_info %||%
Find(Negate(is.null), lapply(x$tbls, \(t) t$custom_info))

if (is.null(info)) {
cli::cli_abort(
c(
"No {.field custom_info} metadata found on the input table.",
"i" = "Ensure the table was created with {.fun tbl_hierarchical_rate_by_grade}."
),
call = get_cli_abort_call()
)
}

soc <- info$soc
ae <- info$ae
grade <- info$grade
grade_groups <- info$grade_groups
lvls <- info$lvls
label_list <- info$label_list

# apply visual formatting to the table body
x <- x |>
gtsummary::modify_table_body(
\(table_body) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider if these anonymous functions can be extracted from the functions, simplified, and reused across the package. Not to address it now but please open an issue

table_body |>
dplyr::rowwise() |>
# create label_grade column
dplyr::mutate(
label_grade = dplyr::case_when(
.data$variable == grade ~ .data$label,
.data$variable == ae | .data$label == "- Any adverse events -" ~ "- Any Grade -",
.default = ""
),
.after = "label"
) |>
# blank the label column for grade rows (safe after merge)
dplyr::mutate(label = if (.data$variable == grade) "" else .data$label) |>
# remove statistics from non-summary rows
dplyr::mutate(
across(
gtsummary::all_stat_cols(),
~ if (
.data$variable %in% c(ae, "..ard_hierarchical_overall..") |
.data$label_grade %in% c(lvls, names(grade_groups)) |
.data$label == "- Any adverse events -"
) {
.
} else {
NA
}
)
) |>
dplyr::ungroup()
}
) |>
# show and align label_grade column
gtsummary::modify_column_unhide("label_grade") |>
gtsummary::modify_column_alignment("label_grade", align = "left") |>
# remove default footnote
gtsummary::remove_footnote_header(columns = everything()) |>
# convert "0 (0.0%)" to "0"
gtsummary::modify_post_fmt_fun(
fmt_fun = ~ ifelse(. %in% c("0 (0.0%)", "0 (NA%)"), "0", .),
columns = gtsummary::all_stat_cols()
) |>
# update header labels
gtsummary::modify_header(
label ~ paste0(
label_list[[soc]], " \n",
paste0(rep("\U00A0", 4L), collapse = ""), label_list[[ae]]
),
label_grade ~ label_list[[grade]],
gtsummary::all_stat_cols() ~ "{level} \n(N = {n})"
)

# indent grade level labels within grade groups
if (!is_empty(grade_groups)) {
x <- x |>
gtsummary::modify_indent(
columns = "label_grade",
rows = .data$variable == grade & .data$label_grade %in% unlist(grade_groups),
indent = 4L
)
}

x
}
32 changes: 28 additions & 4 deletions man/tbl_hierarchical_rate_by_grade.Rd

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

17 changes: 17 additions & 0 deletions tests/testthat/_snaps/add_grade_column.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# add_grade_column() errors when custom_info is missing

Code
add_grade_column(tbl)
Condition
Error in `add_grade_column()`:
! No custom_info metadata found on the input table.
i Ensure the table was created with `tbl_hierarchical_rate_by_grade()`.

# add_grade_column() errors on non-gtsummary input

Code
add_grade_column(data.frame(x = 1))
Condition
Error in `add_grade_column()`:
! `x` must be a <gtsummary> object.

Loading
Loading