Skip to content

Commit 0b93824

Browse files
authored
Merge pull request #2069 from rstudio/fix-stub-helper
fix: ensure `stub()` helper works with multi-column stubs
2 parents 35c6c6a + 82bb635 commit 0b93824

File tree

11 files changed

+534
-15
lines changed

11 files changed

+534
-15
lines changed

R/dt_stub_df.R

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,24 @@ dt_stub_df_init <- function(
7474
data <- dt_boxhead_set_stub(data = data, var = col)
7575
}
7676

77+
# Reorder the boxhead to match the order specified in rowname_col
78+
# This ensures stub columns appear in the order the user specified
79+
dt_boxhead <- dt_boxhead_get(data = data)
80+
81+
# Split boxhead into stub and non-stub rows
82+
stub_rows <- dt_boxhead[dt_boxhead$var %in% rowname_col, ]
83+
non_stub_rows <- dt_boxhead[!dt_boxhead$var %in% rowname_col, ]
84+
85+
# Reorder stub rows to match rowname_col order
86+
stub_rows <- stub_rows[match(rowname_col, stub_rows$var), ]
87+
88+
# Combine: stub rows first (in specified order), then non-stub rows
89+
dt_boxhead <- rbind(stub_rows, non_stub_rows)
90+
rownames(dt_boxhead) <- NULL
91+
92+
# Update the boxhead
93+
data <- dt_boxhead_set(data = data, boxh = dt_boxhead)
94+
7795
# Use the rightmost column as the primary row ID
7896
rightmost_col <- rowname_col[length(rowname_col)]
7997
rownames <- data_tbl[[rightmost_col]]

R/helpers.R

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,14 @@ adjust_luminance <- function(
999999
#' operations, the `stub()` select helper can be used. This obviates the need
10001000
#' to use the name of the column that was selected as the stub column.
10011001
#'
1002+
#' @param column *Stub column selection*
1003+
#'
1004+
#' `scalar<integer>` // *default:* `NULL` (optional)
1005+
#'
1006+
#' For tables with multi-column stubs, optionally specify which stub column to
1007+
#' target. Use `1` for the rightmost stub column, `2` for the second from
1008+
#' right, etc. If `NULL` (the default), all stub columns are selected.
1009+
#'
10021010
#' @return A character vector of class `"stub_column"`.
10031011
#'
10041012
#' @section Examples:
@@ -1031,6 +1039,40 @@ adjust_luminance <- function(
10311039
#' `r man_get_image_tag(file = "man_stub_1.png")`
10321040
#' }}
10331041
#'
1042+
#' For multi-column stubs, you can set widths for all stub columns together or
1043+
#' target specific columns individually:
1044+
#'
1045+
#' ```r
1046+
#' exibble |>
1047+
#' dplyr::select(num, char, row, group) |>
1048+
#' gt(rowname_col = c("group", "row")) |>
1049+
#' cols_width(
1050+
#' stub() ~ px(200), # All stub columns get 200px
1051+
#' everything() ~ px(100)
1052+
#' )
1053+
#' ```
1054+
#'
1055+
#' \if{html}{\out{
1056+
#' `r man_get_image_tag(file = "man_stub_2.png")`
1057+
#' }}
1058+
#'
1059+
#' Or target specific stub columns (1 = rightmost, 2 = second from right):
1060+
#'
1061+
#' ```r
1062+
#' exibble |>
1063+
#' dplyr::select(num, char, row, group) |>
1064+
#' gt(rowname_col = c("group", "row")) |>
1065+
#' cols_width(
1066+
#' stub(1) ~ px(70), # Rightmost stub column (row)
1067+
#' stub(2) ~ px(200), # Second stub column (group)
1068+
#' everything() ~ px(100)
1069+
#' )
1070+
#' ```
1071+
#'
1072+
#' \if{html}{\out{
1073+
#' `r man_get_image_tag(file = "man_stub_3.png")`
1074+
#' }}
1075+
#'
10341076
#' @family helper functions
10351077
#' @section Function ID:
10361078
#' 8-10
@@ -1039,9 +1081,10 @@ adjust_luminance <- function(
10391081
#' `v0.8.0` (November 16, 2022)
10401082
#'
10411083
#' @export
1042-
stub <- function() {
1084+
stub <- function(column = NULL) {
10431085
x <- "::stub::"
10441086
class(x) <- "stub_column"
1087+
attr(x, "column") <- column
10451088
x
10461089
}
10471090

R/resolver.R

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -429,14 +429,48 @@ resolve_cols_i <- function(
429429
# If we use the gt-specific select helper `stub()` then we
430430
# will retrieve the stub var name and return the output in the
431431
# same format as the return value for `tidyselect::eval_select()`
432-
if (rlang::as_label(quo) %in% c("stub()", "gt::stub()")) {
432+
# Check if the expression is a stub() call (with or without arguments)
433+
stub_label <- rlang::as_label(quo)
434+
if (grepl("^(gt::)?stub\\(", stub_label)) {
433435

434436
stub_var <- dt_boxhead_get_var_stub(data = data)
435437

436-
if (!is.null(stub_var)) {
437-
stub_col <- 1
438-
names(stub_col) <- stub_var
439-
return(stub_col)
438+
if (!is.null(stub_var) && !all(is.na(stub_var))) {
439+
440+
# Evaluate the stub() expression to get the column parameter if any
441+
stub_result <- rlang::eval_tidy(quo)
442+
column_idx <- attr(stub_result, "column")
443+
444+
# If column index is specified, select that specific stub column
445+
# (1 = rightmost, 2 = second from right, etc.)
446+
if (!is.null(column_idx)) {
447+
# Validate the column index
448+
if (!is.numeric(column_idx) || length(column_idx) != 1 || column_idx < 1) {
449+
cli::cli_abort(
450+
"The {.arg column} argument in {.fn stub} must be a single positive integer."
451+
)
452+
}
453+
454+
# Check if the requested column exists
455+
if (column_idx > length(stub_var)) {
456+
cli::cli_abort(c(
457+
"Cannot select stub column {column_idx}.",
458+
"i" = "This table has {length(stub_var)} stub column{?s}."
459+
))
460+
}
461+
462+
# Select from right to left (1 = rightmost)
463+
selected_stub_var <- stub_var[length(stub_var) - column_idx + 1]
464+
stub_col <- 1
465+
names(stub_col) <- selected_stub_var
466+
return(stub_col)
467+
468+
} else {
469+
# No column specified, return all stub columns
470+
stub_col <- seq_along(stub_var)
471+
names(stub_col) <- stub_var
472+
return(stub_col)
473+
}
440474
} else {
441475
return(NULL)
442476
}

R/utils_render_html.R

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,15 +2492,21 @@ calculate_hierarchical_stub_rowspans <- function(data) {
24922492
# Check if current row should continue the span from previous row
24932493
should_continue_span <- TRUE
24942494

2495-
# Must match current column value
2496-
if (col_values[row_idx] != col_values[row_idx - 1]) {
2495+
# Must match current column value (handle NAs properly)
2496+
curr_val <- col_values[row_idx]
2497+
prev_val <- col_values[row_idx - 1]
2498+
2499+
# Two values match if they're both identical or both NA
2500+
if (!identical(curr_val, prev_val)) {
24972501
should_continue_span <- FALSE
24982502
}
24992503

25002504
# Must match all values in columns to the left
25012505
if (col_idx > 1) {
25022506
for (left_col_idx in 1:(col_idx - 1)) {
2503-
if (stub_matrix[row_idx, left_col_idx] != stub_matrix[row_idx - 1, left_col_idx]) {
2507+
curr_left <- stub_matrix[row_idx, left_col_idx]
2508+
prev_left <- stub_matrix[row_idx - 1, left_col_idx]
2509+
if (!identical(curr_left, prev_left)) {
25042510
should_continue_span <- FALSE
25052511
break
25062512
}

R/utils_render_latex.R

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -776,15 +776,20 @@ create_body_component_l <- function(data, colwidth_df) {
776776
for (row_idx in 2:n_rows) {
777777
should_hide <- TRUE
778778

779-
# Check if current value matches previous value
780-
if (stub_matrix[row_idx, col_idx] != stub_matrix[row_idx - 1, col_idx]) {
779+
# Check if current value matches previous value (handle NAs properly)
780+
curr_val <- stub_matrix[row_idx, col_idx]
781+
prev_val <- stub_matrix[row_idx - 1, col_idx]
782+
783+
if (!identical(curr_val, prev_val)) {
781784
should_hide <- FALSE
782785
}
783786

784787
# Also check that all columns to the left match
785788
if (should_hide && col_idx > 1) {
786789
for (left_col_idx in 1:(col_idx - 1)) {
787-
if (stub_matrix[row_idx, left_col_idx] != stub_matrix[row_idx - 1, left_col_idx]) {
790+
curr_left <- stub_matrix[row_idx, left_col_idx]
791+
prev_left <- stub_matrix[row_idx - 1, left_col_idx]
792+
if (!identical(curr_left, prev_left)) {
788793
should_hide <- FALSE
789794
break
790795
}

images/man_stub_2.png

109 KB
Loading

images/man_stub_3.png

108 KB
Loading

man/stub.Rd

Lines changed: 42 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-cols_align.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,9 @@ test_that("The stub gets its alignment set properly with `cols_align()`", {
247247
c("stub", "vals")
248248
)
249249

250-
# Expect an error if using `stub()` when there is no stub
251-
expect_error(
250+
# When using `stub()` on a table with no stub, it should not error
251+
# (consistent with tidyselect behavior where selecting nothing is valid)
252+
expect_no_error(
252253
gt(tbl) |>
253254
cols_align(align = "center", columns = stub())
254255
)

0 commit comments

Comments
 (0)