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).
The column model: usage
Every column gets a role via col_spec(usage = …). Picking the right one is the single most important structural decision:
usage
Use it for
Behaviour
"display"(default)
data cells (the arm columns)
one value per row
"group"
section variable (e.g. parameter)
each value becomes a section-header row; the column is hidden
"id"
the row label that must stay visible
like display, but joins the stub and repeats on every horizontal panel
Indentation is not a usage role — it 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.group_display = "header_row" 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.
BigN in the column headers
The (N=…) denominator goes in each arm’s header label. Build it from a BigN table and interpolate with glue:
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.
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-group columns into N chunks and repeats every group/id column on each panel (so the row labels reappear). Make the row label usage = "id" so it rides every panel:
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 id / group 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):