Regulatory-grade clinical tables, listings, and figures in RTF and PDF from a single specification.
arframe is an R package for pharmaceutical submissions. You describe the output once; arframe renders both RTF and PDF — change the file extension, the output adapts.
tbl_demog |>
fr_table() |>
fr_titles("Table 14.1.1", "Demographics and Baseline Characteristics") |>
fr_cols(
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 = c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135)
) |>
fr_hlines("header") |>
fr_render("Table_14_1_1.rtf") # Change to .pdf — same output, different formatThe pharmaverse has excellent tools for data derivation (admiral), analysis (Tplyr, rtables, tern), and table decoration (docorator, r2rtf). arframe occupies a different position: it is a native rendering engine that writes RTF control words directly (from the RTF 1.9.1 spec) and generates LaTeX/tabularray natively, so both formats are first-class outputs.
| Capability | arframe |
|---|---|
| Native RTF rendering | Direct RTF 1.9.1 — no intermediate HTML or gt layer |
| Native PDF rendering | Direct tabularray/XeLaTeX — no RMarkdown/Quarto pipeline |
| Single spec, both formats | Same fr_spec object renders to .rtf or .pdf |
| Group-aware pagination | Keeps groups together, repeats headers, adds continuation text |
| Decimal alignment | 15-type stat display engine (n/%, mean (SD), CI, p-values, ranges) |
| Page headers/footers | 3-slot layout with tokens: {thepage}, {total_pages}, {program} |
| Inline markup | fr_super(), fr_bold(), fr_italic(), fr_dagger() in any cell |
| Dependencies | 7 imports — no dplyr, tidyr, purrr, or gt |
# Install from GitHub
# install.packages("pak")
pak::pak("vthanik/arframe")arframe ships with synthetic CDISC ADaM datasets so every example runs out of the box.
library(arframe)
tbl_demog |> fr_table() |> fr_render(tempfile(fileext = ".rtf"))# Set shared formatting once — inherited by all tables
fr_theme(
font_size = 9,
font_family = "Courier New",
orientation = "landscape",
hlines = "header",
header = list(bold = TRUE, align = "center"),
n_format = "{label}\n(N={n})",
pagehead = list(left = "TFRM-2024-001", right = "CONFIDENTIAL"),
pagefoot = list(left = "{program}",
right = "Page {thepage} of {total_pages}")
)
n_itt <- c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135)
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.")
# Render both formats from the same spec
fr_render(spec, tempfile(fileext = ".rtf"))
fr_render(spec, tempfile(fileext = ".pdf"))tbl_ae_soc |>
fr_table() |>
fr_titles(
"Table 14.3.1",
"Treatment-Emergent Adverse Events by SOC and Preferred Term",
"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 = c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135)
) |>
fr_rows(group_by = "soc", indent_by = "pt") |>
fr_styles(
fr_row_style(rows = fr_rows_matches("row_type", value = "soc"), bold = TRUE)
) |>
fr_render(tempfile(fileext = ".rtf"))Every verb takes an fr_spec, returns a modified fr_spec. Verb order doesn't matter — arframe resolves everything at render time.
| Verb | Purpose |
|---|---|
fr_table() / fr_listing() / fr_figure() |
Entry points — create spec from data or plot (figures support a list of plots with per-page metadata tokens) |
fr_cols() + fr_col() |
Column labels, widths, alignment, visibility, N-counts |
fr_header() |
Header styling (bold, align, valign, colors) |
fr_titles() |
Table titles (1-4 lines, with inline markup) |
fr_footnotes() |
Footnotes with placement control ("every" or "last") |
fr_rows() |
group_by, group_label, group_keep, page_by, indent_by (multi-level), blank_after, sort_by, repeat_cols, wrap |
fr_page() |
Paper size, orientation, margins, font, continuation text |
fr_pagehead() / fr_pagefoot() |
Running headers/footers with token substitution |
fr_hlines() / fr_vlines() / fr_grid() |
Horizontal/vertical rules (8 presets) |
fr_spans() |
Spanning column headers |
fr_styles() |
Cell/row/column conditional formatting |
fr_spacing() |
Gaps between structural sections |
fr_render() |
Render to RTF, PDF, or LaTeX |
fr_render(spec, "output.rtf") # Native RTF
fr_render(spec, "output.pdf") # Native PDF via XeLaTeX
fr_render(spec, "output.tex") # LaTeX sourcespec |> fr_cols(
.n = c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135),
.n_format = "{label}\n(N={n})"
)spec |>
fr_pagehead(left = "Protocol TFRM-2024-001", right = "CONFIDENTIAL") |>
fr_pagefoot(left = "{program}", right = "Page {thepage} of {total_pages}")# Works in titles, footnotes, and data cells
spec |> fr_titles("Table 14.1.1{fr_super('a')}", "Demographics")
spec |> fr_footnotes("{fr_super('a')} Post-hoc analysis")+----------------------------------------------------------------------+
| 4. Per-table verbs fr_page(font_size = 10) <- wins |
| 3. Session theme fr_theme(font_size = 9) |
| 2. Project config _arframe.yml |
| 1. Package defaults inst/defaults/_arframe.yml <- lowest|
+----------------------------------------------------------------------+
company_layout <- fr_recipe(
fr_page(orientation = "landscape", font_size = 9),
fr_pagehead(left = "TFRM-2024-001", right = "CONFIDENTIAL"),
fr_pagefoot(left = "{program}", right = "Page {thepage} of {total_pages}"),
fr_hlines("header")
)
tbl_demog |> fr_table() |> fr_apply(company_layout) |> fr_titles("Table 14.1.1", "Demographics")fr_validate(spec) # Pre-render checks
fr_get_titles(spec) # Programmatic inspection
fr_style_explain(spec, row = 1, col = "total") # Debug style cascade| Dataset | Description |
|---|---|
adsl, adae, adtte, adcm, advs |
Synthetic CDISC ADaM datasets (135 subjects) |
tbl_demog |
Demographics summary (Table 14.1.1) |
tbl_ae_soc |
AE by SOC/PT (Table 14.3.1.2) |
tbl_ae_summary |
Overall AE summary (Table 14.3.1.1) |
tbl_disp |
Subject disposition (Table 14.1.3) |
tbl_tte |
Time-to-event summary (Table 14.2.1) |
tbl_cm |
Concomitant medications (Table 14.4.1) |
tbl_vs |
Vital signs (Table 14.3.6) |
arframe writes output directly — no intermediate HTML, gt, or huxtable layer:
fr_spec → finalize_spec() → RTF backend → .rtf file
└→ LaTeX backend → .tex → XeLaTeX → .pdf
- RTF: Writes RTF 1.9.1 control words directly. Cell-level formatting, decimal alignment,
\trkeepgroup protection, R-side pagination. - PDF: Generates tabularray LaTeX, compiles with XeLaTeX. Falls back to Latin Modern fonts (built into tinytex) on Linux/Docker without Microsoft fonts. Set
ARFRAME_FONT_DIRto a directory of.ttf/.otffiles for project-local fonts without system-wide installation. - Font metrics: Real Adobe Font Metrics (AFM) for 12 font variants — accurate column width estimation without rendering.
- Get Started — Your first table to production-ready output
- Columns & Headers — Widths, alignment, N-counts, decimal alignment
- Titles & Footnotes — Titles, footnotes, inline markup
- Rows & Page Layout — Grouping, pagination, page chrome
- Rules, Spans & Styles — Borders, spanning headers, conditional formatting
- Table Cookbook — 8 complete ICH E3 table programs
- Listings & Figures — Patient listings and embedded figures
- Automation & Batch — Themes, recipes, YAML config, batch rendering
- Architecture — Internal design and backend interface
arframe is designed to complement the pharmaverse ecosystem:
- admiral — ADaM dataset creation
- Tplyr / rtables — Analysis table summarization (upstream of arframe)
- r2rtf — RTF-focused table rendering
- docorator — gt-based document decoration
MIT
