Skip to contents

Wrap a pre-summarised data frame into a tabular_spec ready for the verb chain. tabular() is the entry verb — it owns the data, titles, and footnotes slots; every downstream verb (cols(), headers(), sort_rows(), style(), paginate(), preset()) returns an updated spec for further chaining, terminating in emit() (write to file) or as_grid() (resolve without writing).

Usage

tabular(data, titles = NULL, footnotes = NULL)

Arguments

data

The display rows. <data.frame>: required. Pre-summarised wide-format data; tibbles, data.tables, and arrow tables are coerced via as.data.frame(). Factor columns are preserved (their levels drive sort_rows()).

Restriction: At least one column; column names must be unique. Zero rows is accepted (engine renders a "No data" stub). Interaction: The cards-format counterparts (cdisc_saf_demo_ard, cdisc_saf_aesocpt_ard) are NOT accepted directly; pipe through pivot_across() first.

titles

Page-title block, one element per row. <character> | NULL: default NULL. Each element renders on its own centred line; embedded \n wraps within that row. The backend collapses unused rows so the column-header band sits flush against the lowest used title.

Restriction: No NAs.

Each element supports glue-style {expr} interpolation: braces are evaluated as R code in the calling environment at build time, e.g. "N total = {sum(n)}". Double a brace ({{ or }}) for a literal one. An md() / html() element is passed through without interpolation.

# Canonical 3-line title block with BigN-qualified population.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
titles = c(
  "Table 14.3.1",
  "Adverse Events by System Organ Class and Preferred Term",
  "Safety Population"
)

footnotes

Page-footnote block, one element per row. <character> | NULL: default NULL. User-supplied prose rows only; the backend appends its own program-path / program-name / timestamp band below them at render time.

Restriction: No NAs.

Each element supports glue-style {expr} interpolation (see titles).

# Canonical 3-line footnote block.
footnotes = c(
  "Subjects are counted once per SOC and once per PT.",
  "Percentages based on N per treatment group.",
  "TEAE = treatment-emergent adverse event."
)

Value

A tabular_spec S7 object. Pipe it into cols(), headers(), sort_rows(), style(), paginate(), and preset() to build the display, then into emit() to render or as_grid() to resolve without writing.

Details

Pre-summarised input contract. data is one row per displayed row of the final table. tabular() does not aggregate, filter, weight, or generate subtotal rows — those happen upstream in cards, dplyr, or SAS. If the upstream is a long cards::ard_stack() ARD, pipe through pivot_across() first to land in the wide shape tabular() accepts.

Multi-line titles and footnotes by contract. Clinical tables routinely carry 2-4 title rows and 1-4 user footnote rows. Pass each row as one element of the character vector; the backend renders each element on its own line, collapsing unused rows so the column-header band sits flush against the lowest used title.

See also

Downstream build verbs: cols() / col_spec(), headers(), sort_rows(), style(), paginate(), preset().

Terminal verbs: emit() (write), as_grid() (resolve without I/O).

Input helper: pivot_across() (cards ARD -> wide).

Demo data: cdisc_saf_demo, cdisc_saf_aesocpt, cdisc_eff_resp, cdisc_saf_n, cdisc_eff_n.

Examples

# ---- Example 1: Adverse-event table by SOC and Preferred Term ----
#
# The regulatory work-horse layout: AE-by-SOC/PT with the
# canonical 3-line title block (table number, description,
# population qualifier with BigN drawn inline from `cdisc_saf_n`) and a
# two-line footnote block explaining the denominator. The
# downstream pipeline hides the hierarchy markers (`row_type`,
# `soc_n`, `n_total`) but keeps them in the data so `sort_rows()`
# can arrange SOCs and PTs in descending order of subject count.
# The dataset already ships `n_total` and `soc_n`; here we slice to
# the overall row plus the two highest-incidence SOCs to keep the
# preview compact.
ae <- cdisc_saf_aesocpt
keep_soc <- head(unique(ae$soc[ae$row_type == "soc"]), 2L)
ae <- ae[ae$row_type == "overall" | ae$soc %in% keep_soc, ]
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)

tabular(
  ae,
  titles = c(
    "Table 14.3.1",
    "Adverse Events by System Organ Class and Preferred Term",
    "Safety Population"
  ),
  footnotes = c(
    "Subjects are counted once per SOC and once per PT.",
    "Percentages based on N per treatment group."
  )
) |>
  cols(
    label    = col_spec(label = "SOC / PT", indent = "indent_level"),
    soc      = col_spec(visible = FALSE),
    soc_n    = col_spec(visible = FALSE),
    row_type = 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 / PTPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Total
N=254
TOTAL SUBJECTS WITH AN EVENT52 (60.5)81 (84.4)66 (91.7)199 (78.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS19 (22.1)36 (37.5)35 (48.6)90 (35.4)
PRURITUS8 (9.3)21 (21.9)25 (34.7)54 (21.3)
ERYTHEMA8 (9.3)14 (14.6)14 (19.4)36 (14.2)
RASH5 (5.8)13 (13.5)8 (11.1)26 (10.2)
HYPERHIDROSIS2 (2.3)4 (4.2)8 (11.1)14 (5.5)
SKIN IRRITATION3 (3.5)6 (6.2)5 (6.9)14 (5.5)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS15 (17.4)36 (37.5)30 (41.7)81 (31.9)
APPLICATION SITE PRURITUS6 (7.0)23 (24.0)21 (29.2)50 (19.7)
APPLICATION SITE ERYTHEMA3 (3.5)13 (13.5)14 (19.4)30 (11.8)
APPLICATION SITE DERMATITIS5 (5.8)9 (9.4)7 (9.7)21 (8.3)
APPLICATION SITE IRRITATION3 (3.5)9 (9.4)9 (12.5)21 (8.3)
APPLICATION SITE VESICLES1 (1.2)5 (5.2)5 (6.9)11 (4.3)

Subjects are counted once per SOC and once per PT.

Percentages based on N per treatment group.

# ---- Example 2: Best overall response with CDISC factor ordering ---- # # Efficacy table where response categories must appear in CDISC # clinical order (CR < PR < SD < NON-CR/NON-PD < PD < NE < # MISSING), then the derived ORR / CBR / DCR rate rows, not # alphabetical. `groupid` keeps the four sections ordered while the # `stat_label` factor orders the response block; `sort_rows()` does # both in one pass. `groupid` / `group_label` ride along hidden. 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

 

ResponsePlacebo
N=86
Drug 50
N=84
Drug 100
N=84
CR1 (1.2)1 (1.2)1 (1.2)
PR1 (1.2)00
SD1 (1.2)00
NON-CR/NON-PD001 (1.2)
PD001 (1.2)
NE01 (1.2)0
MISSING83 (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: Minimal three-line BigN table from cdisc_saf_n ---- # # The smallest viable `tabular()` call: the bundled `cdisc_saf_n` 4-row # BigN table, a single-line title, no footnotes. The default # `col_spec` per column kicks in, giving sensible labels (the # data frame's column names) and left-aligned text. Useful when # teaching the core API shape without the clinical-context # surface noise. tabular(cdisc_saf_n, titles = "Safety-population BigN per arm")

 

Safety-population BigN per arm

 

armarm_shortn
Placeboplacebo86
Xanomeline Low Dosedrug_5096
Xanomeline High Dosedrug_10072
TotalTotal254
# ---- Example 4: Nested vital-signs panel — two group levels ---- # # The canonical by-visit vitals shape: each `param` nests its # `visit` blocks, and each visit nests the statistic rows. Two # columns carry `usage = "group"` (`param` then `visit`), so the # engine renders two levels of nested section headers above the # `stat_label` stub. The CDISC `paramcd` rides along as the natural # sort key but hides at render via `col_spec(visible = FALSE)`. # Sliced to the two blood-pressure parameters for a compact preview; # the full 4-parameter frame nests the same way. n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short) vs <- cdisc_saf_vital[cdisc_saf_vital$paramcd %in% c("DIABP", "SYSBP"), ] tabular( vs, titles = c( "Table 14.4.1", "Summary of Vital Signs", "Safety Population" ), footnotes = "Statistics computed on observed cases." ) |> cols( paramcd = col_spec(visible = FALSE), param = col_spec(usage = "group", label = "Parameter"), visit = col_spec(usage = "group", label = "Visit"), stat_label = col_spec(label = "Statistic"), placebo = col_spec( label = "Placebo\nN={n['placebo']}", align = "decimal" ), drug_50 = col_spec( label = "Drug 50\nN={n['drug_50']}", align = "decimal" ), drug_100 = col_spec( label = "Drug 100\nN={n['drug_100']}", align = "decimal" ) )

 

Table 14.4.1

Summary of Vital Signs

Safety Population

 

StatisticPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Diastolic Blood Pressure (mmHg)
Baseline
n340         384         288         
Mean (SD) 77.1 (10.7) 76.6 (9.8)  78.2 (10.3)
Median 77.7        76.7        78.8       
Min, Max 40  , 110   48  , 108   51  , 108  
 
Week 8
n292         240         224         
Mean (SD) 75.2 (9.1)  75.4 (10.6) 77.4 (9.1) 
Median 76.0        74.0        78.3       
Min, Max 49  , 101   52  , 100   54  , 98   
 
Week 16
n272         168         148         
Mean (SD) 75.1 (10.9) 75.2 (10.0) 76.0 (9.0) 
Median 76.0        75.7        77.3       
Min, Max 49  , 98    55  , 98    50  , 92   
 
End of Treatment
n222         177         168         
Mean (SD) 74.4 (10.7) 76.0 (11.2) 76.0 (9.9) 
Median 73.5        76.0        78.0       
Min, Max 49  , 104   50  , 100   56  , 98   
 
Systolic Blood Pressure (mmHg)
Baseline
n340         384         288         
Mean (SD)136.8 (17.6)137.9 (18.5)137.8 (17.2)
Median136.3       138.0       138.0       
Min, Max 80  , 184  100  , 194  100  , 192  
 
Week 8
n292         240         224         
Mean (SD)136.3 (17.0)134.9 (17.8)135.1 (15.5)
Median136.5       132.3       134.0       
Min, Max 90  , 189   92  , 200   91  , 198  
 
Week 16
n272         168         148         
Mean (SD)134.6 (18.3)132.5 (14.3)133.7 (16.0)
Median134.0       130.0       132.0       
Min, Max 76  , 190  100  , 168   99  , 186  
End of Treatment
n222         177         168         
Mean (SD)132.7 (15.4)133.0 (17.1)132.3 (15.6)
Median131.0       130.0       131.0       
Min, Max 78  , 172   92  , 178  100  , 177  

Statistics computed on observed cases.