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:
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")| 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) |
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.
| 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) |
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 |