This article is about shape: which column does what, multi-level headers, and how a table that is too long or too wide is split across pages. It assumes you already have a wide frame (see Data in) and does not cover cosmetics (see Presentation).
Row grouping: group_rows()
Row structure is a fact about the whole table, so it is declared once with group_rows() — not per column. by names only the structural grouping key columns (section headers and hidden break keys), ordered outer to inner. The visible row-label column (the statistic stub) is an ordinary cols() column, not a grouping key — it is indented automatically. display (a single value) picks how the keys render:
display
Use it for
Behaviour
"section"(default)
section variable (e.g. parameter)
each value becomes a section-header row; the key column is hidden
"collapse"
a visible row label
column stays; repeated values are suppressed
"repeat"
a visible row label
column stays; every row repeats the value
A break-only key — hidden, contributing only group transitions (the blank spacer and decimal-section reset) — is not a display mode: mark the key col_spec(visible = FALSE) and list it in by.
cols() handles the per-column cosmetics — labels (x = "Label" is shorthand for col_spec(label = )), alignment, widths, and .hide = for helper columns. Indentation is the separate col_spec(indent = …) argument (a fixed integer level, or a column name for per-row depth).
cols_apply() attaches one shared col_spec to all the arm columns at once — use it instead of repeating cols(placebo = …, drug_50 = …) for a variable number of arms.
Indent from exactly one source.display = "section" already indents its child rows one level, so the stub column (here stat_label) needs noindent — the section supplies it. (An explicit indent on the host overrides that auto-indent rather than stacking, so indent = 1 there still yields a single level.) The same care applies to labels from pivot_across(), which come out with a leading indent baked into the string: keep them as-is or trimws() them and set indent yourself — don’t double up.
Display modes and spacing
The default display = "section" is the submission shape. For a listing, display = "collapse" keeps the keys as visible columns and suppresses the repeats, so only the first row of each run carries the label — swap in "repeat" when every row must be self-describing (an export or QC view):
skip places the blank spacer rows between groups and follows the readr::read_csv(col_names = ) pattern: TRUE (the default) derives it — a "section" key or a hidden break-only key breaks, a visible column key runs continuous; FALSE inserts none (as in the listing above); a character subset of by breaks on exactly those keys. Here a blank line separates parameters but not the visits within one:
Clinical convention: BigN is the population denominator (from ADSL), not the number of rows in the domain dataset — compute it from the population, not from the summarised data.
For a variable number of arms, the per-arm label is one cols_apply() call instead of a hand-written line each: the {.name} token resolves to each matched column’s name, and the rest of the {…} evaluates in the calling environment, so the BigN looks itself up:
Widths: "auto" (default) sizes to content; a pinned value ("1in", 1.0, "20%") wraps within that width. Set the shared arm width via cols_apply() last — its non-default width then wins the field-merge; a later cols() call carrying the default width = "auto" would otherwise be ambiguous.
Sorting rows
Display cells are formatted strings — "54 (21.3)" sorts lexically, not numerically. The idiom: carry one hidden numeric key per sort level, hide it with col_spec(visible = FALSE), and hand the keys to sort_rows(). descending takes one value per key, so mixed-direction sorts are a single call.
The bundled AE table ships its keys precomputed: soc_n (events in the parent SOC, constant down each SOC block) and n_total (events on the row). Sorting on both, descending, clusters every preferred term under its SOC and orders both levels by frequency — the standard SAP ordering:
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
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
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
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
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
1 ( 1.4)
3 ( 1.2)
CYSTITIS
1 ( 1.2)
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
1 ( 1.0)
2 ( 2.8)
3 ( 1.2)
PHARYNGOLARYNGEAL PAIN
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
5 ( 2.0)
INSOMNIA
2 ( 2.3)
0
2 ( 2.8)
4 ( 1.6)
ANXIETY
0
3 ( 3.1)
0
3 ( 1.2)
DELUSION
1 ( 1.2)
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
3 ( 1.2)
MUSCLE SPASMS
0
1 ( 1.0)
1 ( 1.4)
2 ( 0.8)
ARTHRITIS
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
5 ( 2.0)
ELECTROCARDIOGRAM T WAVE INVERSION
2 ( 2.3)
1 ( 1.0)
1 ( 1.4)
4 ( 1.6)
BLOOD GLUCOSE INCREASED
0
1 ( 1.0)
1 ( 1.4)
2 ( 0.8)
ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED
1 ( 1.2)
1 ( 1.0)
0
2 ( 0.8)
BIOPSY
0
0
1 ( 1.4)
1 ( 0.4)
Because soc_n is constant within a SOC block and never smaller than any PT’s n_total inside it, each SOC’s summary row sorts to the top of its own block — no separate “parent first” switch needed.
Pagination — long tables
paginate() derives the rows-per-page budget from the preset (paper, font, margins) and the title/footnote/header line counts — you never set rows-per-page by hand. keep_together stops a page break landing inside a section’s run:
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
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
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
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
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
1 ( 1.4)
3 ( 1.2)
CYSTITIS
1 ( 1.2)
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
1 ( 1.0)
2 ( 2.8)
3 ( 1.2)
PHARYNGOLARYNGEAL PAIN
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
5 ( 2.0)
INSOMNIA
2 ( 2.3)
0
2 ( 2.8)
4 ( 1.6)
ANXIETY
0
3 ( 3.1)
0
3 ( 1.2)
DELUSION
1 ( 1.2)
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
3 ( 1.2)
MUSCLE SPASMS
0
1 ( 1.0)
1 ( 1.4)
2 ( 0.8)
ARTHRITIS
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
5 ( 2.0)
ELECTROCARDIOGRAM T WAVE INVERSION
2 ( 2.3)
1 ( 1.0)
1 ( 1.4)
4 ( 1.6)
BLOOD GLUCOSE INCREASED
0
1 ( 1.0)
1 ( 1.4)
2 ( 0.8)
ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED
1 ( 1.2)
1 ( 1.0)
0
2 ( 0.8)
BIOPSY
0
0
1 ( 1.4)
1 ( 0.4)
The preview above is one continuous table: row pagination, keep_together, and the continuation marker materialise only in the paged backends (RTF, PDF, DOCX), not in HTML. Emit to one of those to see the page breaks:
emit(ae_pages, "ae_soc_pt.pdf")# continuation marker repeats on each continued page
Panels — wide tables
When the columns don’t fit one page, paginate(panels = N) splits the non-stub columns into N chunks and repeats the stub on each panel (so the row labels reappear). The stub defaults to the group_rows() keys; name it explicitly with repeat_cols to carry the row label too:
Panels are a paged-backend feature: in HTML and Markdown the table stays one continuous block (the preview above), while RTF, PDF, and DOCX place each panel on its own page with the stub columns repeated. Emit to a paged backend to see the split:
emit(wide_split, "demographics_wide.pdf")# panel 2 carries the (continued) marker
Two things to know:
panels = N splits into Nequal chunks — there is no explicit split position (no “first 5, then the rest”). Equal split is fine for page-fit; if you need a specific boundary, that is a known limitation.
panels is a positive integer (default 1 = no split). Width-aware automatic splitting is a planned future feature, not a current option.
Subgroups and per-page BigN
subgroup() partitions the table — one page block per value, with a banner and a hard page break. A partition-constant column can ride into the banner:
For a different (N=) per arm on each page (the column headers re-resolving per subgroup), pass big_n — a small table of N per page × arm. No bundled dataset carries per-arm-per-page counts, so build it inline (this is also the shape big_n expects):
big_n accepts this wide shape (page column + one column per arm) or a long count()-style table (page, arm, n).
Empty tables: no data to report
A table whose data has zero rows still renders — the full page chrome and the column headers stay intact, and an empty-data placeholder takes the place of the body. This is the correct output for a population that produced no records (a cohort with no subjects, a serious-AE table with no events), rather than an error or a blank page. The per-table message is tabular(empty_text = ...); set a house default for every table with preset(empty_text = ...). The message renders as a single horizontally centred row in the table body, where the first data row would sit.
# Same demographics shell, but the population filter has left no rows.empty_demo<-cdisc_saf_demo[0, ]tabular(empty_demo, titles =c("Table 14.1.1","Demographic and Baseline Characteristics","Safety Population"), footnotes ="No subjects met the inclusion criteria for this cohort.", empty_text ="No data available to report")|>group_rows(by ="variable")|>cols( stat_label =col_spec(label ="Statistic"), placebo =col_spec(label ="Placebo", align ="decimal"), drug_50 =col_spec(label ="Drug 50 mg", align ="decimal"), drug_100 =col_spec(label ="Drug 100 mg", align ="decimal"), Total =col_spec(align ="decimal"))
Table 14.1.1
Demographic and Baseline Characteristics
Safety Population
variable
Statistic
Placebo
Drug 50 mg
Drug 100 mg
Total
No data available to report
No subjects met the inclusion criteria for this cohort.
The same applies under subgroup(): a zero-N crossing is dropped by default, but subgroup(..., keep_empty = TRUE) keeps it and renders its banner above the empty-data page — so every level in the shell appears even when one has no data.