Repository: insightsengineering/cards
Pull request #424: Sort `ard_stack_hierarchical()` results by default
Author: login: edelarua
Labels: name: sme
Created: 2025-04-09T21:28:15Z | Merged: 2025-04-09T22:04:05Z

**What changes are proposed in this pull request?**
* Updated `ard_stack_hierarchical()` and `ard_stack_hierarchical_count()` to automatically sort results alphanumerically. (#423)

Required 2 additional small fixes to the sorting function:
- Including attributes as "overall results" so they are sorted last
- Accounting for ARDs by highest severity, which means the last hierarchy level is specified in `by` instead of `variables`

Closes #423 

--------------------------------------------------------------------------------

Pre-review Checklist (if item does not apply, mark is as complete)
- [ ] **All** GitHub Action workflows pass with a :white_check_mark:
- [ ] PR branch has pulled the most recent updates from master branch: `usethis::pr_merge_main()`
- [ ] If a bug was fixed, a unit test was added.
- [ ] Code coverage is suitable for any new functions/features (generally, 100% coverage for new code): `devtools::test_coverage()`
- [ ] Request a reviewer

Reviewer Checklist (if item does not apply, mark is as complete)

- [ ] If a bug was fixed, a unit test was added.
- [ ] Run `pkgdown::build_site()`. Check the R console for errors, and review the rendered website.
- [ ] Code coverage is suitable for any new functions/features: `devtools::test_coverage()`

When the branch is ready to be merged:
- [ ] Update `NEWS.md` with the changes from this pull request under the heading "`# cards (development version)`". If there is an issue associated with the pull request, reference it in parentheses at the end update (see `NEWS.md` for examples).
- [ ] **All** GitHub Action workflows pass with a :white_check_mark:
- [ ] Approve Pull Request
- [ ] Merge the PR. Please use "Squash and merge" or "Rebase and merge".

_Optional Reverse Dependency Checks_:

Install `checked` with `pak::pak("Genentech/checked")` or `pak::pak("checked")`

```shell
# Check dev versions of `cardx`, `gtsummary`, and `tfrmt` which are in the `ddsjoberg` R Universe
Rscript -e "options(checked.check_envvars = c(NOT_CRAN = TRUE)); checked::check_rev_deps(path = '.', n = parallel::detectCores() - 2L, repos = c('https://ddsjoberg.r-universe.dev', 'https://cloud.r-project.org'))"

# Check CRAN reverse dependencies but run tests skipped on CRAN
Rscript -e "options(checked.check_envvars = c(NOT_CRAN = TRUE)); checked::check_rev_deps(path = '.', n = parallel::detectCores() - 2, repos = 'https://cloud.r-project.org')"

# Check CRAN reverse dependencies in a CRAN-like environment
Rscript -e "options(checked.check_envvars = c(NOT_CRAN = FALSE), checked.check_build_args = '--as-cran'); checked::check_rev_deps(path = '.', n = parallel::detectCores() - 2, repos = 'https://cloud.r-project.org')"
```


Linked issue #423: Sort `ard_stack_hierarchical()` results alphanumerically by default
Update `ard_stack_hierarchical()` to automatically sort results alphanumerically.

Diff:
diff --git a/NEWS.md b/NEWS.md
index 839231291..9b8397f94 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -8,6 +8,8 @@
 
 * Fixed sorting order of logical variables in `nest_for_ard()`. (#411)
 
+* Updated `ard_stack_hierarchical()` and `ard_stack_hierarchical_count()` to automatically sort results alphanumerically. (#423)
+
 # cards 0.5.1
 
 * Small update to account for a change in R-devel.
diff --git a/R/ard_stack_hierarchical.R b/R/ard_stack_hierarchical.R
index d709d6a6b..b703ae26a 100644
--- a/R/ard_stack_hierarchical.R
+++ b/R/ard_stack_hierarchical.R
@@ -455,6 +455,9 @@ internal_stack_hierarchical <- function(data,
   # append attributes used for sorting/filtering -------------------------------
   attr(result, "args") <- list(by = by, variables = variables, include = include)
 
+  # sort ARD alphanumerically --------------------------------------------------
+  result <- result |> sort_ard_hierarchical(sort = "alphanumeric")
+
   # return final result --------------------------------------------------------
   result |> as_card()
 }
diff --git a/R/sort_ard_hierarchical.R b/R/sort_ard_hierarchical.R
index 6fb30592c..0b09bc133 100644
--- a/R/sort_ard_hierarchical.R
+++ b/R/sort_ard_hierarchical.R
@@ -71,6 +71,14 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
 
   x_args <- attributes(x)$args
   by_cols <- if (length(x_args$by) > 0) paste0("group", seq_along(length(x_args$by)), c("", "_level")) else NULL
+
+  # for calculations by highest severity, innermost variable is extracted from by
+  if (length(x_args$by) > 1) {
+    x_args$variables <- c(x_args$variables, x_args$by[-1])
+    x_args$include <- c(x_args$include, x_args$by[-1])
+    x_args$by <- x_args$by[-1]
+  }
+
   outer_cols <- if (length(x_args$variables) > 1) {
     x_args$variables |>
       utils::head(-1) |>
@@ -144,7 +152,7 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
 # this function reformats a hierarchical ARD for sorting
 .ard_reformat_sort <- function(x, sort, by, outer_cols) {
   # reformat data from overall column (if present)
-  is_overall_col <- apply(x, 1, function(x) !isTRUE(any(x %in% by)))
+  is_overall_col <- apply(x, 1, function(x) !isTRUE(any(x %in% by)) || x$context == "attributes")
   if (sum(is_overall_col) > 0 && length(by) > 0) {
     x_overall_col <- x[is_overall_col, ] |>
       cards::rename_ard_groups_shift(shift = length(by)) |>
@@ -184,7 +192,8 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
         dat |>
           dplyr::mutate(
             group1 = "..empty..",
-            variable = NA
+            group1_level = list(NA),
+            variable = NA,
           )
       } else {
         dat
diff --git a/tests/testthat/_snaps/ard_stack_hierarchical.md b/tests/testthat/_snaps/ard_stack_hierarchical.md
index 8ba274f54..868d2376b 100644
--- a/tests/testthat/_snaps/ard_stack_hierarchical.md
+++ b/tests/testthat/_snaps/ard_stack_hierarchical.md
@@ -115,22 +115,22 @@
     Output
          group1 group1_level group2 group2_level group3 group3_level variable variable_level stat_name stat_label stat
       1   AESEV         MILD   <NA>                <NA>                 AESOC      GENERAL …         n          n    4
-      2   AESEV         MILD   <NA>                <NA>                 AESOC      SKIN AND…         n          n    1
-      3   AESEV         MILD  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    2
+      2   AESEV     MODERATE   <NA>                <NA>                 AESOC      GENERAL …         n          n    0
+      3    <NA>                <NA>                <NA>                 AESOC      GENERAL …         n          n    4
       4   AESEV         MILD  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    2
-      5   AESEV         MILD  AESOC    SKIN AND…   <NA>               AEDECOD       ERYTHEMA         n          n    1
-      6   AESEV         MILD  AESOC    SKIN AND…   <NA>               AEDECOD      PRURITUS…         n          n    0
-      7   AESEV     MODERATE   <NA>                <NA>                 AESOC      GENERAL …         n          n    0
-      8   AESEV     MODERATE   <NA>                <NA>                 AESOC      SKIN AND…         n          n    1
-      9   AESEV     MODERATE  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    0
-      10  AESEV     MODERATE  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    0
-      11  AESEV     MODERATE  AESOC    SKIN AND…   <NA>               AEDECOD       ERYTHEMA         n          n    0
-      12  AESEV     MODERATE  AESOC    SKIN AND…   <NA>               AEDECOD      PRURITUS…         n          n    1
-      13   <NA>                <NA>                <NA>                 AESOC      GENERAL …         n          n    4
-      14   <NA>                <NA>                <NA>                 AESOC      SKIN AND…         n          n    2
-      15  AESOC    GENERAL …   <NA>                <NA>               AEDECOD      APPLICAT…         n          n    2
-      16  AESOC    GENERAL …   <NA>                <NA>               AEDECOD      APPLICAT…         n          n    2
-      17  AESOC    SKIN AND…   <NA>                <NA>               AEDECOD       ERYTHEMA         n          n    1
+      5   AESEV     MODERATE  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    0
+      6   AESOC    GENERAL …   <NA>                <NA>               AEDECOD      APPLICAT…         n          n    2
+      7   AESEV         MILD  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    2
+      8   AESEV     MODERATE  AESOC    GENERAL …   <NA>               AEDECOD      APPLICAT…         n          n    0
+      9   AESOC    GENERAL …   <NA>                <NA>               AEDECOD      APPLICAT…         n          n    2
+      10  AESEV         MILD   <NA>                <NA>                 AESOC      SKIN AND…         n          n    1
+      11  AESEV     MODERATE   <NA>                <NA>                 AESOC      SKIN AND…         n          n    1
+      12   <NA>                <NA>                <NA>                 AESOC      SKIN AND…         n          n    2
+      13  AESEV         MILD  AESOC    SKIN AND…   <NA>               AEDECOD       ERYTHEMA         n          n    1
+      14  AESEV     MODERATE  AESOC    SKIN AND…   <NA>               AEDECOD       ERYTHEMA         n          n    0
+      15  AESOC    SKIN AND…   <NA>                <NA>               AEDECOD       ERYTHEMA         n          n    1
+      16  AESEV         MILD  AESOC    SKIN AND…   <NA>               AEDECOD      PRURITUS…         n          n    0
+      17  AESEV     MODERATE  AESOC    SKIN AND…   <NA>               AEDECOD      PRURITUS…         n          n    1
       18  AESOC    SKIN AND…   <NA>                <NA>               AEDECOD      PRURITUS…         n          n    1
     Message
       i 4 more variables: context, fmt_fn, warning, error
diff --git a/tests/testthat/_snaps/filter_ard_hierarchical.md b/tests/testthat/_snaps/filter_ard_hierarchical.md
index 890e1579b..2ebf85be8 100644
--- a/tests/testthat/_snaps/filter_ard_hierarchical.md
+++ b/tests/testthat/_snaps/filter_ard_hierarchical.md
@@ -6,16 +6,16 @@
       {cards} data frame: 39 x 15
     Output
          group1 group1_level group2 group2_level group3 group3_level                     variable variable_level stat_name stat_label  stat
-      1    TRTA      Placebo   <NA>                <NA>                                       SEX              M         n          n    13
-      2    TRTA      Placebo   <NA>                <NA>                                       SEX              M         N          N    33
-      3    TRTA      Placebo   <NA>                <NA>                                       SEX              M         p          % 0.394
-      4    TRTA      Placebo   <NA>                <NA>              ..ard_hierarchical_overall..           TRUE         n          n    26
-      5    TRTA      Placebo   <NA>                <NA>              ..ard_hierarchical_overall..           TRUE         N          N    86
-      6    TRTA      Placebo   <NA>                <NA>              ..ard_hierarchical_overall..           TRUE         p          % 0.302
-      7    TRTA      Placebo    SEX            M   <NA>                                      RACE          WHITE         n          n    12
-      8    TRTA      Placebo    SEX            M   <NA>                                      RACE          WHITE         N          N    30
-      9    TRTA      Placebo    SEX            M   <NA>                                      RACE          WHITE         p          %   0.4
-      10   TRTA    Xanomeli…   <NA>                <NA>                                       SEX              M         n          n    24
+      1    <NA>                <NA>                <NA>                                      TRTA        Placebo         n          n    86
+      2    <NA>                <NA>                <NA>                                      TRTA        Placebo         N          N   254
+      3    <NA>                <NA>                <NA>                                      TRTA        Placebo         p          % 0.339
+      4    <NA>                <NA>                <NA>                                      TRTA      Xanomeli…         n          n    84
+      5    <NA>                <NA>                <NA>                                      TRTA      Xanomeli…         N          N   254
+      6    <NA>                <NA>                <NA>                                      TRTA      Xanomeli…         p          % 0.331
+      7    <NA>                <NA>                <NA>                                      TRTA      Xanomeli…         n          n    84
+      8    <NA>                <NA>                <NA>                                      TRTA      Xanomeli…         N          N   254
+      9    <NA>                <NA>                <NA>                                      TRTA      Xanomeli…         p          % 0.331
+      10   TRTA      Placebo   <NA>                <NA>              ..ard_hierarchical_overall..           TRUE         n          n    26
     Message
       i 29 more rows
       i Use `print(n = ...)` to see more rows
diff --git a/tests/testthat/test-ard_stack_hierarchical.R b/tests/testthat/test-ard_stack_hierarchical.R
index 222edd937..270832616 100644
--- a/tests/testthat/test-ard_stack_hierarchical.R
+++ b/tests/testthat/test-ard_stack_hierarchical.R
@@ -112,32 +112,40 @@ test_that("ard_stack_hierarchical(by)", {
   )
 
   # check AEDECOD match
+  ard_match <- ard_hierarchical(
+    ADAE_small,
+    variables = c(AESOC, AEDECOD),
+    by = TRTA,
+    id = USUBJID,
+    denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
+  ) |>
+    cards::tidy_ard_row_order()
+  attr(ard_match, "args") <- list(by = "TRTA", variables = c("AESOC", "AEDECOD"), include = c("AESOC", "AEDECOD"))
+
   expect_equal(
     ard |> dplyr::filter(!is.na(group2)),
-    ard_hierarchical(
-      ADAE_small,
-      variables = c(AESOC, AEDECOD),
-      by = TRTA,
-      id = USUBJID,
-      denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
-    ) |>
-      cards::tidy_ard_row_order(),
+    ard_match |>
+      sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE
   )
 
   # check AESOC match
+  ard_match <- ard_hierarchical(
+    ADAE_small |> dplyr::slice_tail(n = 1L, by = c("USUBJID", "TRTA", "AESOC")),
+    variables = AESOC,
+    by = TRTA,
+    id = USUBJID,
+    denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
+  ) |>
+    cards::tidy_ard_row_order()
+  attr(ard_match, "args") <- list(by = "TRTA", variables = c("AESOC"), include = c("AESOC"))
+
   expect_equal(
     ard |>
       dplyr::filter(variable %in% "AESOC") |>
       dplyr::select(-all_ard_group_n(2L)),
-    ard_hierarchical(
-      ADAE_small |> dplyr::slice_tail(n = 1L, by = c("USUBJID", "TRTA", "AESOC")),
-      variables = AESOC,
-      by = TRTA,
-      id = USUBJID,
-      denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
-    ) |>
-      cards::tidy_ard_row_order(),
+    ard_match |>
+      sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE
   )
 })
@@ -206,36 +214,44 @@ test_that("ard_stack_hierarchical(by) with columns not in `denominator`", {
   )
 
   # check the rates for AEDECOD are correct
+  ard_match <- ADAE_small |>
+    dplyr::arrange(USUBJID, TRTA, AESOC, AEDECOD, AESEV) |>
+    dplyr::filter(.by = c(USUBJID, TRTA, AESOC, AEDECOD), dplyr::n() == dplyr::row_number()) |>
+    ard_hierarchical(
+      variables = c(AESOC, AEDECOD),
+      by = c(TRTA, AESEV),
+      denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
+    ) |>
+    tidy_ard_row_order()
+  attr(ard_match, "args") <- list(by = "TRTA", variables = c("AESOC", "AEDECOD", "AESEV"), include = c("AESOC", "AEDECOD", "AESEV"))
+
   expect_equal(
     ard |>
       dplyr::filter(variable == "AEDECOD"),
-    ADAE_small |>
-      dplyr::arrange(USUBJID, TRTA, AESOC, AEDECOD, AESEV) |>
-      dplyr::filter(.by = c(USUBJID, TRTA, AESOC, AEDECOD), dplyr::n() == dplyr::row_number()) |>
-      ard_hierarchical(
-        variables = c(AESOC, AEDECOD),
-        by = c(TRTA, AESEV),
-        denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
-      ) |>
-      tidy_ard_row_order(),
+    ard_match |>
+      sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE,
     ignore_function_env = TRUE
   )
 
   # check the rates for AESOC are correct
+  ard_match <- ADAE_small |>
+    dplyr::arrange(USUBJID, TRTA, AESOC, AESEV) |>
+    dplyr::filter(.by = c(USUBJID, TRTA, AESOC), dplyr::n() == dplyr::row_number()) |>
+    ard_hierarchical(
+      variables = AESOC,
+      by = c(TRTA, AESEV),
+      denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
+    ) |>
+    tidy_ard_row_order()
+  attr(ard_match, "args") <- list(by = "TRTA", variables = c("AESOC", "AESEV"), include = c("AESOC", "AESEV"))
+
   expect_equal(
     ard |>
       dplyr::filter(variable == "AESOC") |>
       rename_ard_columns(),
-    ADAE_small |>
-      dplyr::arrange(USUBJID, TRTA, AESOC, AESEV) |>
-      dplyr::filter(.by = c(USUBJID, TRTA, AESOC), dplyr::n() == dplyr::row_number()) |>
-      ard_hierarchical(
-        variables = AESOC,
-        by = c(TRTA, AESEV),
-        denominator = ADSL |> dplyr::rename(TRTA = TRT01A)
-      ) |>
-      tidy_ard_row_order() |>
+    ard_match |>
+      sort_ard_hierarchical("alphanumeric") |>
       rename_ard_columns(),
     ignore_attr = TRUE,
     ignore_function_env = TRUE
@@ -306,16 +322,22 @@ test_that("ard_stack_hierarchical_count(variables)", {
   )
 
   # check AEDECOD match
+  ard_match <- ard_hierarchical_count(ADAE_small, variables = c(AESOC, AEDECOD))
+  attr(ard_match, "args") <- list(by = NULL, variables = c("AESOC", "AEDECOD"), include = c("AESOC", "AEDECOD"))
+
   expect_equal(
     ard |> dplyr::filter(!is.na(group1)),
-    ard_hierarchical_count(ADAE_small, variables = c(AESOC, AEDECOD)),
+    ard_match |> sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE
   )
 
   # check AESOC match
+  ard_match <- ard_hierarchical_count(ADAE_small, variables = AESOC)
+  attr(ard_match, "args") <- list(by = NULL, variables = "AESOC", include = "AESOC")
+
   expect_equal(
     ard |> dplyr::filter(is.na(group1)) |> dplyr::select(-all_ard_group_n(1L)),
-    ard_hierarchical_count(ADAE_small, variables = AESOC),
+    ard_match |> sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE
   )
 })
@@ -337,18 +359,24 @@ test_that("ard_stack_hierarchical_count(by)", {
   )
 
   # check AEDECOD match
+  ard_match <- ard_hierarchical_count(ADAE_small, variables = c(AESOC, AEDECOD), by = TRTA) |>
+    cards::tidy_ard_row_order()
+  attr(ard_match, "args") <- list(by = "TRTA", variables = c("AESOC", "AEDECOD"), include = c("AESOC", "AEDECOD"))
+
   expect_equal(
     ard |> dplyr::filter(!is.na(group2)),
-    ard_hierarchical_count(ADAE_small, variables = c(AESOC, AEDECOD), by = TRTA) |>
-      cards::tidy_ard_row_order(),
+    ard_match |> sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE
   )
 
   # check AESOC match
+  ard_match <- ard_hierarchical_count(ADAE_small, variables = AESOC, by = TRTA) |>
+    cards::tidy_ard_row_order()
+  attr(ard_match, "args") <- list(by = "TRTA", variables = "AESOC", include = "AESOC")
+
   expect_equal(
     ard |> dplyr::filter(is.na(group2)) |> dplyr::select(-all_ard_group_n(2L)),
-    ard_hierarchical_count(ADAE_small, variables = AESOC, by = TRTA) |>
-      cards::tidy_ard_row_order(),
+    ard_match |> sort_ard_hierarchical("alphanumeric"),
     ignore_attr = TRUE
   )
 })
@@ -550,7 +578,8 @@ test_that("ard_stack_hierarchical_count(attributes)", {
       dplyr::select(-all_missing_columns()),
     ADAE_small |>
       ard_attributes(variables = c(TRTA, AESOC, AEDECOD)) |>
-      dplyr::select(-all_missing_columns()),
+      dplyr::select(-all_missing_columns()) |>
+      dplyr::slice(c(5:6, 1:2, 3:4)),
     ignore_attr = TRUE
   )
 })


Reviews:
user: login: ddsjoberg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:41:46Z
user: login: ddsjoberg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:43:01Z
user: login: edelarua
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:45:52Z
user: login: ddsjoberg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:46:56Z
user: login: edelarua
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:47:05Z
user: login: ddsjoberg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:47:58Z
user: login: ddsjoberg
body: Thank you @edelarua !! This looks great!

FYI @bzkrouse the hierarchical ARDs are now sorted alphanumerically (which, TBH, is what I thought the default already was 😆 )
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:49:07Z

Review comments:
diff_hunk: @@ -71,6 +71,14 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
 
   x_args <- attributes(x)$args
   by_cols <- if (length(x_args$by) > 0) paste0("group", seq_along(length(x_args$by)), c("", "_level")) else NULL
+
+  # for calculations by highest severity, innermost variable is extracted from by
+  if (length(x_args$by) > 1) {
+    x_args$variables <- c(x_args$variables, x_args$by[-1])
+    x_args$include <- c(x_args$include, x_args$by[-1])
+    x_args$by <- x_args$by[-1]
+  }
path: R/sort_ard_hierarchical.R
user: login: ddsjoberg
body: Can you help me understand why this is needed? I thought we'd just add the sort function at the bottom and be done 😆 
created_at: 2025-04-09T21:41:46Z
author_association: COLLABORATOR
diff_hunk: @@ -184,7 +192,8 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
         dat |>
           dplyr::mutate(
             group1 = "..empty..",
-            variable = NA
+            group1_level = list(NA),
+            variable = NA,
path: R/sort_ard_hierarchical.R
user: login: ddsjoberg
body: Same here: can you help me understand what this change does?
created_at: 2025-04-09T21:43:01Z
author_association: COLLABORATOR
diff_hunk: @@ -71,6 +71,14 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
 
   x_args <- attributes(x)$args
   by_cols <- if (length(x_args$by) > 0) paste0("group", seq_along(length(x_args$by)), c("", "_level")) else NULL
+
+  # for calculations by highest severity, innermost variable is extracted from by
+  if (length(x_args$by) > 1) {
+    x_args$variables <- c(x_args$variables, x_args$by[-1])
+    x_args$include <- c(x_args$include, x_args$by[-1])
+    x_args$by <- x_args$by[-1]
+  }
path: R/sort_ard_hierarchical.R
user: login: edelarua
body: I think they're just minor bugs that we didn't encounter previously - this is to account for ARDs by highest severity, which means the last hierarchy level is specified in `by` instead of `variables` which affects the sorting.
created_at: 2025-04-09T21:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +71,14 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
 
   x_args <- attributes(x)$args
   by_cols <- if (length(x_args$by) > 0) paste0("group", seq_along(length(x_args$by)), c("", "_level")) else NULL
+
+  # for calculations by highest severity, innermost variable is extracted from by
+  if (length(x_args$by) > 1) {
+    x_args$variables <- c(x_args$variables, x_args$by[-1])
+    x_args$include <- c(x_args$include, x_args$by[-1])
+    x_args$by <- x_args$by[-1]
+  }
path: R/sort_ard_hierarchical.R
user: login: ddsjoberg
body: gotcha! thanks!
created_at: 2025-04-09T21:46:56Z
author_association: COLLABORATOR
diff_hunk: @@ -184,7 +192,8 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
         dat |>
           dplyr::mutate(
             group1 = "..empty..",
-            variable = NA
+            group1_level = list(NA),
+            variable = NA,
path: R/sort_ard_hierarchical.R
user: login: edelarua
body: This is so that when `unnest()` is applied, `total_n` and `attribute` rows are not removed and are sorted last.
created_at: 2025-04-09T21:47:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -184,7 +192,8 @@ sort_ard_hierarchical <- function(x, sort = c("descending", "alphanumeric")) {
         dat |>
           dplyr::mutate(
             group1 = "..empty..",
-            variable = NA
+            group1_level = list(NA),
+            variable = NA,
path: R/sort_ard_hierarchical.R
user: login: ddsjoberg
body: thank you for the clarification! 
created_at: 2025-04-09T21:47:58Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-09T21:32:37Z
body: ## Unit Tests Summary
  1 files  211 suites   1m 0s ⏱️
208 tests 208 ✅ 0 💤 0 ❌
542 runs  542 ✅ 0 💤 0 ❌

Results for commit a73824eb.

[test-results]:data:application/gzip;base64,H4sIAOHs9mcC/12Myw6DIBQFf8Ww7gLwCtKfMbxMSFUaHivTfy8SbWmXMydndjS7xUZ078itQzG7VIGSA00OMjm/FcFw4bKluuLxoilmrf/Vwz2L+jymWbrlR9gQfDhNyNvRHICecCVb8y1WboKV25726+pSASR5P1KwSrKBGsEFBiyI6hkwrkFJIByDNJqj1xvnLJG4BgEAAA==

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-09T21:32:40Z
body: 
## Unit Test Performance Difference

<details>
  <summary><b>Additional test case details</b></summary>

| Test Suite | $Status$ | Time on `main` | $±Time$ | Test Case |
|:-----|:----:|:----:|:----:|:-----|
| ard_categorical | 💚 | $11.90$ | $-1.06$ | ard_categorical_univariate |
</details>

Results for commit ae5dde398eb06ff6c9e6596daa9773f36c657551

♻️ This comment has been updated with latest results.

<!-- Sticky Pull Request Commentmarkdown-table -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-09T21:33:26Z
body: ![badge](https://raw.githubusercontent.com/insightsengineering/cards/_xml_coverage_reports/data/423-tbl_stack_hierarchical_auto_sort/./badge.svg)


## Code Coverage Summary

```
Filename                       Stmts    Miss  Cover    Missing
---------------------------  -------  ------  -------  -------------------------------
R/add_calculated_row.R            46       0  100.00%
R/apply_fmt_fn.R                 116       0  100.00%
R/ard_attributes.R                46       1  97.83%   57
R/ard_categorical.R              393      10  97.46%   248, 461-465, 472-473, 650, 684
R/ard_complex.R                   36       1  97.22%   82
R/ard_continuous.R               187       3  98.40%   208-209, 308
R/ard_dichotomous.R               80       1  98.75%   62
R/ard_formals.R                   13       0  100.00%
R/ard_hierarchical.R              71       0  100.00%
R/ard_missing.R                   58       1  98.28%   50
R/ard_pairwise.R                  46       0  100.00%
R/ard_stack_hierarchical.R       234       1  99.57%   452
R/ard_stack.R                     90       1  98.89%   117
R/ard_strata.R                    33       0  100.00%
R/ard_total_n.R                   10       0  100.00%
R/as_card_fn.R                     8       0  100.00%
R/as_card.R                        5       0  100.00%
R/as_nested_list.R                41       0  100.00%
R/bind_ard.R                      47      11  76.60%   74-85
R/cards-package.R                  1       1  0.00%    13
R/check_ard_structure.R           39       6  84.62%   31, 55-59
R/default_stat_labels.R           20       0  100.00%
R/deprecated.R                     6       6  0.00%    24-30
R/eval_capture_conditions.R       30       0  100.00%
R/filter_ard_hierarchical.R       85       0  100.00%
R/get_ard_statistics.R            16       0  100.00%
R/mock.R                         137       2  98.54%   116, 244
R/nest_for_ard.R                  71       1  98.59%   60
R/print_ard_conditions.R          83       0  100.00%
R/print.R                         80       0  100.00%
R/process_selectors.R            126       1  99.21%   337
R/rename_ard_groups.R             60       0  100.00%
R/replace_null_statistic.R        11       0  100.00%
R/round5.R                         1       0  100.00%
R/selectors.R                     23       0  100.00%
R/shift_ard_columns.R            102       1  99.02%   185
R/shuffle_ard.R                  129       0  100.00%
R/sort_ard_hierarchical.R        133       0  100.00%
R/summary_functions.R             25       1  96.00%   59
R/tidy_ard_order.R                34       0  100.00%
R/tidy_as_ard.R                   40       0  100.00%
R/unlist_ard_columns.R            28       0  100.00%
R/update_ard.R                    53       0  100.00%
R/utils.R                         24       0  100.00%
TOTAL                           2917      49  98.32%
```

### Diff against main

```
Filename                      Stmts    Miss  Cover
--------------------------  -------  ------  -------
R/ard_stack_hierarchical.R       +1       0  +0.00%
R/sort_ard_hierarchical.R        +5      -5  +3.91%
TOTAL                            +6      -5  +0.18%
```


Results for commit: a73824eba652d97904091b36467c4ba41704adc7


_Minimum allowed coverage is `80%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentcards//coverage.xml -->
author_association: CONTRIBUTOR