Skip to content

vthanik/arframe

Repository files navigation

arframe

Regulatory-grade clinical tables, listings, and figures in RTF and PDF from a single specification.

R-CMD-check License: MIT

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 format

Why arframe?

The 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

Installation

# Install from GitHub
# install.packages("pak")
pak::pak("vthanik/arframe")

Quick start

arframe ships with synthetic CDISC ADaM datasets so every example runs out of the box.

A table in 3 lines

library(arframe)

tbl_demog |> fr_table() |> fr_render(tempfile(fileext = ".rtf"))

A production demographics table

# 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"))

An AE table with SOC/PT hierarchy

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

Pipeline verbs

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

Key features

Dual-format rendering

fr_render(spec, "output.rtf")  # Native RTF
fr_render(spec, "output.pdf")  # Native PDF via XeLaTeX
fr_render(spec, "output.tex")  # LaTeX source

Automatic N-counts in headers

spec |> fr_cols(
  .n = c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135),
  .n_format = "{label}\n(N={n})"
)

Page tokens

spec |>
  fr_pagehead(left = "Protocol TFRM-2024-001", right = "CONFIDENTIAL") |>
  fr_pagefoot(left = "{program}", right = "Page {thepage} of {total_pages}")

Inline markup

# 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")

Four-tier defaults

+----------------------------------------------------------------------+
| 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|
+----------------------------------------------------------------------+

Recipes: reusable pipeline fragments

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

Validation and QC

fr_validate(spec)                        # Pre-render checks
fr_get_titles(spec)                      # Programmatic inspection
fr_style_explain(spec, row = 1, col = "total")  # Debug style cascade

Built-in datasets

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)

Architecture

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, \trkeep group 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_DIR to a directory of .ttf/.otf files 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.

Documentation

Related packages

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

License

MIT

Contributors