Regulatory-grade clinical tables, listings, and figures in RTF, PDF, and HTML from a single specification.
arframe is an R package for pharmaceutical submissions. You describe the output once; arframe renders RTF, PDF, and HTML — 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 summarisation (gtsummary, tfrmt). But no single package closes the last mile: taking a finished analysis table and producing submission-grade RTF, PDF, and HTML from the same specification.
- r2rtf produces RTF only. Zero PDF.
- tfrmt + docorator produces good PDF. RTF is documented as experimental/lossy.
- gt / gtsummary produces HTML-first output. RTF page headers are not field-code-based.
- None produce all three formats from one spec with running page headers, decimal alignment, and study-wide font control.
arframe is a native rendering engine — it writes RTF 1.9.1 control words and tabularray/LaTeX directly. There is no intermediate gt or pandoc layer. Both RTF and PDF are first-class outputs, produced from the same fr_spec without a format-specific pipeline.
| 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 |
| HTML preview | Self-contained HTML with paper simulation and viewer integration |
| Single spec → RTF + PDF + HTML | Same fr_spec renders to .rtf, .pdf, or .html |
| Font family (Courier New, Times New Roman) | fr_page(font_family = "Courier New") — first-class parameter via fontspec |
| Automatic decimal alignment | AFM font metrics for 15 stat types — no manual markers |
| Auto column width | Physical width from AFM metrics for 12 font variants |
| Group-aware pagination | Keeps SOC groups together, repeats headers, adds continuation text |
| Page headers/footers | 3-slot layout with live field tokens: {thepage}, {total_pages}, {program} |
| Study-wide config | _arframe.yml auto-discovered — one file sets font, margins, headers for all programs |
| 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 = "Times New Roman",
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 (with list form for label), group_keep, page_by (with list form for visible), indent_by (multi-level), blank_after, sort_by, suppress, 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, HTML, or LaTeX |
fr_render(spec, "output.rtf") # Native RTF
fr_render(spec, "output.pdf") # Native PDF via XeLaTeX
fr_render(spec, "output.html") # Self-contained HTML preview
fr_render(spec, "output.tex") # LaTeX source# In RStudio/Positron — just print the spec to see it in the Viewer panel
spec
# In Rmd/Quarto/pkgdown — tables render inline automatically via knit_printspec |> 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 gt or huxtable layer:
fr_spec → finalize_spec() → RTF backend → .rtf file
├→ LaTeX backend → .tex → XeLaTeX → .pdf
└→ HTML backend → .html file
- 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 Adobe open-source fonts (Source Serif 4, Source Sans 3, Source Code Pro — SIL OFL) on Linux/Docker without Microsoft fonts. Set
ARFRAME_FONT_DIRto a directory of.ttf/.otffiles for project-local fonts without system-wide installation. - HTML: Self-contained document with paper simulation (orientation-aware page dimensions, margins). Auto-preview in RStudio/Positron viewer via
print(). Inline rendering in Rmd/Quarto viaknit_print(). - 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 sits at the output layer of the pharmaverse — it receives whatever the analysis layer produces and renders it to files. It does not replace upstream packages; it completes them.
| Package | Role | Relationship to arframe |
|---|---|---|
| admiral | ADaM dataset creation | Upstream — arframe renders admiral outputs |
| Tplyr / rtables | Table summarisation | Upstream — arframe renders their wide-format output |
| cards / cardx | ARD creation | Upstream — fr_wide_ard() converts ARD to arframe input |
| gtsummary | High-level summaries | Upstream — arframe renders the same data gtsummary summarises |
| tfrmt | ARD display spec + mock shells | Complementary — tfrmt for SAP-phase shell review; arframe for production output |
| r2rtf | RTF-only rendering | Parallel — r2rtf is RTF-specialist; arframe adds PDF + HTML from the same spec |
| docorator | gt-based PDF decoration | Parallel — docorator wraps gt objects; arframe is a native engine without a gt layer |
MIT
