Attach a sort_spec to a tabular_spec. The engine applies the
sort before pagination, so by may reference any column in
spec@data whether or not the column is declared in cols().
Usage
sort_rows(.spec, by = character(), descending = FALSE)Arguments
- .spec
The
tabular_specto attach the sort to.<tabular_spec>: required.- by
Ordered column names to sort by, in precedence order.
<character>: default character(). Length 0 is accepted (no-op sort). May reference columns not declared incols()— sort-only helper columns ride along through the engine.Restriction: Every entry must be a column in
spec@data. Cannot reference arm columns produced bypivot_across(); pivot upstream of the sort instead. Arm cells hold rendered stat strings (e.g."75.2 (8.3)") that do not order meaningfully.- descending
Per-key sort direction.
<logical(1) | logical(length(by))>: default FALSE.TRUEsorts the corresponding key descending; length 1 recycles to every key.Restriction: No NAs. Length must be 1 or
length(by). Tip: For mixed-direction multi-key sorts, passlength(by)values; the engine inverts thextfrmrank of each descending key and callsorder()once on all keys.
Value
The updated tabular_spec. Continue chaining with
style(), paginate(), preset(), then render via
emit() (or resolve without I/O via as_grid()).
Details
Replace, not stack. A second sort_rows() call REPLACES the
prior sort — sort is a single spec, not a stackable list. Call
with no arguments to clear.
NA last, regardless of direction. NA values in a sort key are
placed at the end whether the key is ascending or descending
(matching order(..., na.last = TRUE)).
Factor levels drive the order. Factor columns sort by factor
levels, not by the character label. The CDISC BOR ordering
(CR < PR < SD < NON-CR/NON-PD < PD < NE < MISSING) survives a
tabular pipeline without an explicit mutate() — coerce
stat_label to a factor with the levels in clinical order
upstream, then sort_rows(by = "stat_label") does the rest.
See also
Sibling build verbs: cols() / col_spec(),
headers(), style(), paginate(), preset().
Examples
# ---- Example 1: AE table clustered by SOC, PTs nested by count ----
#
# AE-by-SOC/PT table sorted so each SOC is followed immediately by
# its own preferred terms, SOC clusters in descending subject-count
# order. The sort runs on the bundled numeric helpers `soc_n` and
# `n_total`, not the formatted `Total` text, which would sort
# lexically ("14" < "171" < "29").
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_aesocpt,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = "Subjects are counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
sort_rows(by = c("soc_n", "n_total"), descending = c(TRUE, TRUE))
Table 14.3.1
Adverse Events by System Organ Class and Preferred Term
Safety Population
SOC / PT Placebo
N=86 Drug 50
N=96 Drug 100
N=72 Total
N=254 TOTAL SUBJECTS WITH AN EVENT 52 (60.5) 81 (84.4) 66 (91.7) 199 (78.3) SKIN AND SUBCUTANEOUS TISSUE DISORDERS 19 (22.1) 36 (37.5) 35 (48.6) 90 (35.4) PRURITUS 8 (9.3) 21 (21.9) 25 (34.7) 54 (21.3) ERYTHEMA 8 (9.3) 14 (14.6) 14 (19.4) 36 (14.2) RASH 5 (5.8) 13 (13.5) 8 (11.1) 26 (10.2) HYPERHIDROSIS 2 (2.3) 4 (4.2) 8 (11.1) 14 (5.5) SKIN IRRITATION 3 (3.5) 6 (6.2) 5 (6.9) 14 (5.5) GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS 15 (17.4) 36 (37.5) 30 (41.7) 81 (31.9) APPLICATION SITE PRURITUS 6 (7.0) 23 (24.0) 21 (29.2) 50 (19.7) APPLICATION SITE ERYTHEMA 3 (3.5) 13 (13.5) 14 (19.4) 30 (11.8) APPLICATION SITE DERMATITIS 5 (5.8) 9 (9.4) 7 (9.7) 21 (8.3) APPLICATION SITE IRRITATION 3 (3.5) 9 (9.4) 9 (12.5) 21 (8.3) APPLICATION SITE VESICLES 1 (1.2) 5 (5.2) 5 (6.9) 11 (4.3) GASTROINTESTINAL DISORDERS 13 (15.1) 12 (12.5) 17 (23.6) 42 (16.5) DIARRHOEA 9 (10.5) 5 (5.2) 3 (4.2) 17 (6.7) VOMITING 3 (3.5) 4 (4.2) 6 (8.3) 13 (5.1) NAUSEA 3 (3.5) 3 (3.1) 6 (8.3) 12 (4.7) ABDOMINAL PAIN 1 (1.2) 3 (3.1) 1 (1.4) 5 (2.0) SALIVARY HYPERSECRETION 0 (0.0) 0 (0.0) 4 (5.6) 4 (1.6) NERVOUS SYSTEM DISORDERS 6 (7.0) 18 (18.8) 17 (23.6) 41 (16.1) DIZZINESS 2 (2.3) 9 (9.4) 10 (13.9) 21 (8.3) HEADACHE 3 (3.5) 3 (3.1) 5 (6.9) 11 (4.3) SYNCOPE 0 (0.0) 5 (5.2) 2 (2.8) 7 (2.8) SOMNOLENCE 2 (2.3) 3 (3.1) 1 (1.4) 6 (2.4) TRANSIENT ISCHAEMIC ATTACK 0 (0.0) 2 (2.1) 1 (1.4) 3 (1.2) CARDIAC DISORDERS 7 (8.1) 12 (12.5) 14 (19.4) 33 (13.0) SINUS BRADYCARDIA 2 (2.3) 7 (7.3) 8 (11.1) 17 (6.7) MYOCARDIAL INFARCTION 4 (4.7) 2 (2.1) 4 (5.6) 10 (3.9) ATRIAL FIBRILLATION 1 (1.2) 2 (2.1) 2 (2.8) 5 (2.0) SUPRAVENTRICULAR EXTRASYSTOLES 1 (1.2) 1 (1.0) 1 (1.4) 3 (1.2) VENTRICULAR EXTRASYSTOLES 0 (0.0) 2 (2.1) 1 (1.4) 3 (1.2) INFECTIONS AND INFESTATIONS 12 (14.0) 6 (6.2) 11 (15.3) 29 (11.4) NASOPHARYNGITIS 2 (2.3) 4 (4.2) 6 (8.3) 12 (4.7) UPPER RESPIRATORY TRACT INFECTION 6 (7.0) 1 (1.0) 3 (4.2) 10 (3.9) INFLUENZA 1 (1.2) 1 (1.0) 1 (1.4) 3 (1.2) URINARY TRACT INFECTION 2 (2.3) 0 (0.0) 1 (1.4) 3 (1.2) CYSTITIS 1 (1.2) 0 (0.0) 1 (1.4) 2 (0.8) RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS 5 (5.8) 8 (8.3) 9 (12.5) 22 (8.7) COUGH 1 (1.2) 5 (5.2) 5 (6.9) 11 (4.3) NASAL CONGESTION 3 (3.5) 1 (1.0) 3 (4.2) 7 (2.8) DYSPNOEA 1 (1.2) 1 (1.0) 1 (1.4) 3 (1.2) EPISTAXIS 0 (0.0) 1 (1.0) 2 (2.8) 3 (1.2) PHARYNGOLARYNGEAL PAIN 0 (0.0) 1 (1.0) 1 (1.4) 2 (0.8) PSYCHIATRIC DISORDERS 7 (8.1) 9 (9.4) 3 (4.2) 19 (7.5) CONFUSIONAL STATE 2 (2.3) 3 (3.1) 1 (1.4) 6 (2.4) AGITATION 2 (2.3) 3 (3.1) 0 (0.0) 5 (2.0) INSOMNIA 2 (2.3) 0 (0.0) 2 (2.8) 4 (1.6) ANXIETY 0 (0.0) 3 (3.1) 0 (0.0) 3 (1.2) DELUSION 1 (1.2) 0 (0.0) 1 (1.4) 2 (0.8) MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS 3 (3.5) 6 (6.2) 5 (6.9) 14 (5.5) BACK PAIN 1 (1.2) 1 (1.0) 3 (4.2) 5 (2.0) ARTHRALGIA 1 (1.2) 2 (2.1) 1 (1.4) 4 (1.6) SHOULDER PAIN 1 (1.2) 2 (2.1) 0 (0.0) 3 (1.2) MUSCLE SPASMS 0 (0.0) 1 (1.0) 1 (1.4) 2 (0.8) ARTHRITIS 0 (0.0) 0 (0.0) 1 (1.4) 1 (0.4) INVESTIGATIONS 5 (5.8) 4 (4.2) 3 (4.2) 12 (4.7) ELECTROCARDIOGRAM ST SEGMENT DEPRESSION 4 (4.7) 1 (1.0) 0 (0.0) 5 (2.0) ELECTROCARDIOGRAM T WAVE INVERSION 2 (2.3) 1 (1.0) 1 (1.4) 4 (1.6) BLOOD GLUCOSE INCREASED 0 (0.0) 1 (1.0) 1 (1.4) 2 (0.8) ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED 1 (1.2) 1 (1.0) 0 (0.0) 2 (0.8) BIOPSY 0 (0.0) 0 (0.0) 1 (1.4) 1 (0.4)
Subjects are counted once per SOC and once per PT.
# ---- Example 2: BOR table in CDISC factor order ----
#
# Efficacy BOR table that must appear in CDISC clinical order
# (CR < PR < SD < NON-CR/NON-PD < PD < NE < MISSING), then the
# derived ORR / CBR / DCR rate rows ordered by `groupid`,
# not alphabetical. `cdisc_eff_resp$stat_label` arrives as character, so
# coerce to a factor with the canonical levels upstream and
# `sort_rows()` uses those levels directly.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}")
) |>
sort_rows(by = c("groupid", "stat_label"))
Table 14.2.1
Best Overall Response and Response Rates
Efficacy Evaluable Population
Response Placebo
N=86 Drug 50
N=84 Drug 100
N=84 CR 1 (1.2) 1 (1.2) 1 (1.2) PR 1 (1.2) 0 0 SD 1 (1.2) 0 0 NON-CR/NON-PD 0 0 1 (1.2) PD 0 0 1 (1.2) NE 0 1 (1.2) 0 MISSING 83 (96.5) 82 (97.6) 81 (96.4) ORR (CR + PR) 2 (2.3) 1 (1.2) 1 (1.2) 95% CI (Clopper-Pearson) (0.3, 8.1) (0.0, 6.5) (0.0, 6.5) CBR (CR + PR + SD) 3 (3.5) 1 (1.2) 1 (1.2) 95% CI (Clopper-Pearson) (0.7, 9.9) (0.0, 6.5) (0.0, 6.5) DCR (CR + PR + SD + NON-CR/NON-PD) 3 (3.5) 1 (1.2) 2 (2.4) 95% CI (Clopper-Pearson) (0.7, 9.9) (0.0, 6.5) (0.3, 8.3)
Response per RECIST 1.1, investigator assessment.
# ---- Example 3: Mixed-direction multi-key sort with hidden helper ----
#
# Demographics-style table sorted by `variable` ascending and a
# hidden numeric key descending. The `descending` argument takes
# one value per `by` entry so each key can flip direction
# independently. The helper column rides in `spec@data` for the
# sort but never renders (visible = FALSE on its col_spec).
demo <- cdisc_saf_demo
demo$display_order <- match(demo$variable, unique(demo$variable))
tabular(demo, titles = "Demographics, ranked within section") |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
display_order = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
sort_rows(
by = c("display_order", "stat_label"),
descending = c(FALSE, TRUE)
)
Demographics, ranked within section
Statistic Placebo Drug 50 Drug 100 Total Age (years) n 86 96 72 254 Q1, Q3 69.2, 81.8 71.0, 82.0 70.5, 79.0 70.0, 81.0 Min, Max 52 , 89 51 , 88 56 , 88 51 , 89 Median 76.0 78.0 75.5 77.0 Mean (SD) 75.2 (8.59) 76.0 (8.11) 73.8 (7.94) 75.1 (8.25) Sex, n (%) M 33 (38.4) 41 (42.7) 37 (51.4) 111 (43.7) F 53 (61.6) 55 (57.3) 35 (48.6) 143 (56.3) Race, n (%) WHITE 78 (90.7) 90 (93.8) 62 (86.1) 230 (90.6) BLACK OR AFRICAN AMERICAN 8 ( 9.3) 6 ( 6.2) 9 (12.5) 23 ( 9.1) ASIAN 0 0 0 0 AMERICAN INDIAN OR ALASKA NATIVE 0 0 1 ( 1.4) 1 ( 0.4)