Skip to contents

Production table programs for study TFRM-2024-001 using built-in datasets. Every table follows ICH E3 numbering.

Study setup

Real programs don’t repeat settings in every table. Define shared formatting once at the top of your study program (or in _arframe.yml):

# ── Study-wide theme (set once, inherited by all tables) ──
fr_theme(
  font_size   = 9,
  font_family = "Times New Roman",
  orientation = "landscape",
  hlines      = "header",
  header      = list(bold = TRUE, align = "center"),
  n_format    = "{label}\n(N={n})",
  footnote_separator = FALSE,
  pagehead    = list(left = "TFRM-2024-001", right = "CONFIDENTIAL"),
  pagefoot    = list(left = "{program}",
                     right = "Page {thepage} of {total_pages}")
)

Every table below inherits these settings automatically. Individual tables only specify what is unique to them: data, titles, columns, footnotes, and table-specific row logic.

Principle: If you’re writing the same verb in two tables, it belongs in the theme or a recipe — not in the table program.

N-counts

Define N-counts once per population. Reuse the same vector in every table that shares that population:

# ── Population N-counts (reusable across tables) ──
n_itt    <- c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135)
n_safety <- c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135)

14.1.1 Demographics

demog_spec <- tbl_demog |>
  fr_table() |>
  fr_titles(
    "Table 14.1.1",
    "Demographics and Baseline Characteristics",
    "Intent-to-Treat Population"
  ) |>
  fr_cols(
    .width = "fit",
    characteristic = fr_col("", width = 2.5),
    placebo        = fr_col("Placebo", align = "decimal"),
    zom_50mg       = fr_col("Zomerane 50mg", align = "decimal"),
    zom_100mg      = fr_col("Zomerane 100mg", align = "decimal"),
    total          = fr_col("Total", align = "decimal"),
    group          = fr_col(visible = FALSE),
    .n = n_itt
  ) |>
  fr_rows(group_by = "group", blank_after = "group") |>
  fr_footnotes(
    "Percentages based on number of subjects per treatment group.",
    "MMSE = Mini-Mental State Examination."
  )
fr_validate(demog_spec)

Note what is not here: fr_header(), fr_hlines(), fr_pagehead(), fr_pagefoot(), .n_format, .separator — all inherited from the theme.

demog_spec |> fr_render("output/Table_14_1_1.rtf")
TFRM-2024-001 CONFIDENTIAL
Table 14.1.1
Demographics and Baseline Characteristics
Intent-to-Treat Population
Placebo
(N=45)
Zomerane 50mg
(N=45)
Zomerane 100mg
(N=45)
Total
(N=135)
Subjects, n 45 45 45 135
Age (years)
Mean (SD) 75.0 (6.75) 73.1 (8.43) 75.3 (7.09) 74.4 (7.46)
Median 74.0 74.0 73.0 74.0
Min, Max 65.0, 88.0 55.0, 88.0 55.0, 88.0 55.0, 88.0
Age Group, n (%)
<65 0 7 (15.6) 1 ( 2.2) 8 ( 5.9)
65-80 36 (80.0) 29 (64.4) 31 (68.9) 96 (71.1)
>80 9 (20.0) 9 (20.0) 13 (28.9) 31 (23.0)
Sex, n (%)
Female 27 (60.0) 28 (62.2) 20 (44.4) 75 (55.6)
Male 18 (40.0) 17 (37.8) 25 (55.6) 60 (44.4)
Race, n (%)
White 38 (84.4) 32 (71.1) 35 (77.8) 105 (77.8)
Black or African American 1 ( 2.2) 5 (11.1) 5 (11.1) 11 ( 8.1)
Asian 6 (13.3) 6 (13.3) 5 (11.1) 17 (12.6)
American Indian or Alaska Native 0 2 ( 4.4) 0 2 ( 1.5)
BMI (kg/m2)
Mean (SD) 27.1 (4.76) 26.5 (4.60) 24.9 (5.81) 26.1 (5.13)
Median 27.8 25.9 24.6 25.8
Min, Max 18.7, 42.3 17.3, 36.5 14.2, 44.5 14.2, 44.5
MMSE Score at Baseline
Mean (SD) 19.9 (3.51) 19.8 (3.36) 19.5 (3.55) 19.7 (3.45)
Median 20.0 20.0 19.0 20.0
Min, Max 13.0, 26.0 13.0, 26.0 13.0, 26.0 13.0, 26.0
Study Completion, n (%)
Completed 44 (97.8) 41 (91.1) 37 (82.2) 122 (90.4)
Discontinued 1 ( 2.2) 4 ( 8.9) 8 (17.8) 13 ( 9.6)
Percentages based on number of subjects per treatment group.
MMSE = Mini-Mental State Examination.
/tmp/RtmpKCCmfM/callr-scr-6801c568b2e52 Page 1 of 1

14.1.2 Demographics with group_label

When group variable names and statistic values are in separate columns, group_label auto-injects group headers into the display column:

# ── Sample data: long-form demographics ──
demog_long <- data.frame(
  variable = c(
    "Sex", "Sex",
    "Age (years)", "Age (years)", "Age (years)", "Age (years)",
    "Race", "Race", "Race"
  ),
  statistic = c(
    "Female", "Male",
    "Mean (SD)", "Median", "Min, Max", "n",
    "White", "Black or African American", "Asian"
  ),
  placebo = c(
    "18 (40.0)", "27 (60.0)",
    "67.8 (6.9)", "68.0", "52, 84", "45",
    "38 (84.4)", "5 (11.1)", "2 (4.4)"
  ),
  zom_50mg = c(
    "21 (46.7)", "24 (53.3)",
    "68.2 (7.1)", "69.0", "54, 82", "45",
    "36 (80.0)", "6 (13.3)", "3 (6.7)"
  ),
  zom_100mg = c(
    "20 (44.4)", "25 (55.6)",
    "68.0 (7.3)", "67.0", "53, 83", "45",
    "37 (82.2)", "4 (8.9)", "4 (8.9)"
  ),
  stringsAsFactors = FALSE
)

demog_gl_spec <- demog_long |>
  fr_table() |>
  fr_titles(
    "Table 14.1.2",
    "Demographics by Category (Long-Form Layout)",
    "Intent-to-Treat Population"
  ) |>
  fr_cols(
    .width = "fit",
    variable  = fr_col(visible = FALSE),
    statistic = fr_col("", width = 2.5),
    placebo   = fr_col("Placebo", align = "decimal"),
    zom_50mg  = fr_col("Zomerane 50mg", align = "decimal"),
    zom_100mg = fr_col("Zomerane 100mg", align = "decimal"),
    .n = c(placebo = 45, zom_50mg = 45, zom_100mg = 45)
  ) |>
  fr_rows(
    group_by = list(cols = "variable", label = "statistic")
  ) |>
  fr_footnotes("Percentages based on N per treatment arm.")
fr_validate(demog_gl_spec)

label = "statistic" in the group_by list injects "Sex", "Age (years)", and "Race" as header rows in the statistic column. Detail rows are automatically indented (inferred from label). Style header rows via fr_styles() for bold or other formatting.

14.1.4 Subject Disposition

disp_spec <- tbl_disp |>
  fr_table() |>
  fr_titles(
    "Table 14.1.4",
    list("Subject Disposition", bold = TRUE),
    "All Randomized Subjects"
  ) |>
  fr_cols(
    category  = fr_col("", width = 2.5),
    placebo   = fr_col("Placebo", align = "decimal"),
    zom_50mg  = fr_col("Zomerane 50mg", align = "decimal"),
    zom_100mg = fr_col("Zomerane 100mg", align = "decimal"),
    total     = fr_col("Total", align = "decimal"),
    .n = n_itt
  ) |>
  fr_footnotes("Percentages based on number of subjects randomized per arm.")
fr_validate(disp_spec)

14.3.1 AE by SOC/PT

Multi-page table with continuation text, SOC/PT indentation, and content-based row styling:

ae_soc_spec <- tbl_ae_soc |>
  fr_table() |>
  fr_titles(
    "Table 14.3.1",
    list("Treatment-Emergent Adverse Events by SOC and Preferred Term",
         bold = TRUE),
    "Safety Population"
  ) |>
  fr_page(continuation = "(continued)") |>
  fr_cols(
    soc       = fr_col(visible = FALSE),
    pt        = fr_col("System Organ Class\n  Preferred Term", width = 3.5),
    row_type  = fr_col(visible = FALSE),
    placebo   = fr_col("Placebo", align = "decimal"),
    zom_50mg  = fr_col("Zomerane\n50mg", align = "decimal"),
    zom_100mg = fr_col("Zomerane\n100mg", align = "decimal"),
    total     = fr_col("Total", align = "decimal"),
    .n = n_safety
  ) |>
  fr_rows(group_by = "soc", indent_by = "pt") |>
  fr_styles(
    fr_row_style(
      rows = fr_rows_matches("row_type", value = "total"), bold = TRUE),
    fr_row_style(
      rows = fr_rows_matches("row_type", value = "soc"), bold = TRUE)
  ) |>
  fr_footnotes(
    "MedDRA version 26.0.",
    "Subjects counted once per SOC and Preferred Term.",
    "Sorted by descending total incidence."
  )
fr_validate(ae_soc_spec)

Only fr_page(continuation = "(continued)") is added here because continuation text is specific to this multi-page table.

TFRM-2024-001 CONFIDENTIAL
Table 14.3.1
Treatment-Emergent Adverse Events by SOC and Preferred Term
Safety Population
System Organ Class
Preferred Term
Placebo
(N=45)
Zomerane
50mg
(N=45)
Zomerane
100mg
(N=45)
Total
(N=135)
SUBJECTS WITH >=1 TEAE 44 (97.8) 44 (97.8) 45 (100.0) 133 (98.5)
Gastrointestinal disorders 17 (37.8) 28 (62.2) 27 ( 60.0) 72 (53.3)
Nausea 5 (11.1) 10 (22.2) 9 ( 20.0) 24 (17.8)
Vomiting 1 ( 2.2) 6 (13.3) 11 ( 24.4) 18 (13.3)
Diarrhoea 2 ( 4.4) 7 (15.6) 8 ( 17.8) 17 (12.6)
Flatulence 5 (11.1) 5 (11.1) 3 ( 6.7) 13 ( 9.6)
Abdominal pain upper 4 ( 8.9) 2 ( 4.4) 5 ( 11.1) 11 ( 8.1)
Dyspepsia 1 ( 2.2) 4 ( 8.9) 2 ( 4.4) 7 ( 5.2)
Constipation 1 ( 2.2) 2 ( 4.4) 3 ( 6.7) 6 ( 4.4)
Nervous system disorders 14 (31.1) 24 (53.3) 26 ( 57.8) 64 (47.4)
Headache 6 (13.3) 7 (15.6) 7 ( 15.6) 20 (14.8)
Dizziness 2 ( 4.4) 9 (20.0) 9 ( 20.0) 20 (14.8)
Tremor 3 ( 6.7) 7 (15.6) 2 ( 4.4) 12 ( 8.9)
Somnolence 2 ( 4.4) 2 ( 4.4) 8 ( 17.8) 12 ( 8.9)
Insomnia 3 ( 6.7) 3 ( 6.7) 4 ( 8.9) 10 ( 7.4)
Paraesthesia 1 ( 2.2) 3 ( 6.7) 2 ( 4.4) 6 ( 4.4)
Infections and infestations 21 (46.7) 15 (33.3) 15 ( 33.3) 51 (37.8)
Nasopharyngitis 11 (24.4) 6 (13.3) 4 ( 8.9) 21 (15.6)
Urinary tract infection 4 ( 8.9) 5 (11.1) 4 ( 8.9) 13 ( 9.6)
Upper respiratory tract infection 3 ( 6.7) 4 ( 8.9) 5 ( 11.1) 12 ( 8.9)
Influenza 5 (11.1) 1 ( 2.2) 2 ( 4.4) 8 ( 5.9)
Bronchitis 1 ( 2.2) 2 ( 4.4) 4 ( 8.9) 7 ( 5.2)
General disorders and administration site conditions 16 (35.6) 14 (31.1) 15 ( 33.3) 45 (33.3)
Fatigue 10 (22.2) 6 (13.3) 4 ( 8.9) 20 (14.8)
Asthenia 3 ( 6.7) 4 ( 8.9) 9 ( 20.0) 16 (11.9)
Pyrexia 2 ( 4.4) 3 ( 6.7) 4 ( 8.9) 9 ( 6.7)
Peripheral oedema 1 ( 2.2) 3 ( 6.7) 4 ( 8.9) 8 ( 5.9)
Vascular disorders 11 (24.4) 20 (44.4) 11 ( 24.4) 42 (31.1)
Hypertension 6 (13.3) 8 (17.8) 2 ( 4.4) 16 (11.9)
Hot flush 3 ( 6.7) 4 ( 8.9) 4 ( 8.9) 11 ( 8.1)
Hypotension 1 ( 2.2) 4 ( 8.9) 5 ( 11.1) 10 ( 7.4)
Flushing 3 ( 6.7) 5 (11.1) 0 8 ( 5.9)
Respiratory, thoracic and mediastinal disorders 11 (24.4) 13 (28.9) 17 ( 37.8) 41 (30.4)
Rhinorrhoea 7 (15.6) 3 ( 6.7) 5 ( 11.1) 15 (11.1)
Cough 2 ( 4.4) 5 (11.1) 5 ( 11.1) 12 ( 8.9)
Epistaxis 2 ( 4.4) 2 ( 4.4) 4 ( 8.9) 8 ( 5.9)
Dyspnoea 0 3 ( 6.7) 4 ( 8.9) 7 ( 5.2)
Oropharyngeal pain 1 ( 2.2) 2 ( 4.4) 2 ( 4.4) 5 ( 3.7)
Musculoskeletal and connective tissue disorders 10 (22.2) 13 (28.9) 16 ( 35.6) 39 (28.9)
Back pain 5 (11.1) 5 (11.1) 10 ( 22.2) 20 (14.8)
Myalgia 3 ( 6.7) 6 (13.3) 2 ( 4.4) 11 ( 8.1)
Arthralgia 2 ( 4.4) 3 ( 6.7) 4 ( 8.9) 9 ( 6.7)
Pain in extremity 3 ( 6.7) 0 2 ( 4.4) 5 ( 3.7)
Psychiatric disorders 15 (33.3) 12 (26.7) 11 ( 24.4) 38 (28.1)
Anxiety 6 (13.3) 4 ( 8.9) 4 ( 8.9) 14 (10.4)
Confusional state 4 ( 8.9) 3 ( 6.7) 2 ( 4.4) 9 ( 6.7)
Agitation 3 ( 6.7) 2 ( 4.4) 4 ( 8.9) 9 ( 6.7)
Depression 2 ( 4.4) 4 ( 8.9) 2 ( 4.4) 8 ( 5.9)
Hallucination 1 ( 2.2) 1 ( 2.2) 0 2 ( 1.5)
Cardiac disorders 13 (28.9) 11 (24.4) 12 ( 26.7) 36 (26.7)
Palpitations 5 (11.1) 4 ( 8.9) 1 ( 2.2) 10 ( 7.4)
Bradycardia 3 ( 6.7) 2 ( 4.4) 4 ( 8.9) 9 ( 6.7)
Tachycardia 1 ( 2.2) 4 ( 8.9) 3 ( 6.7) 8 ( 5.9)
Extrasystoles 3 ( 6.7) 1 ( 2.2) 3 ( 6.7) 7 ( 5.2)
Atrial fibrillation 2 ( 4.4) 1 ( 2.2) 4 ( 8.9) 7 ( 5.2)
Metabolism and nutrition disorders 12 (26.7) 11 (24.4) 12 ( 26.7) 35 (25.9)
Dehydration 5 (11.1) 4 ( 8.9) 5 ( 11.1) 14 (10.4)
Decreased appetite 2 ( 4.4) 5 (11.1) 3 ( 6.7) 10 ( 7.4)
Weight decreased 4 ( 8.9) 2 ( 4.4) 1 ( 2.2) 7 ( 5.2)
Hypokalaemia 3 ( 6.7) 1 ( 2.2) 3 ( 6.7) 7 ( 5.2)
Skin and subcutaneous tissue disorders 8 (17.8) 10 (22.2) 17 ( 37.8) 35 (25.9)
Rash 3 ( 6.7) 3 ( 6.7) 9 ( 20.0) 15 (11.1)
Hyperhidrosis 1 ( 2.2) 1 ( 2.2) 9 ( 20.0) 11 ( 8.1)
Pruritus 2 ( 4.4) 6 (13.3) 1 ( 2.2) 9 ( 6.7)
Dermatitis 2 ( 4.4) 2 ( 4.4) 1 ( 2.2) 5 ( 3.7)
Dry skin 1 ( 2.2) 0 2 ( 4.4) 3 ( 2.2)
Investigations 6 (13.3) 10 (22.2) 10 ( 22.2) 26 (19.3)
Weight increased 2 ( 4.4) 5 (11.1) 5 ( 11.1) 12 ( 8.9)
Blood creatinine increased 4 ( 8.9) 2 ( 4.4) 2 ( 4.4) 8 ( 5.9)
Alanine aminotransferase increased 0 3 ( 6.7) 4 ( 8.9) 7 ( 5.2)
Renal and urinary disorders 7 (15.6) 10 (22.2) 8 ( 17.8) 25 (18.5)
Incontinence 4 ( 8.9) 6 (13.3) 0 10 ( 7.4)
Nocturia 2 ( 4.4) 3 ( 6.7) 3 ( 6.7) 8 ( 5.9)
Dysuria 0 1 ( 2.2) 4 ( 8.9) 5 ( 3.7)
Pollakiuria 1 ( 2.2) 2 ( 4.4) 1 ( 2.2) 4 ( 3.0)
Reproductive system and breast disorders 8 (17.8) 8 (17.8) 7 ( 15.6) 23 (17.0)
Erectile dysfunction 3 ( 6.7) 4 ( 8.9) 3 ( 6.7) 10 ( 7.4)
Menstrual disorder 2 ( 4.4) 5 (11.1) 2 ( 4.4) 9 ( 6.7)
Gynaecomastia 4 ( 8.9) 1 ( 2.2) 3 ( 6.7) 8 ( 5.9)
Eye disorders 7 (15.6) 9 (20.0) 6 ( 13.3) 22 (16.3)
Conjunctivitis 2 ( 4.4) 6 (13.3) 2 ( 4.4) 10 ( 7.4)
Dry eye 3 ( 6.7) 1 ( 2.2) 3 ( 6.7) 7 ( 5.2)
Vision blurred 2 ( 4.4) 2 ( 4.4) 1 ( 2.2) 5 ( 3.7)
Lacrimation increased 0 3 ( 6.7) 1 ( 2.2) 4 ( 3.0)
Ear and labyrinth disorders 6 (13.3) 8 (17.8) 8 ( 17.8) 22 (16.3)
Tinnitus 1 ( 2.2) 5 (11.1) 6 ( 13.3) 12 ( 8.9)
Vertigo 4 ( 8.9) 2 ( 4.4) 2 ( 4.4) 8 ( 5.9)
Ear pain 1 ( 2.2) 1 ( 2.2) 0 2 ( 1.5)
Blood and lymphatic system disorders 4 ( 8.9) 7 (15.6) 7 ( 15.6) 18 (13.3)
Anaemia 2 ( 4.4) 5 (11.1) 3 ( 6.7) 10 ( 7.4)
Leukopenia 1 ( 2.2) 3 ( 6.7) 1 ( 2.2) 5 ( 3.7)
Thrombocytopenia 1 ( 2.2) 0 3 ( 6.7) 4 ( 3.0)
Hepatobiliary disorders 5 (11.1) 5 (11.1) 8 ( 17.8) 18 (13.3)
Hepatic enzyme increased 2 ( 4.4) 2 ( 4.4) 3 ( 6.7) 7 ( 5.2)
Cholelithiasis 2 ( 4.4) 2 ( 4.4) 3 ( 6.7) 7 ( 5.2)
Hepatic steatosis 1 ( 2.2) 1 ( 2.2) 3 ( 6.7) 5 ( 3.7)
MedDRA version 26.0.
Subjects counted once per SOC and Preferred Term.
Sorted by descending total incidence.
/tmp/RtmpKCCmfM/callr-scr-6801c568b2e52 Page 1 of 1

14.3.2 AE by SOC / HLT / PT (Three-Level Hierarchy)

Multi-level indent_by with a key column that drives indent depth:

# ── Sample data: three-level AE hierarchy ──
ae_3level <- data.frame(
  soc = c(
    rep("Gastrointestinal disorders", 5),
    rep("Nervous system disorders", 4)
  ),
  term = c(
    "Gastrointestinal disorders", "GI signs and symptoms",
    "Nausea", "Vomiting", "Diarrhoea",
    "Nervous system disorders", "Headaches",
    "Headache", "Migraine"
  ),
  row_type = c(
    "soc", "hlt", "pt", "pt", "pt",
    "soc", "hlt", "pt", "pt"
  ),
  placebo = c(
    "28 (62.2)", "20 (44.4)", "12 (26.7)", "5 (11.1)", "3 (6.7)",
    "18 (40.0)", "14 (31.1)", "10 (22.2)", "4 (8.9)"
  ),
  zom_100mg = c(
    "32 (71.1)", "24 (53.3)", "14 (31.1)", "6 (13.3)", "4 (8.9)",
    "22 (48.9)", "16 (35.6)", "12 (26.7)", "4 (8.9)"
  ),
  stringsAsFactors = FALSE
)

ae_3level_spec <- ae_3level |>
  fr_table() |>
  fr_titles(
    "Table 14.3.2",
    "TEAEs by SOC, HLT, and Preferred Term",
    "Safety Population"
  ) |>
  fr_cols(
    soc      = fr_col(visible = FALSE),
    row_type = fr_col(visible = FALSE),
    term     = fr_col("SOC / HLT / Preferred Term", width = 3.5),
    placebo  = fr_col("Placebo\n(N=45)", align = "decimal"),
    zom_100mg = fr_col("Zomerane 100mg\n(N=45)", align = "decimal")
  ) |>
  fr_rows(
    group_by  = "soc",
    indent_by = list(
      key    = "row_type",
      col    = "term",
      levels = c(soc = 0, hlt = 1, pt = 2)
    )
  ) |>
  fr_styles(
    fr_row_style(
      rows = fr_rows_matches("row_type", value = "soc"), bold = TRUE
    )
  ) |>
  fr_footnotes(
    "MedDRA version 26.0.",
    "Subjects counted once per SOC, HLT, and Preferred Term."
  )
fr_validate(ae_3level_spec)
#> Warning: ! 1 validation issue found:
#>  `indent_by` column not found in data: c(soc = 0, hlt = 1, pt = 2).

The levels vector maps row_type values to indent depth: SOC = 0 (flush left), HLT = 1 level, PT = 2 levels.

14.2.1 Time-to-Event

tte_spec <- tbl_tte |>
  fr_table() |>
  fr_titles(
    "Table 14.2.1",
    list("Time to Study Withdrawal", bold = TRUE),
    "Intent-to-Treat Population"
  ) |>
  fr_cols(
    section   = fr_col(visible = FALSE),
    statistic = fr_col("", width = 3.5),
    zom_50mg  = fr_col("Zomerane\n50mg", align = "decimal"),
    zom_100mg = fr_col("Zomerane\n100mg", align = "decimal"),
    placebo   = fr_col("Placebo", align = "decimal"),
    .n = c(zom_50mg = 45, zom_100mg = 45, placebo = 45)
  ) |>
  fr_rows(group_by = "section", blank_after = "section") |>
  fr_styles(
    fr_row_style(
      rows = fr_rows_matches("statistic", pattern = "^[A-Z]"),
      bold = TRUE
    )
  ) |>
  fr_footnotes(
    "[a] Kaplan-Meier estimate with Greenwood 95% CI.",
    "[b] Two-sided log-rank test stratified by age group.",
    "[c] Cox proportional hazards model.",
    "NE = Not Estimable."
  )
fr_validate(tte_spec)

.n uses a local vector here because the column order differs from other tables (treatment arms exclude total).

14.4.1 Concomitant Medications

cm_spec <- tbl_cm |>
  fr_table() |>
  fr_titles(
    "Table 14.4.1",
    list("Concomitant Medications by Category and Agent", bold = TRUE),
    "Safety Population"
  ) |>
  fr_cols(
    category   = fr_col(visible = FALSE),
    medication = fr_col("Medication Category / Agent", width = 3.0),
    row_type   = fr_col(visible = FALSE),
    placebo    = fr_col("Placebo", align = "decimal"),
    zom_50mg   = fr_col("Zomerane\n50mg", align = "decimal"),
    zom_100mg  = fr_col("Zomerane\n100mg", align = "decimal"),
    total      = fr_col("Total", align = "decimal"),
    .n = n_safety
  ) |>
  fr_rows(group_by = "category", indent_by = "medication") |>
  fr_styles(
    fr_row_style(
      rows = fr_rows_matches("row_type", value = "total"), bold = TRUE),
    fr_row_style(
      rows = fr_rows_matches("row_type", value = "category"), bold = TRUE)
  ) |>
  fr_footnotes("Subjects counted once per category and medication.")
fr_validate(cm_spec)

14.3.6 Vital Signs with page_by

Multi-parameter table with spanning headers and per-page N-counts:

# Pre-compute per-parameter N-counts from ADVS
vs_n <- aggregate(
  USUBJID ~ PARAM + TRTA, data = advs[advs$AVISIT == "Baseline", ],
  FUN = function(x) length(unique(x))
)

vs_spec <- tbl_vs[tbl_vs$timepoint == "Week 24", ] |>
  fr_table() |>
  fr_titles(
    "Table 14.3.6",
    "Vital Signs --- Week 24 Summary",
    "Safety Population"
  ) |>
  fr_cols(
    param     = fr_col(visible = FALSE),
    timepoint = fr_col(visible = FALSE),
    statistic         = fr_col("Statistic", width = 1.2),
    placebo_base      = fr_col("Baseline"),
    placebo_value     = fr_col("Value"),
    placebo_chg       = fr_col("CFB"),
    zom_50mg_base     = fr_col("Baseline"),
    zom_50mg_value    = fr_col("Value"),
    zom_50mg_chg      = fr_col("CFB"),
    zom_100mg_base    = fr_col("Baseline"),
    zom_100mg_value   = fr_col("Value"),
    zom_100mg_chg     = fr_col("CFB"),
    .n = vs_n
  ) |>
  fr_rows(page_by = "param") |>
  fr_spans(
    "Placebo"        = c("placebo_base", "placebo_value", "placebo_chg"),
    "Zomerane 50mg"  = c("zom_50mg_base", "zom_50mg_value", "zom_50mg_chg"),
    "Zomerane 100mg" = c("zom_100mg_base", "zom_100mg_value", "zom_100mg_chg")
  ) |>
  fr_footnotes("CFB = Change from Baseline.")
fr_validate(vs_spec)

.n takes a 3-column data frame (parameter + treatment + count) for automatic per-page N-counts when combined with page_by.

Wide table with column split

When columns exceed the page width, .split = TRUE creates panels:

wide_spec <- tbl_vs[tbl_vs$timepoint == "Week 24" &
                     tbl_vs$param == "Systolic BP (mmHg)", ] |>
  fr_table() |>
  fr_titles("Table 14.3.6", "Systolic BP --- Column Split") |>
  fr_cols(
    param     = fr_col(visible = FALSE),
    timepoint = fr_col(visible = FALSE),
    statistic = fr_col("Statistic", width = 1.2, stub = TRUE),
    placebo_base      = fr_col("Placebo\nBaseline", width = 1.0),
    placebo_value     = fr_col("Placebo\nValue", width = 1.0),
    placebo_chg       = fr_col("Placebo\nCFB", width = 1.0),
    zom_50mg_base     = fr_col("Zom 50mg\nBaseline", width = 1.0),
    zom_50mg_value    = fr_col("Zom 50mg\nValue", width = 1.0),
    zom_50mg_chg      = fr_col("Zom 50mg\nCFB", width = 1.0),
    zom_100mg_base    = fr_col("Zom 100mg\nBaseline", width = 1.0),
    zom_100mg_value   = fr_col("Zom 100mg\nValue", width = 1.0),
    zom_100mg_chg     = fr_col("Zom 100mg\nCFB", width = 1.0),
    .split = TRUE, .width = "fit"
  )
fr_validate(wide_spec)

What each layer owns

Setting Where it belongs Why
Font, orientation, margins fr_theme() or _arframe.yml Same for every table
Header bold/center, .n_format fr_theme() or _arframe.yml Same for every table
fr_hlines("header") fr_theme() or _arframe.yml Same for every table
Page headers/footers fr_theme() or _arframe.yml Same for every table
footnote_separator = FALSE fr_theme() or _arframe.yml Same for every table
N-count vectors Named variables (n_itt, n_safety) Reused across tables
Titles, footnotes Per-table Unique to each table
Column definitions Per-table Unique to each table
Row logic (group_by, indent_by) Per-table Unique to each table
Row styles Per-table Unique to each table
continuation, page_by Per-table Only some tables need them