Skip to contents

tabular turns one pre-summarised, wide data frame into a publication-ready table and renders it natively to RTF, HTML, DOCX, PDF/LaTeX and Markdown — from a single spec, with no Java or Office dependency.

Two things to internalise up front:

  1. tabular is display-only. It never aggregates, filters, or computes statistics. You bring a summarised data frame (one input row = one display row); tabular lays it out and renders it. (Producing that frame from a cards ARD is the Data in article.)
  2. One immutable spec, built with verbs. You pipe a tabular() object through verbs (cols(), headers(), paginate(), preset(), …) and finish with emit(). Each verb returns a new spec; nothing renders until emit().

Your first table

Start from a wide frame — here the bundled demographics summary, one row per statistic, one column per treatment arm:

data(cdisc_saf_demo, package = "tabular")
head(cdisc_saf_demo)
#>      variable stat_label     placebo     drug_50    drug_100       Total
#> 1 Age (years)          n          86          96          72         254
#> 2 Age (years)  Mean (SD) 75.2 (8.59) 76.0 (8.11) 73.8 (7.94) 75.1 (8.25)
#> 3 Age (years)     Median        76.0        78.0        75.5        77.0
#> 4 Age (years)     Q1, Q3  69.2, 81.8  71.0, 82.0  70.5, 79.0  70.0, 81.0
#> 5 Age (years)   Min, Max      52, 89      51, 88      56, 88      51, 89
#> 6  Sex, n (%)          F   53 (61.6)   55 (57.3)   35 (48.6)  143 (56.3)

Describe the columns. The spec prints as a live HTML table — this is the same render emit() produces, shown inline:

spec <- tabular(
  cdisc_saf_demo,
  titles = c(
    "Table 14-2.01",
    "Demographic and Baseline Characteristics",
    "ITT Population"
  )
) |>
  cols(
    variable = col_spec(
      usage = "group",
      group_display = "header_row",
      label = ""
    ),
    stat_label = col_spec(label = "")
  )

spec

 

Table 14-2.01

Demographic and Baseline Characteristics

ITT Population

 

placebo drug_50 drug_100 Total
Age (years)
n 86 96 72 254
Mean (SD) 75.2 (8.59) 76.0 (8.11) 73.8 (7.94) 75.1 (8.25)
Median 76.0 78.0 75.5 77.0
Q1, Q3 69.2, 81.8 71.0, 82.0 70.5, 79.0 70.0, 81.0
Min, Max 52, 89 51, 88 56, 88 51, 89
 
Sex, n (%)
F 53 (61.6) 55 (57.3) 35 (48.6) 143 (56.3)
M 33 (38.4) 41 (42.7) 37 (51.4) 111 (43.7)
 
Race, n (%)
WHITE 78 (90.7) 90 (93.8) 62 (86.1) 230 (90.6)
BLACK OR AFRICAN AMERICAN 8 (9.3) 6 (6.2) 9 (12.5) 23 (9.1)
ASIAN 0 (0.0) 0 (0.0) 0 (0.0) 0 (0.0)
AMERICAN INDIAN OR ALASKA NATIVE 0 (0.0) 0 (0.0) 1 (1.4) 1 (0.4)

To write a file, hand the spec to emit(); the backend is chosen by the file extension (or an explicit format =):

out <- tempfile(fileext = ".rtf")
emit(spec, out) # RTF here; swap to .docx / .pdf / .html / .md
file.exists(out)
#> [1] TRUE

That is the whole loop: wide frame → tabular()cols()emit() — one spec, any backend.

The pipeline at a glance

Read it left to right. You summarise upstream — with cards/cardx, dplyr, or SAS — into a long ARD, widen that to a display-ready frame with pivot_across(), then hand it to tabular. Inside the package the work happens in three phases (Build → Resolve → Emit), and the same resolved spec emits to every backend, so the HTML you preview and the RTF you ship can never disagree.

An ADaM dataset is aggregated into a long ARD, widened by pivot_across into a wide data frame, then Build, Resolve, and Emit render that one spec to RTF, PDF, HTML, LaTeX, and DOCX.

tabular’s pipeline: summarise upstream into a long ARD, widen it with pivot_across(), then build, resolve, and emit one immutable spec to every backend.

Anatomy of a clinical table page

A submission table is not just a grid of numbers — it is a page with four stacked sections, and a reviewer expects each one in its place. Every tabular verb maps onto a piece of this picture:

A clinical table page split into header section, title lines, data section, and footnote lines, each annotated with the tabular verb that produces it.

The four-section clinical page and the verb that fills each section.
  • Header section — the running protocol, optional status, and page x of y, set as page chrome with preset(pagehead =, pagefoot =).
  • Title lines — the table number and up to four centred titles, passed to tabular(titles =).
  • Data section — an optional subgroup() banner, the column-header band built by cols() and headers(), then the decimal-aligned data.
  • Footnote lines — your static footnotes = plus any auto-numbered footnote() markers, then the program path, name, and timestamp.

preset() frames all four by controlling the page geometry (paper, orientation, margins, fonts).

Where to next

The rest of the docs are task-oriented — read the one that matches what you are doing: