Skip to contents
library(writetfl)
library(ggplot2)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

writetfl produces multi-page PDF files from ggplot2 figures, data-frame tables, gt tables, rtables tables, flextable tables, table1 tables, and other grid content with precise, composable page layouts required for clinical trial TFL deliverables and regulatory submissions.

Each page is divided into up to five vertical sections — header, caption, content, footnote, and footer — whose heights are computed dynamically from live font metrics so that the content area always fills exactly the remaining space. Nothing ever overlaps.


Page layout

Every page follows this structure:

┌─────────────────────────────────────────────────┐  ← page edge
│              (outer margin)                     │
│  ┌───────────────────────────────────────────┐  │
│  │  header_left  header_center  header_right │  │  header
│  │  ---------------------------------------- │  │  ← header_rule (optional)
│  │  caption                                  │  │  caption
│  │                                           │  │
│  │         content (fills remainder)         │  │  content
│  │                                           │  │
│  │  footnote                                 │  │  footnote
│  │  ---------------------------------------- │  │  ← footer_rule (optional)
│  │  footer_left  footer_center  footer_right │  │  footer
│  └───────────────────────────────────────────┘  │
│              (outer margin)                     │
└─────────────────────────────────────────────────┘

Absent sections and their padding gaps are suppressed entirely — no blank space is reserved for them.


Figures

Pass a single ggplot directly. "Page 1 of 1" is added to the footer automatically.

p <- ggplot(mtcars, aes(wt, mpg)) +
  geom_point() +
  labs(x = "Weight (1000 lb)", y = "Miles per gallon")

export_tfl(p, preview = TRUE)

Build a multi-page report by supplying a list of page specs. Arguments in ... are shared across all pages; values inside a page’s list element take priority.

pages <- list(
  list(
    content  = ggplot(mtcars, aes(wt, mpg))  + geom_point(),
    caption  = "Figure 1. Weight is negatively associated with fuel efficiency.",
    footnote = "n = 32 vehicles."
  ),
  list(
    content  = ggplot(mtcars, aes(hp, mpg))  + geom_point(),
    caption  = "Figure 2. Higher horsepower predicts lower fuel efficiency.",
    footnote = "Pearson r = -0.78."
  )
)

export_tfl(
  pages,
  preview      = TRUE,
  header_left  = "Fuel Economy Analysis",
  header_right = format(Sys.Date(), "%d %b %Y"),
  header_rule  = TRUE,
  footer_rule  = TRUE
)

For the full set of layout controls — separator rules, typography, multi-line text, overlap detection, preview mode, and more — see vignette("v01-figure_output").


Data-frame tables

tfl_table() converts a data frame into a paginated table grob. Pass the result directly to export_tfl().

ae_summary <- data.frame(
  system_organ_class = c("Gastrointestinal", "Nervous system", "Skin"),
  n_subjects         = c(12L, 7L, 4L),
  pct                = c(24.0, 14.0, 8.0)
)

tbl <- tfl_table(
  ae_summary,
  col_labels = c(system_organ_class = "System Organ Class",
                 n_subjects = "n", pct = "(%)"),
  col_align  = c(system_organ_class = "left",
                 n_subjects = "right", pct = "right")
)

export_tfl(
  tbl,
  preview     = TRUE,
  header_left = "Table 1. Adverse Events by System Organ Class",
  footnote    = "Percentages are based on the safety population (N = 50)."
)

Use dplyr::group_by() to designate row-header columns. Group columns repeat on every column-split page and suppress repeated values in consecutive rows.

pk_data <- data.frame(
  visit     = rep(c("Week 4", "Week 8", "Week 12"), each = 4),
  treatment = rep(c("Placebo", "Active 10 mg", "Active 20 mg", "Active 40 mg"), 3),
  n         = c(48L, 50L, 49L, 51L, 45L, 47L, 48L, 50L, 41L, 43L, 44L, 46L),
  mean_auc  = c(120.4, 145.2, 178.9, 201.3,
                118.7, 148.6, 185.2, 219.4,
                115.1, 152.3, 191.7, 228.6),
  stringsAsFactors = FALSE
)

pk_data |>
  group_by(visit) |>
  tfl_table(
    col_labels = c(visit = "Visit", treatment = "Treatment",
                   n = "n", mean_auc = "Mean AUC\n(ng\u00b7h/mL)")
  ) |>
  export_tfl(
    preview     = 1,
    header_left = "Table 2. PK Summary by Visit"
  )

tfl_table() paginates automatically:

  • Row pagination — rows split across pages with optional (continued) markers; groups are kept together where possible.
  • Column pagination — columns that exceed the page width are split across pages; set balance_col_pages = TRUE to distribute columns evenly.
  • Column widths — auto-sized from content, fixed (unit()), or relative-weight numeric.
  • Word wrappingwrap_cols reflows long text within a fixed column width.

For the complete table reference — column specs, continuation messages, cell padding, line height, and more — see vignette("v02-tfl_table_intro").

For table typography and styling, see vignette("v03-tfl_table_styling").


gt tables

Pass a gt_tbl object directly to export_tfl(). Title, subtitle, source notes, and footnotes are extracted into writetfl’s annotation zones so they are not duplicated. Tables that exceed the page height are automatically paginated with row group boundaries respected.

library(gt)

tbl <- gt(head(iris, 10)) |>
  tab_header(title = "Iris Measurements", subtitle = "First 10 rows") |>
  tab_source_note("Source: Anderson (1935).")

export_tfl(tbl, preview = TRUE,
  header_left = "Appendix A",
  header_rule = TRUE,
  footer_rule = TRUE
)

A list of gt_tbl objects produces a multi-page PDF with one table per page. For the full reference — annotation mapping, pagination, preserved features, and more — see vignette("v05-gt_tables").


rtables tables

Pass an rtables VTableTree object directly to export_tfl(). Main title and subtitles map to writetfl’s caption; main footer and provenance footer map to the footnote. The table body is rendered as monospace text via toString(). When a table is too tall for a single page, rtables’ built-in paginate_table() splits it across pages respecting row group boundaries.

library(rtables)
#> Loading required package: formatters
#> 
#> Attaching package: 'formatters'
#> The following object is masked from 'package:base':
#> 
#>     %||%
#> Loading required package: magrittr
#> 
#> Attaching package: 'rtables'
#> The following object is masked from 'package:utils':
#> 
#>     str

lyt <- basic_table(
  title       = "Iris Sepal Length by Species",
  main_footer = "Source: Anderson (1935)."
) |>
  split_cols_by("Species") |>
  analyze("Sepal.Length", mean)

tbl <- build_table(lyt, iris)

export_tfl(tbl, preview = TRUE,
  header_left = "Study Report",
  header_rule = TRUE,
  footer_rule = TRUE
)

A list of VTableTree objects produces a multi-page PDF. Font parameters (rtables_font_family, rtables_font_size, rtables_lineheight) can be passed via .... For the full reference see vignette("v06-rtables").


flextable tables

Pass a flextable object directly to export_tfl(). Captions (from set_caption()) are extracted into writetfl’s caption zone. Footer rows (from footnote() or add_footer_lines()) are extracted into writetfl’s footnote zone. The table is rendered via gen_grob() with all formatting preserved.

library(flextable)

ft <- flextable(head(iris, 10)) |>
  set_caption("Iris Measurements") |>
  add_footer_lines("Source: Anderson (1935).")

export_tfl(ft, preview = TRUE,
  header_left = "Appendix B",
  header_rule = TRUE,
  footer_rule = TRUE
)

A list of flextable objects produces a multi-page PDF with one table per page. For the full reference — caption handling, footnote extraction, pagination, preserved features, and more — see vignette("v07-flextable").


table1 tables

Pass a table1 object directly to export_tfl(). Caption and footnote are extracted into writetfl’s annotation zones. Column labels (label()), bold variable names, indented summary statistics, and stratification headers are all preserved via t1flex() conversion.

library(table1)
#> 
#> Attaching package: 'table1'
#> The following objects are masked from 'package:base':
#> 
#>     units, units<-

dat <- data.frame(
  age = c(45, 52, 61, 38, 55, 47, 63, 41, 58, 50),
  sex = c("Male", "Female", "Male", "Female", "Male",
          "Female", "Male", "Female", "Male", "Female"),
  trt = c(rep("Treatment", 5), rep("Placebo", 5))
)
label(dat$age) <- "Age (years)"
label(dat$sex) <- "Sex"

tbl <- table1(~ age + sex | trt, data = dat,
              caption = "Table 1. Baseline Demographics")

export_tfl(tbl, preview = TRUE,
  header_left = "Study Report",
  header_rule = TRUE,
  footer_rule = TRUE
)

A list of table1 objects produces a multi-page PDF. Pagination is group-aware: variable labels and their summary rows are kept together. For the full reference see vignette("v08-table1").


Multi-page reports

export_tfl() accepts a list of page specifications, so different figures can coexist in one PDF with per-page captions, footnotes, or other annotations alongside any shared header and footer.

export_tfl(
  list(
    list(content = ggplot(mtcars, aes(wt, mpg)) + geom_point(),
         caption  = "Figure 1. Weight vs fuel efficiency.",
         footnote = "Pearson r = -0.87."),
    list(content = ggplot(mtcars, aes(factor(cyl), mpg)) + geom_boxplot() +
                     labs(x = "Cylinders", y = "MPG"),
         caption  = "Figure 2. Fuel efficiency by cylinder count.")
  ),
  preview     = TRUE,
  header_left = "Analysis Report",
  header_rule = TRUE
)

tfl_table() objects are passed as the top-level x argument to export_tfl() rather than inside a page spec list — the function handles pagination and page construction automatically in that case.


Key shared features

Automatic page numbering

page_num (default "Page {i} of {n}") fills footer_right unless a footer_right value is already set. Use a glue template or NULL to disable.

export_tfl(pages, file = "numbered.pdf", page_num = "{i} / {n}")
export_tfl(pages, file = "no-numbers.pdf", page_num = NULL)

Typography

Pass a bare gpar() to style all annotation text uniformly, or a named list for section- or element-level control. Resolution priority (highest wins): element → section → global.

export_tfl(
  p,
  preview     = TRUE,
  header_left = "Protocol XY-001",
  caption     = "Figure 1. Results.",
  gp = list(
    header  = gpar(fontsize = 11, fontface = "bold"),
    caption = gpar(fontsize =  9, fontface = "italic"),
    footer  = gpar(fontsize =  8)
  )
)

Preview mode

export_tfl(..., preview = TRUE) draws to the currently open device without opening or closing a PDF — useful for interactive layout tuning in RStudio or Positron, and for inline graphics in vignettes. Pass an integer vector to render specific pages only.

export_tfl_page(
  x            = list(content = p),
  header_left  = "Draft",
  caption      = "Figure 1.",
  header_rule  = TRUE,
  preview      = TRUE
)


Vignette index

Vignette What it covers
vignette("writetfl") This overview
vignette("v01-figure_output") Full export_tfl() / export_tfl_page() reference for figures: page dimensions, margins, rules, typography, overlap detection, preview mode
vignette("v02-tfl_table_intro") tfl_table() in depth: column specs, widths, alignment, wrapping, row/column pagination, group columns
vignette("v03-tfl_table_styling") Table typography with gp: per-section and per-element gpar() overrides, cell padding, line height
vignette("v04-troubleshooting") Troubleshooting guide: common errors, debugging layout issues
vignette("v05-gt_tables") Exporting gt tables: annotation extraction, pagination, preserved features
vignette("v06-rtables") Exporting rtables tables: annotation mapping, pagination, font control
vignette("v07-flextable") Exporting flextable tables: caption/footnote extraction, pagination, preserved features
vignette("v08-table1") Exporting table1 tables: column labels, indentation, stratification, group-aware pagination