Skip to contents

Build infographic-style PDF pages in R from blocks that size themselves.

bodybuildr handles the annoying part of report and profile layout: deciding how tall everything is. Each piece of a page is a block that knows its height at the width it gets. You stack blocks, split them into columns, and place them between a page header and a footer that stays pinned to the bottom margin. No absolute coordinates, no hand-tuned row heights, and translated text that runs longer just makes the block taller.

Documentation, a step-by-step tutorial and worked examples are at bodybuildr.sunstonebio.info.

Why bodybuildr?

  • Self-sizing layout. bb_stack() and bb_cols() pass widths down and collect heights up. Plots stretch to match their neighbours.
  • Real pages. page_template() gives margins, a running header, and a footer anchored to the bottom with {page} / {pages} numbering. export_pdf() writes a whole document to one PDF.
  • Editorial tables. bb_table() draws rules-only or boxed tables with rows that grow with wrapped text, per-column styles and alignment, and any block as a cell (status icons, dots, images). Side-by-side tables can share row heights.
  • Typography that holds up. text_style() supports weights such as 600 (through the installed font face), line height and letter case; themes define every text role once.
  • Mixed content. Text (plain, Markdown, HTML), inline span()s and underlined link()s, ggplot2 plots, images, grobs, legends, status marks, banners, section headers and patterned placeholders.

Install

# install.packages("remotes")
remotes::install_github("sunstone-bioinformatics/bodybuildr")

Learn bodybuildr

  1. Tutorial Starts with a single block and works up to a multi-page PDF report with a header, a footer, tables, plots, a theme and a block you write yourself. One idea per step, each with its rendered output.

  2. Layout model How blocks measure themselves, how stacks, columns and pages share space, and the three separate kinds of spacing.

  3. Showcase Three very different documents built from random data: a dark landscape dashboard, a serif magazine and small-format event passes.

  4. Function reference Every function, grouped by what it does.

Note

On the documentation site, the Tutorial and Articles menus in the navigation bar link to all of these guides.

Quick start

library(bodybuildr)

template <- page_template(
  margin_x = 0.5,
  header   = bb_running_header("Immunization Profile", "{country}, 2025"),
  footer   = bb_page_footer("Organisation, 2026", "{country} · {page} / {pages}")
)

indicators <- bb_table(
  list(
    list("Total population",  "45,696,159"),
    list("Surviving infants", "530,193"),
    list("Skilled birth attendance", cell("No data", text_style(color = "faint")))
  ),
  widths = c(2, 1), align = c("left", "right"),
  col_style = list(NULL, text_style(weight = 600))
)

page <- bb_page(
  bb_section_header("General indicators", refs = "[1][2]"),
  bb_cols(indicators, my_ggplot, gap = 0.3),      # the plot matches the table's height
  bb_section_header("Notes"),
  bb_box(bb_text("Values below <80% are flagged.", style = "note"), box_style_panel()),
  template = template,
  header   = bb_banner("Immunization Profile", "Argentina, 2025"),
  gap      = c(0.08, 0.3, 0.08),
  data     = list(country = "Argentina")
)

export_pdf(bb_document(page, page_two), "profile.pdf")

Spacing: three separate bands

  1. Page margins come from page_template(). Full-bleed blocks (banners) escape the side margins.
  2. Layout gaps are the gap of bb_stack(), bb_cols() and bb_page().
  3. Box spacing is the padding and margin of bb_box().

Each band is set in one place, so changing one never shifts the others.

Upgrading from 0.4.x

0.5.0 replaces the row-based API. The canvas (new_canvas(), canvas_add_row()), the row builders (str_banner_row(), str_subtitle_row(), str_n_panel_row(), str_three_panel_row()), the layout-style constructors, text_box() / md_text_box() / html_text_box() / blank_box() and table_box() / rich_table_box() are gone.

0.4.x 0.5.0
new_canvas() + canvas_add_row(height = ) bb_page(...) (heights are measured)
str_n_panel_row(items) bb_cols(...) with optional widths
str_three_panel_row(A, B, C) bb_cols(A, bb_stack(B, C))
str_banner_row() bb_banner()
str_subtitle_row() bb_box(bb_text(...), box_style(fill = gradient_fill(...))) or bb_section_header()
text_box() / md_text_box() / html_text_box() bb_text(markup = ), wrapped in bb_box() for fills and borders
blank_box() / NULL slot NULL in bb_cols()
table_box() / rich_table_box() bb_table(style = table_style_grid())
text_style(face = "bold") text_style(weight = 700)
export_pdf(canvas, margin_*) export_pdf(page_or_document); margins live in page_template()

Notes

  • Text renders with gridtext. Plain text is escaped, so <80%, * and [1] print literally. Links in plain text are underlined; in Markdown/HTML text they are coloured only, because gridtext can’t underline. Links are not clickable PDF annotations, but each line of a URL is one text run so most viewers detect it.
  • R graphics can’t switch on OpenType features such as tabular figures (tnum). Pick a font whose default figures are equal width (Source Sans 3 is) when numbers must line up.

Cairo and PDF rendering

export_pdf() uses cairo_pdf() when available, for full UTF-8 output and installed font weights, and measures text on the same kind of device so measured heights match the file.

Platform Cairo status
Windows Bundled with R
Linux Usually present; install libcairo2-dev if missing
macOS Requires XQuartz

Check with capabilities("cairo"). Without Cairo, bodybuildr falls back to pdf() and prints a message.

Contribute

Suggestions, bug reports and feature requests are welcome on the GitHub Issues page. To contribute code or fixes, open a pull request.