| Title: | Render Tables and Listings for Clinical Submissions |
| Version: | 0.1.0 |
| Description: | Render clinical submission tables and listings to 'RTF', 'LaTeX', 'HTML', 'PDF', and 'DOCX' from pre-summarised data frames, with no external 'Java' or 'SAS' dependency. Features include decimal alignment via font metrics, multi-level column headers with passthrough leaves, predicate-targeted cell styling, footnotes, and group-aware pagination. Built for Clinical Data Interchange Standards Consortium (CDISC) Analysis Data Model (ADaM) workflows and regulatory submissions to agencies such as the Food and Drug Administration (FDA), European Medicines Agency (EMA), and Pharmaceuticals and Medical Devices Agency (PMDA). |
| License: | MIT + file LICENSE |
| URL: | https://vthanik.github.io/tabular/, https://github.com/vthanik/tabular |
| BugReports: | https://github.com/vthanik/tabular/issues |
| Encoding: | UTF-8 |
| Depends: | R (≥ 4.3.0) |
| Imports: | S7, cli, commonmark, rlang, xml2 |
| Suggests: | testthat (≥ 3.0.0), withr, digest, ggplot2, htmltools, knitr, quarto, pkgdown, rstudioapi, systemfonts, tibble, tinytex, webshot2, yaml |
| VignetteBuilder: | quarto |
| SystemRequirements: | Quarto command line tool (<https://github.com/quarto-dev/quarto-cli>), needed only to build the package vignettes. |
| Config/testthat/edition: | 3 |
| Config/Needs/website: | pkgdown |
| LazyData: | true |
| Config/roxygen2/version: | 8.0.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-06-05 20:23:01 UTC; vignesh |
| Author: | Vignesh Thanikachalam [aut, cre, cph] |
| Maintainer: | Vignesh Thanikachalam <about.vignesh@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-06-11 12:30:02 UTC |
tabular: Render Tables and Listings for Clinical Submissions
Description
Render clinical submission tables and listings to 'RTF', 'LaTeX', 'HTML', 'PDF', and 'DOCX' from pre-summarised data frames, with no external 'Java' or 'SAS' dependency. Features include decimal alignment via font metrics, multi-level column headers with passthrough leaves, predicate-targeted cell styling, footnotes, and group-aware pagination. Built for Clinical Data Interchange Standards Consortium (CDISC) Analysis Data Model (ADaM) workflows and regulatory submissions to agencies such as the Food and Drug Administration (FDA), European Medicines Agency (EMA), and Pharmaceuticals and Medical Devices Agency (PMDA).
Author(s)
Maintainer: Vignesh Thanikachalam about.vignesh@gmail.com
See Also
Useful links:
Report bugs at https://github.com/vthanik/tabular/issues
Convert a tabular_spec to an htmltools tagList
Description
Renders the spec to a self-contained HTML fragment and wraps
it in an htmltools::tagList suitable for inline embedding in
Quarto / Rmd chunks, RStudio / Positron viewer panes,
pkgdown reference pages, and Shiny UIs.
Usage
## S3 method for class 'tabular_spec'
as.tags(x, ..., id = NULL)
Arguments
x |
The |
... |
Reserved. Ignored. |
id |
Wrapping div id. |
Details
Fragment extraction. Tabular's HTML backend emits a full
<!DOCTYPE html> document with a <style> block in the head
and the table inside <body>. For inline embedding we
extract the <style> and <body> content separately and re-
wrap them in an htmltools::tagList:
<style>...table CSS...</style> <div id="..." style="overflow-x:auto;max-width:100%;"> ...table content... </div>
The wrapping <div> gets a random unique id (so multiple
tables on the same page have CSS-scopable hooks) and
overflow-x: auto so wide tables get a horizontal scrollbar
instead of overflowing their container.
Value
An htmltools::tagList containing a <style>
block plus a wrapping <div> containing the table. Knitr,
htmltools, and RStudio / Positron viewer panes all know how
to render it.
See Also
Renders via: print.tabular_spec, knit_print().
Terminal verb: emit().
Examples
# `as.tags()` converts a spec into an htmltools tagList you can drop into
# a custom HTML page, a Shiny UI, or a Quarto / Rmd chunk. `print()` and
# `knit_print()` call it under the hood, so you seldom call it directly --
# but it is the seam for composing several tables into one container.
s1 <- tabular(cdisc_saf_demo, titles = "Demographics")
s2 <- tabular(cdisc_saf_ae, titles = "AE overall")
# Compose two tables into one parent tagList. Autoprinting `tables` in a
# Quarto / Rmd chunk renders both inline (via knit_print); embed it with
# htmltools::save_html() or a Shiny renderUI().
tables <- htmltools::tagList(
htmltools::as.tags(s1),
htmltools::as.tags(s2)
)
# The common path is autoprinting a spec: the viewer at an interactive
# prompt, an inline live table under pkgdown / knitr, and HTML source
# under R CMD check. This is the gt / flextable / tinytable convention --
# end on a bare table object and let the registered print method choose,
# with no browsable() / if (interactive()) wrapper, so R CMD check never
# launches a browser.
s1
Resolve a tabular_spec into a tabular_grid
Description
Runs the full engine pipeline against spec and returns the
resolved tabular_grid — the same intermediate object emit()
hands to a backend. Pure function: no files written, no global
state touched. Use this during development to inspect what
emit() will pass downstream, when building a custom backend,
or when piping the resolved grid into a non-file consumer (e.g. an
inline preview chunk in a Quarto notebook).
Usage
as_grid(.spec)
Arguments
.spec |
The |
Details
Engine pipeline order is load-bearing. Phases run in this fixed order; the order matters because each phase reads the post- previous-phase state of the spec:
-
engine_sort()— apply the sort spec. -
engine_headers()— validate the header tree and flatten it to a band grid. -
engine_style()— evaluate every style predicate against the post-sort data grid. A predicate may reference any column inspec@data. -
engine_format()— apply per-column formats, substitutena_text, and parse every cell / title / footnote / label throughparse_inline()to itsinline_ast. -
engine_decimal()— column-wide decimal alignment for any column flaggedcol_spec(align = "decimal"). Operates on the formatted text; output is the same character matrix with NBSP padding inserted so the decimal marks line up. -
engine_paginate()— split into pages (vertical row chunks + horizontal panel chunks). The plan drives the per-page slicing of cells / styles / ASTs below.
The grid is the backend contract. Every backend
(backend_md, future backend_html, etc.) consumes a
tabular_grid — never a tabular_spec. New backends only need
to walk grid@pages and grid@metadata; the engine pipeline is
a fixed dependency they never re-implement.
No I/O. as_grid() writes nothing to disk and touches no
global state. It is safe to call repeatedly during interactive
exploration; cost is roughly that of one emit() without the
backend write step.
Value
A tabular_grid S7 object. Two slots:
-
@pages— a list of one entry per display page. Each entry is a named list with pagination fields (page_index,panel_index,is_continuation,continuation,show_titles,repeat_headers,show_footnotes_here), row + column slice indices (row_indices,col_indices,col_names), the sliced cell text (cells_text— character matrix), sliced inline ASTs (cells_ast— list-matrix ofinline_ast), sliced style nodes (cells_style— list-matrix ofstyle_node), and the column-label ASTs for the visible columns (col_labels_ast). -
@metadata— per-table information backends consume once per render:format(the resolved backend tag,NA_character_foras_grid()calls),rows_per_page,total_pages,total_panels,nrow_data,ncol_data,col_names,cols(the originalcol_spec()entries keyed by column name),headers(the flattened header band grid),titles,footnotes,titles_ast,footnotes_ast,col_labels_ast,pagehead_ast/pagefoot_ast(resolved page-band content —NULLwhen the active preset declares no band, otherwiselist(left, center, right)of length-N lists ofinline_astwhere N = row count and index 1 is the body-edge row).
See Also
I/O sibling: emit() writes the resolved grid to a file
via a registered backend; as_grid() is the no-I/O entry into
the same pipeline.
Build verbs the pipeline feeds from: tabular(),
cols() / col_spec(), headers(), sort_rows(),
style(), paginate(), preset().
Inline formatting helpers: md(), html().
Examples
# ---- Example 1: Demographics — inspect the resolved grid ----
#
# Resolve the canonical safety-pop demographics pipeline into a
# `tabular_grid` and inspect what `emit()` would hand a backend.
# The first page's `cells_text` matrix is the decimal-aligned
# output as the backend would render it; the metadata carries the
# pagination plan + header / title / footnote ASTs.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
demo <- tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
),
footnotes = "Source: ADSL."
) |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("variable", "stat_label"))
demo_grid <- as_grid(demo)
demo_grid@metadata$total_pages
demo_grid@pages[[1]]$cells_text[1:3, c("stat_label", "placebo")]
# ---- Example 2: AE-by-SOC/PT paginated grid — verify the split ----
#
# Same shape as Example 1 plus pagination protecting the SOC
# grouping. With a tight font size the grid carries multiple page
# entries; concatenating each page's `row_indices` reconstructs
# the full data, and every page carries the full header band grid
# at `grid@metadata$headers` so backends can re-render the header
# on every continuation page.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
ae_spec <- tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by SOC and Preferred Term",
"Safety Population"
),
footnotes = "Subjects counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(usage = "group", visible = FALSE,
group_display = "column_repeat"),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE)) |>
paginate(keep_together = "soc")
ae_grid <- as_grid(ae_spec)
length(ae_grid@pages)
# ---- Example 3: Subgroup partition — one page set per group ----
#
# When `subgroup()` is attached, `as_grid()` runs the resolve
# pipeline once per group and concatenates the pages. `cdisc_saf_subgroup`
# carries `sex` as a natural partition axis; inspect
# `@pages[[i]]$subgroup_index` and `@pages[[i]]$subgroup_line_ast`
# to confirm each page knows its group identity and banner text.
# `sex` auto-hides as the partition `by` column; no explicit
# `col_spec(visible = FALSE)` needed.
sg_spec <- tabular(cdisc_saf_subgroup) |>
cols(
agegr = col_spec(usage = "group", label = "Age Group"),
sex_n = col_spec(visible = FALSE),
agegr_n = col_spec(visible = FALSE),
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic")
) |>
subgroup("sex")
sg_grid <- as_grid(sg_spec)
length(sg_grid@pages)
vapply(sg_grid@pages, function(p) p$subgroup_index %||% NA_integer_, integer(1))
# ---- Example 4: Pre-flight inspection before emit() ----
#
# Resolve a spec to its grid without writing anywhere. Useful in
# tests, for snapshotting cell text under different presets, or
# for spec-introspection inside higher-level wrappers that need
# to know how many pages a render will produce.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
demog_spec <- tabular(
cdisc_saf_demo,
titles = "Demographics"
) |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
),
Total = col_spec(
label = "Total\nN={n['Total']}",
align = "decimal"
)
)
grid <- as_grid(demog_spec)
length(grid@pages)
dim(grid@pages[[1]]$cells_text)
Border-line specification
Description
Build a small immutable record describing one border line —
width, style, and colour. A brdr() value is the stroke you hand
to the preset() rules knob (one entry per rule name, e.g.
rules = list(midrule = brdr(width = 0.75))) or to style()'s
border arguments (style(border_top = brdr(...), .at = cells_table(side = "rows"))). Successive preset() calls layer
cleanly, so a one-off override composes onto a house-style template
without disturbing the other rules.
Usage
brdr(width = "thin", style = "solid", color = "ink")
is_brdr(x)
Arguments
width |
Stroke width. |
style |
Line style. |
color |
Stroke colour. |
x |
Any R object — tested by |
Details
Surface. A single tabular_brdr value is a length-3 named
list with class "tabular_brdr": list(style, width, color).
The shape is identical to the bare triple
style()'s per-side scalars accept, so the resolver in
R/borders.R can ingest either form transparently. Construct
with brdr(); test with is_brdr().
Width keywords. width accepts either a numeric in points
(typical clinical values: 0.25, 0.5, 1, 1.5) or one of the four
named keywords:
| keyword | points |
"hairline" | 0.25 |
"thin" | 0.5 |
"medium" | 1 |
"thick" | 1.5 |
Keywords resolve to numeric points immediately; the constructed
value carries a numeric width. Numeric inputs pass through
unchanged after a non-negative check.
Style enum. style is one of "solid" (default),
"dashed", "dotted", "double", "dashdot", "none".
"none" is the explicit clear-this-rule sentinel: setting a rule
to brdr(style = "none") (or the bare string "none") in
preset()(rules = list(...)) suppresses the baseline rule that
backend would otherwise draw.
Color. Hex ("#212529"), CSS colour name ("black",
"slategray"), the "ink" token (default; resolves to the
primary rule ink #212529, decoupled from the surrounding text
colour so a recoloured header keeps a neutral rule), or
"currentColor" (inherit the surrounding text colour per backend
convention — w:color="auto" in DOCX, the document text colour in
RTF, the CSS currentColor keyword in HTML).
Value
A tabular_brdr S3 object — a length-3 named list
suitable for preset(rules = list(<rule> = .)) or style(border_* = .).
See Also
Where to attach: preset()'s rules knob (one brdr() per
rule name) and style()'s border_* arguments.
Per-cell predicates: style() accepts the same per-side
border_<side>_{style,width,color} triples without going through
brdr().
Resolver internals: tabular_classes (style_node's 12
border scalars).
Examples
# ---- Example 1: A house-style rule set ----
#
# The `rules` knob takes one brdr() value per rule name. Here a
# thick column-label divider (midrule), a hairline dotted rule
# between body rows (rowrule), and the muted spanner rule dropped.
# Unlisted rules keep their booktabs defaults.
demo_n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_ae,
titles = c(
"Table 14.3.1",
"Overall Summary of Adverse Events",
"Safety Population"
),
footnotes = "Subjects counted once per category."
) |>
cols(
stat_label = col_spec(usage = "group", label = "Category"),
placebo = col_spec(label = "Placebo\nN={demo_n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={demo_n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={demo_n['drug_100']}"),
Total = col_spec(label = "Total\nN={demo_n['Total']}")
) |>
preset(
rules = list(
midrule = brdr(width = "thick"),
rowrule = brdr(width = "hairline", style = "dotted"),
spanrule = "none"
)
)
# ---- Example 2: Wrap a custom style into a reusable function ----
#
# The recommended way to share a rule style across many tables is to
# wrap the `preset()` call in a small function. A later `preset()` /
# `style()` call layers a one-off override cleanly on top.
custom_style <- function(spec) {
spec |>
preset(
rules = list(
toprule = brdr(width = "thin", color = "#212529"),
midrule = brdr(width = "thin", color = "#212529"),
bottomrule = brdr(width = "thin", color = "#212529")
)
)
}
tabular(cdisc_saf_n) |>
custom_style() |>
preset(rules = list(rowrule = brdr("hairline", "dashed")))
# ---- Example 3: Width keyword vs numeric, every style enum value ----
#
# Width accepts both the four named keywords and a bare numeric
# in points; style accepts six enum values. Use `is_brdr()` to
# confirm the constructor returned a valid `tabular_brdr` rather
# than a fallback list.
for (w in c("hairline", "thin", "medium", "thick")) {
cat(w, "=", brdr(width = w)$width, "pt\n")
}
is_brdr(brdr(width = 0.75))
lapply(
c("solid", "dashed", "dotted", "double", "dashdot", "none"),
function(s) brdr(style = s)
)
# ---- Example 4: A full grid via the body-edge style() path ----
#
# The `rules` knob covers the named booktabs anatomy; for the body
# outer frame and inter-column separators, hand brdr() to
# `style(.at = cells_table(side = ...))`. Here a medium outer frame
# plus hairline column separators on a demographics table.
tabular(cdisc_saf_demo, titles = "Demographics with a full grid") |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
style(border = brdr(width = "medium"), .at = cells_table(side = "outer")) |>
style(border_left = brdr("hairline"), .at = cells_table(side = "cols"))
Treatment-effect estimates by model
Description
Four competing efficacy models with their treatment-effect point
estimate, 95% confidence-interval bounds, and nominal p-value.
Shaped as a numeric-cell table (one row per model) rather than the
usual pre-formatted character cells, so it exercises the
col_spec(format = ...) + col_spec(na_text = ...) cascade. One
row (MMRM) carries NA CI bounds to demonstrate na_text.
Usage
cdisc_eff_estimates
Format
A data frame with 4 rows and 5 columns:
modelModel name (
"ANCOVA","MMRM","Cox PH","Bootstrap (1000 reps)").estimateNumeric point estimate.
lower_ci,upper_ciNumeric 95% CI bounds. The MMRM row has
NAbounds.p_valueNominal p-value (numeric).
Source
Synthetic estimates following the
_archive/.../arframe-examples/tables/tte-summary.qmd and
efficacy-bor.qmd shapes. Not derived from any patient-level
data — illustrative values only.
See Also
col_spec() for the formatting cascade these values
exercise.
Examples
# Numeric-cell efficacy table — format = "%.2f" pins precision,
# na_text = "--" renders the MMRM row's NA bounds as dashes.
tabular(cdisc_eff_estimates, titles = "Treatment-effect estimates by model") |>
cols(
model = col_spec(usage = "group", label = "Model", valign = "top"),
estimate = col_spec(label = "Estimate", align = "decimal",
format = "%.2f"),
lower_ci = col_spec(label = "Lower\n95% CI", align = "decimal",
format = "%.2f", na_text = "--"),
upper_ci = col_spec(label = "Upper\n95% CI", align = "decimal",
format = "%.2f", na_text = "--"),
p_value = col_spec(label = "p-value", align = "decimal",
format = "%.4f")
)
Efficacy-population BigN per arm
Description
Per-arm subject counts (BigN) for the efficacy population used by
cdisc_eff_resp / eff_resp_card — subjects with a BOR record in
pharmaverseadam::adrs_onco. Same two-column naming convention
as cdisc_saf_n; the totals differ from cdisc_saf_n because not every
safety-pop subject contributes a best-overall-response record.
Usage
cdisc_eff_n
Format
A data frame with 4 rows and 3 columns; same schema as
cdisc_saf_n (arm, arm_short, n).
Source
Derived in data-raw/bundle-demo.R from the per-arm BOR
denominator computed inside the cdisc_eff_resp pipeline.
See Also
cdisc_saf_n for the safety-population counterpart.
Examples
# Efficacy BigN joined into column headers.
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
col_spec(label = "Placebo\nN={ne['placebo']}")@label
Best Overall Response and Response Rates
Description
Pre-summarised efficacy table. Per-arm counts of best overall
response (BOR) per CDISC category, plus derived ORR, CBR, and DCR
rate rows each followed by an exact (Clopper-Pearson) 95% CI row.
Four sections (Best Overall Response, Objective Response Rate,
Clinical Benefit Rate, Disease Control Rate) are encoded via the
groupid + group_label pair so a single usage = "group" /
group_display = "header_row" on group_label synthesises one
bold section band per groupid block; the body rows render below
each band via usage = "indent" on stat_label.
Usage
cdisc_eff_resp
Format
A data frame with 13 rows and 7 columns:
stat_labelRow label (
"CR","PR","SD","NON-CR/NON-PD","PD","NE","MISSING","ORR (CR + PR)","95% CI (Clopper-Pearson)","CBR (CR + PR + SD)","95% CI (Clopper-Pearson)","DCR (CR + PR + SD + NON-CR/NON-PD)","95% CI (Clopper-Pearson)").row_type"category"for BOR categorical rows,"derived"for ORR / CBR / DCR rate rows,"ci"for the paired confidence-interval rows. Hide viacol_spec(visible = FALSE).placebo,drug_50,drug_100Per-arm cell text (
"n (pct)"on rate rows,"(lower, upper)"on CI rows).groupidInteger section id (1 = Best Overall Response, 2 = Objective Response Rate, 3 = Clinical Benefit Rate, 4 = Disease Control Rate). Hide via
col_spec(visible = FALSE); used as the section sort / partition key.group_labelCharacter section label, repeating across every row of its groupid block ("Best Overall Response" x7, "Objective Response Rate" x2, ...). Drives the engine's
usage = "group"header_row synthesis when paired withgroup_display = "header_row".
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::adrs_onco filtered to PARAMCD == "BOR".
See Also
cdisc_eff_n for BigN denominators.
Examples
# 95% efficacy pattern: four bold section bands (Best Overall
# Response / Objective Response Rate / Clinical Benefit Rate /
# Disease Control Rate), each followed by indented stat rows. The
# source already ships in the right display order, so no sort step
# is needed; `group_label` repeats across every row of its section
# so the engine's `header_row` mode emits exactly one band per
# section.
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
cdisc_eff_resp,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
)
) |>
cols(
group_label = col_spec(usage = "group", group_display = "header_row"),
stat_label = col_spec(usage = "indent", label = "Response"),
groupid = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
placebo = col_spec(
label = "Placebo\nN={ne['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={ne['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={ne['drug_100']}",
align = "decimal"
)
)
Overall adverse-event summary, Safety Population
Description
Pre-summarised wide-format AE overview. Two clinical blocks:
high-level flag rows (any TEAE, any SAE, any treatment-related,
any AE leading to death, any AE recovered / resolved) and
maximum-severity rows (mild / moderate / severe). Severity rows
are indented with two leading spaces so a single
cols(stat_label = col_spec(usage = "group")) declaration drives
both the block-header rows and the indented detail rows.
Usage
cdisc_saf_ae
Format
A data frame with 8 rows and 5 columns:
stat_labelRow label (
"Any TEAE","Any Serious AE (SAE)","Any AE Related to Study Drug","Any AE Leading to Death","Any AE Recovered / Resolved"," Maximum severity: Mild"," Maximum severity: Moderate"," Maximum severity: Severe").placeboPlacebo arm cell text (
"n (pct)").drug_50Drug 50 arm cell text.
drug_100Drug 100 arm cell text.
TotalPooled-across-arms cell text.
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::adae filtered to SAFFL == "Y" and
TRTEMFL == "Y".
See Also
cdisc_saf_n for BigN denominators; cdisc_saf_aesocpt for the SOC / PT detail companion.
Examples
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_ae,
titles = c(
"Table 14.3.0",
"Adverse Event Overview",
"Safety Population"
)
) |>
cols(
stat_label = col_spec(usage = "group", label = ""),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
),
Total = col_spec(
label = "Total\nN={n['Total']}",
align = "decimal"
)
)
Adverse events by System Organ Class and Preferred Term
Description
Pre-summarised AE-by-SOC/PT table. Interleaved row order: overall
"any TEAE" row first, then per-SOC blocks where each SOC row is
followed by its preferred-term detail rows. Top 10 SOCs and top
5 PTs per SOC are kept; row_type marks the role of each row and
indent_level carries the canonical depth (0 for overall and SOC,
1 for PT) so the downstream pipeline drives the SOC -> PT indent
via col_spec(indent_by = "indent_level") without reconstructing
it in every script. The richer SOC × PT slice exercises
paginate() and the engine's horizontal-panel splitter end-to-end
on a realistic submission shell.
Usage
cdisc_saf_aesocpt
Format
A data frame with 61 rows and 10 columns:
socSystem Organ Class label. Repeats across the SOC's PT rows; hide via
col_spec(visible = FALSE)oncelabelcarries the same SOC text on SOC rows.labelThe row's display label. Equal to
socon the overall and SOC-summary rows; equal to the preferred-term name on PT detail rows. Promoted to the primary display column — pair withindent_by = "indent_level"to drive the SOC -> PT indent.row_typeOne of
"overall","soc","pt". Partition marker; hide viacol_spec(visible = FALSE).indent_levelInteger depth (0 on overall and SOC rows, 1 on PT rows). Consumed by
col_spec(indent_by = "indent_level")on thelabelcolumn; the engine auto-hides this column at resolve time.n_totalInteger. The row's own subject count — overall TEAE count on the overall row, the SOC's count on each SOC row, the PT's count on each PT row. Inner sort key.
soc_nInteger. The parent SOC's count, broadcast to every row in that SOC's cluster (SOC row + its PT children) so a descending sort on
soc_nkeeps PTs grouped under their parent. On the overall row, equal to the overall TEAE count. Outer sort key.placeboPlacebo arm cell text (
"n (pct)").drug_50,drug_100Drug arms cell text.
TotalPooled-across-arms cell text.
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::adae. Filtered to the top 10 SOCs by total
incidence and the top 5 PTs per SOC. Body rows are pre-sorted
with the cards-style two-level rule
(arrange(desc(soc_n), soc, desc(n_total))) so the canonical
render order is already baked in; the render-time
sort_rows(by = c("soc_n", "n_total"), descending = c(TRUE, TRUE))
reproduces it via stable sort.
See Also
cdisc_saf_aesocpt_ard for the hierarchical long ARD; cdisc_saf_n for BigN denominators.
Examples
# 95% safety pattern: SOC/PT table where `label` carries SOC text
# on SOC rows and PT text on PT rows, indented by `indent_level`.
# `soc` / `row_type` / `n_total` / `soc_n` ride along as hidden
# partition + sort keys. `sort_rows(soc_n, n_total)` clusters PTs
# under their parent SOC and orders both levels by descending count.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_aesocpt,
titles = c(
"Table 14.3.1",
"Adverse Events by SOC and Preferred Term",
"Safety Population"
)
) |>
cols(
label = col_spec(
label = "SOC / PT",
indent_by = "indent_level",
align = "left"
),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
),
Total = col_spec(
label = "Total\nN={n['Total']}",
align = "decimal"
)
) |>
sort_rows(
by = c("soc_n", "n_total"),
descending = c(TRUE, TRUE)
)
Cards hierarchical ARD for AEs by SOC and PT
Description
Long-format companion to cdisc_saf_aesocpt. Produced by
cards::ard_stack_hierarchical() over (AEBODSYS, AEDECOD) with
adsl-level denominators, sorted by descending overall incidence
via cards::sort_ard_hierarchical(). Limited to the same top-10
SOC, top-5 PT subset as cdisc_saf_aesocpt so the two datasets describe
the same slice of the data.
Usage
cdisc_saf_aesocpt_ard
Format
A card-classed tibble. Carries an
..ard_hierarchical_overall.. sentinel row that
pivot_across() passes through as the table's "overall" row.
Details
This is the package's canonical hierarchical ARD demo
(two grouping variables nested SOC -> PT). Its flat counterpart is
cdisc_saf_demo_ard; together they cover both shapes pivot_across()
must handle.
Source
Derived in data-raw/bundle-demo.R via
cards::ard_stack_hierarchical() over
pharmaverseadam::adae filtered to the top SOC / PT subset.
See Also
pivot_across() for the long-to-wide bridge;
cdisc_saf_aesocpt for the wide companion.
Examples
# Hierarchical ARD pivot. pivot_across() recognises the
# ard_stack_hierarchical shape and emits soc / label / row_type.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
cdisc_saf_aesocpt_ard |>
pivot_across(statistic = "{n} ({p}%)") |>
tabular(
titles = c(
"Table 14.3.1",
"Adverse Events by SOC and PT",
"Safety Population"
)
) |>
cols(
label = col_spec(label = "SOC / PT", align = "left"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
`Placebo` = col_spec(align = "decimal"),
`Xanomeline Low Dose` = col_spec(align = "decimal"),
`Xanomeline High Dose` = col_spec(align = "decimal")
)
Demographics summary, Safety Population
Description
Pre-summarised wide-format demographics suitable for direct
passing into tabular(). One row per displayed statistic. Eight
parameter blocks:
Usage
cdisc_saf_demo
Format
A data frame with 35 rows and 6 columns:
variableDisplay-block label (
"Age (years)","Age Group, n (%)","Sex, n (%)","Race, n (%)","Ethnicity, n (%)"). Driven bycols(usage = "group")to collapse repeat values at render.stat_labelStatistic or level label (
"n","Mean (SD)","Median","M","WHITE", ...).placeboPlacebo arm cell text.
drug_50Xanomeline Low Dose (50 mg) arm cell text.
drug_100Xanomeline High Dose (100 mg) arm cell text.
TotalPooled-across-arms cell text.
Details
continuous:
Age (years),Weight (kg),Height (cm),BMI (kg/m^2)— each emitted asn,Mean (SD),Median,Q1, Q3,Min, Maxcategorical:
Age Group,Sex,Race,Ethnicity,BMI Category— each level rendered asn (%)
Shaped for the display-only contract: every cell is the final string that will appear in the rendered table.
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::adsl filtered to SAFFL == "Y" and the three
CDISCPILOT01 treatment arms. Baseline Weight / Height / BMI are
joined in from pharmaverseadam::advs.
See Also
cdisc_saf_demo_ard for the long-format ARD companion; cdisc_saf_n for the matching BigN denominators.
Examples
# 95% safety pattern: demographics table with BigN-embedded
# column labels and CDISC-canonical statistic order.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
)
) |>
cols(
variable = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
),
Total = col_spec(
label = "Total\nN={n['Total']}",
align = "decimal"
)
)
Cards ARD for demographics (flat ARD companion)
Description
The same demographics summary as cdisc_saf_demo, but in the long
Analysis Results Data (ARD) format produced by
cards::ard_stack(). One row per (treatment arm, variable,
statistic). Shipped as a teaching dataset that shows the upstream
shape users typically have when they start from cards. Convert
it to the wide form tabular() accepts via pivot_across() —
tabular itself does not consume the long ARD format, since
pre-summarised wide data is the package boundary.
Usage
cdisc_saf_demo_ard
Format
A card-classed tibble with columns group1,
group1_level, variable, variable_level, context,
stat_name, stat_label, stat. group1 == "TRT01A" and
group1_level carries the original pharmaverseadam arm labels
("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose").
cards::ard_stack(.overall = TRUE) adds overall rows with
group1_level = NA; pivot_across() renders those into a
Total column.
Details
Continuous variables: AGE, WEIGHT, HEIGHT, BMI (each
emitting N, mean, sd, median, p25, p75, min, max).
Categorical variables: AGEGR1, SEX, RACE, ETHNIC,
BMI_CAT (each emitting n, N, p).
This is the package's canonical flat ARD demo. Its hierarchical
counterpart is cdisc_saf_aesocpt_ard; together they cover both shapes
pivot_across() must handle.
Source
Derived in data-raw/bundle-demo.R via
cards::ard_stack(.by = "TRT01A", .overall = TRUE) over
pharmaverseadam::adsl.
See Also
pivot_across() for the long-to-wide bridge;
cdisc_saf_demo for the wide companion.
Examples
# 95% demographics pattern: cards ARD -> wide -> rendered table.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
cdisc_saf_demo_ard |>
pivot_across(
statistic = list(
continuous = "{mean} ({sd})",
categorical = "{n} ({p}%)"
),
label = c(AGE = "Age (years)", SEX = "Sex", RACE = "Race")
) |>
tabular(
titles = c(
"Table 14.1.1",
"Demographics",
"Safety Population"
)
)
Safety-population BigN per arm
Description
Per-arm subject counts (BigN) for the safety population, plus a
Total row. Use this table to embed BigN inline in column headers
with a glue-style {expr} template against cols(col_spec(label = ...)); there is no dedicated BigN field on col_spec because the
denominator already lives here in a discoverable, joinable form.
Usage
cdisc_saf_n
Format
A data frame with 4 rows and 3 columns:
armRaw pharmaverseadam arm label (
"Placebo","Xanomeline Low Dose","Xanomeline High Dose","Total"). Matchesgroup1_levelin the_cardARDs (so the pivot output's column names match asetNames(cdisc_saf_n$n, cdisc_saf_n$arm)lookup).arm_shortRenamed label (
"placebo","drug_50","drug_100","Total"). Matches the column names ofcdisc_saf_demo,cdisc_saf_ae,cdisc_saf_aesocpt, andcdisc_saf_vital.nInteger subject count.
Details
Two arm-naming columns are shipped side by side so the same table
can serve both the _card ARDs (raw pharmaverseadam labels in
group1_level) and the renamed wide datasets (snake-cased arm
column names).
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::adsl filtered to SAFFL == "Y" and the three
CDISCPILOT01 arms.
See Also
cdisc_eff_n for the efficacy-population counterpart.
Examples
# Use cdisc_saf_n$arm_short when joining into the wide datasets
# (cdisc_saf_demo, cdisc_saf_ae, cdisc_saf_aesocpt, cdisc_saf_vital).
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
col_spec(label = "Placebo\nN={n['placebo']}")@label
# Use cdisc_saf_n$arm when joining into pivot_across() output
# (column names match the raw pharmaverseadam arm labels).
n_arm <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm)
col_spec(label = "Placebo\nN={n_arm['Placebo']}")@label
Vital-signs subgroup summary by Sex and Age Group
Description
Pre-summarised vital-signs stats partitioned by sex (F / M)
and age group (<65 / >=65) at the End-of-Treatment visit. Two
parameters (Systolic BP, Diastolic BP) emit four statistic rows
each (n, Mean (SD), Median, Min, Max). Partition-constant
BigN columns (sex_n, agegr_n) ride alongside so banners can
inline the denominator via
subgroup(label = "Sex: {sex} (N = {sex_n})") without reaching for
a separate lookup.
Usage
cdisc_saf_subgroup
Format
A data frame with 32 rows and 11 columns:
sexFactor (
F/M).agegrFactor (
<65/>=65).sex_nInteger BigN — number of subjects in the partition row's sex (partition-constant; rides into the banner via
{sex_n}template tokens).agegr_nInteger BigN per age group.
paramcdCDISC parameter code (
SYSBP/DIABP).paramDecoded parameter name (
"Systolic BP (mmHg)","Diastolic BP (mmHg)").stat_labelStatistic label (
n,Mean (SD),Median,Min, Max).placebo,drug_50,drug_100,TotalPer-arm cell text.
Details
Designed for subgroup() and as_grid() examples: the two
partition axes plus the partition-constant BigN columns cover both
single-variable cohort-style partitions and the multi-variable
(sex × agegr) crossing.
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::advs filtered to SAFFL == "Y", the three
CDISCPILOT01 arms, the SYSBP / DIABP parameters, and the
End-of-Treatment visit.
See Also
cdisc_saf_n for BigN denominators; subgroup() for the verb
this dataset is designed for.
Examples
# 95% pattern: subgroup partition by sex with inline BigN.
# `sex` and `sex_n` auto-hide from the body: `sex` because it is
# the partition `by` column; `sex_n` because the banner template
# references it. No explicit `col_spec(visible = FALSE)` needed.
tabular(cdisc_saf_subgroup, titles = "Vital Signs at End of Treatment") |>
cols(
agegr = col_spec(usage = "group", label = "Age Group"),
agegr_n = col_spec(visible = FALSE),
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
subgroup(by = "sex", label = "Sex: {sex} (N = {sex_n})")
Vital-signs summary
Description
Pre-summarised vital-signs stats. Four parameters (SYSBP, DIABP,
PULSE, TEMP) at four visits (Baseline, Week 8, Week 16, End of
Treatment), each producing four statistic rows (n, Mean (SD),
Median, Min, Max). The 4 x 4 x 4 grid makes this dataset a
natural fit for paginate() examples — 64 rows comfortably exceed
a single page under typical clinical row-per-page settings.
Usage
cdisc_saf_vital
Format
A data frame with 64 rows and 7 columns:
paramcdCDISC parameter code (
SYSBP/DIABP/PULSE/TEMP). Repeats across visit and statistic; usecol_spec(usage = "group")to collapse.paramDecoded parameter name.
visitAnalysis visit label (
"Baseline"/"Week 8"/"Week 16"/"End of Treatment").stat_labelStatistic label.
placebo,drug_50,drug_100Per-arm cell text.
Source
Derived in data-raw/bundle-demo.R from
pharmaverseadam::advs.
See Also
cdisc_saf_n for BigN denominators.
Examples
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_vital,
titles = c(
"Table 14.4.1",
"Vital Signs Summary at Baseline and End of Treatment",
"Safety Population"
)
) |>
cols(
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
visit = col_spec(usage = "group", label = "Visit"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
)
)
Cell-location constructors for style()
Description
Build a tabular_location value naming one region of the rendered
table; pass the result to style()'s at argument. Each
constructor targets one surface (body, headers, footnotes, ...);
optional i / j / where / level / labels filters narrow
the target within that surface.
Usage
cells_body(i = NULL, j = NULL, where = NULL)
cells_headers(level = NULL, labels = NULL, j = NULL)
cells_group_headers(j = NULL, where = NULL)
cells_title()
cells_subgroup_labels()
cells_footnotes()
cells_pagehead(slot = NULL)
cells_pagefoot(slot = NULL)
cells_table(side = NULL, i = NULL, j = NULL)
is_tabular_location(x)
Arguments
i |
Row index filter. |
j |
Column index filter. |
where |
Predicate. An unquoted expression evaluating to a
length- |
level |
Header-band depth (for |
labels |
Header-band labels (for |
slot |
Band slot (for |
side |
Table edge / separator (for |
x |
Any R object — tested by |
Details
One surface per location. A tabular_location always names
exactly one of: body, headers, group_headers, title,
subgroup_labels, footnotes, pagehead, pagefoot, table.
Cross-surface styling layers in via multiple chained style()
calls (one per location).
Index vocabulary. Where supported, the i (rows) and j
(columns) arguments accept integer, logical, or character vectors
— matching the convention established by flextable
(bold(ft, i, j)) and tinytable (style_tt(i, j)). Character
vectors match against the data frame's column names (j) or row
labels (i); integers are 1-based positions; logicals broadcast
to nrow / ncol.
Predicate vocabulary. cells_body(where = pvalue < 0.05) is
the canonical data-driven filter — where is captured as an rlang
quosure and evaluated at engine time against the post-sort grid.
Mutually exclusive with i (you target either by index or by
predicate, not both).
Why cells_headers not cells_column_spanners. The verb that
builds the multi-level header tree is named headers(). The
location follows the same vocabulary: one word ("headers") covers
the entire column-header section — inner spanner bands AND the
leaf band of per-column labels. Pass level or labels to narrow.
Value
A tabular_location S3 list with slots surface, i,
j, where, labels, level, slot, side (unused slots
are NULL). Pass to style()'s at argument.
Surface filters
| constructor | filters |
cells_body(i, j, where) | row index / col index / predicate |
cells_headers(level, labels, j) | band depth / spanner label / cols |
cells_group_headers(j, where) | injected section rows |
cells_title() | (no filter — whole block) |
cells_subgroup_labels() | (no filter) |
cells_footnotes() | (no filter) |
cells_pagehead(slot) | "left" / "center" / "right" |
cells_pagefoot(slot) | "left" / "center" / "right" |
cells_table(side, i, j) | outer edge / row separator / etc. |
See Also
Verb that consumes locations: style().
Border value type: brdr().
Reusable house style: style_template().
Examples
# Whole body cells (the default for style())
cells_body()
# Row index 1:3, column "Total"
cells_body(i = 1:3, j = "Total")
# Data-driven subset
cells_body(where = stat_label == "Mean (SD)")
# Topmost spanner band only
cells_headers(level = 1)
# Leaf band (per-column labels)
cells_headers(level = -1)
# A specific spanner by label
cells_headers(labels = "Treatment Group")
# Section-header rows for col_spec(group_display = "header_row")
cells_group_headers()
# Title / footnotes blocks
cells_title()
cells_footnotes()
# Page-header / page-footer slots
cells_pagehead(slot = "left")
cells_pagefoot(slot = "right")
# Outer table frame
cells_table(side = "outer")
# Horizontal rules between body rows
cells_table(side = "rows")
Check font availability across backends
Description
Walks the resolved font fallback chain for each backend and reports which entries the local machine can find. Useful for answering "is the preview I'm seeing the same fonts the downstream reviewer will see?".
Usage
check_fonts(.spec)
Arguments
.spec |
A |
Details
The diagnostic does NOT change what emit() writes to the
file. Tabular's backends emit font names (CSS strings, LaTeX
\setmainfont commands, RTF font-table entries); the consuming
application (browser, LaTeX engine, Word, Adobe Reader) on the
opening machine resolves those names against its own installed
fonts. check_fonts() is purely informational — it tells you
which entries of the cross-platform fallback chain you can see
on this machine, so you can predict drift.
Status markers:
-
v— font is installed on this machine (viasystemfonts). -
o— font is a CSS / LaTeX generic; always resolvable by the consuming application. -
x— font is not installed on this machine; the consuming app on a different machine may or may not have it.
Requires the systemfonts package (in Suggests); call
install.packages("systemfonts") first if it isn't installed.
Value
Invisibly returns the resolved per-backend chains as a named list of character vectors. Side effect: prints a cli tree showing the availability marker for every entry.
See Also
Builds the spec: tabular(), preset().
Resolves the spec: as_grid(), emit().
Examples
# ---- Example 1: Inspect default font resolution ----
#
# Build a spec with the default font_family ("mono") and ask
# which entries in the cross-platform chain are findable
# locally. Useful before sharing a render with downstream
# reviewers who may be on a different OS.
spec <- tabular(
cdisc_saf_demo,
titles = "Demographics"
)
if (requireNamespace("systemfonts", quietly = TRUE)) {
check_fonts(spec)
}
# ---- Example 2: Diagnose a Courier New request ----
#
# A request for "Courier New" (a specific named font) renders
# on macOS / Windows but may fall back to a serif on Linux.
# `check_fonts()` flags this so the user knows to switch to
# the "mono" generic for portable output.
spec_mono <- tabular(
cdisc_saf_demo,
titles = "Mono request"
) |>
preset(font_family = "Courier New")
if (requireNamespace("systemfonts", quietly = TRUE)) {
check_fonts(spec_mono)
}
# ---- Example 3: Explicit cross-platform stack ----
#
# A length>1 input is treated as an explicit fallback chain and
# emitted verbatim — no alias lookup, no fabrication. Use this
# when the first choice is a sponsor / brand face that needs an
# honest fallback for reviewers who don't have it installed.
spec_brand <- tabular(cdisc_saf_demo) |>
preset(font_family = c("Inter", "Liberation Sans", "Arial", "sans"))
if (requireNamespace("systemfonts", quietly = TRUE)) {
check_fonts(spec_brand)
}
# ---- Example 4: Compare serif vs sans fallback chains ----
#
# Side-by-side check of the two generic families. Useful when
# deciding the house-style default: the serif chain leads with
# Liberation Serif (Linux-server-first); the sans chain leads
# with Liberation Sans. Both close with the backend's native
# fallback layer (CSS generic on HTML, Latin Modern on LaTeX).
if (requireNamespace("systemfonts", quietly = TRUE)) {
tabular(cdisc_saf_demo) |>
preset(font_family = "serif") |>
check_fonts()
tabular(cdisc_saf_demo) |>
preset(font_family = "sans") |>
check_fonts()
}
Check LaTeX-package availability for PDF output
Description
Reports, for every TeX package the LaTeX / PDF backend can emit,
whether it is present in the local TeX tree, and prints the exact
tinytex::tlmgr_install() call that installs any that are
missing. Run this before emit(spec, "out.pdf") on a fresh
machine to turn a cryptic mid-compile File 'tabularray.sty' not found into an up-front, actionable checklist.
Usage
check_latex(quiet = FALSE)
Arguments
quiet |
Suppress the printed cli report.
|
Details
The required set is a superset of every \\usepackage{} /
\\UseTblrLibrary{} directive the backend emits, across all
conditional branches (running headers / footers pull fancyhdr +
lastpage; xelatex pulls fontspec; pdflatex pulls the
classic font bundles). The check is informational, it does not
install anything.
OS-managed TeX Live gotcha. On Linux distributions that ship
TeX Live through the system package manager (RHEL / Fedora via
dnf, Debian / Ubuntu via apt), tlmgr is locked against
user installs and tlmgr_install() will fail. The fix is to
install a user-space TinyTeX with tinytex::install_tinytex()
and let that tree own the packages. Never force a locked tlmgr
with --ignore-warning: it leaves the system tree half-written.
Slow / stuck install (often Windows). The default CTAN
repository mirror.ctan.org redirects to a random mirror on
every call, and a slow or stale one makes tinytex::tlmgr_install()
appear to hang. Pin a concrete mirror once with
tinytex::tlmgr_repo()("auto") (it follows the redirect a
single time and remembers the result), then retry the install.
Status markers:
-
v— package is installed in the local TeX tree. -
x— package is missing; thetlmgr_install()line at the bottom of the report installs every missing package at once. -
?— availability could not be determined (notinytex, ortlmgrnot reachable); treated as missing for remediation.
Requires the tinytex package (in Suggests); call
install.packages("tinytex") first if it isn't installed.
Value
Invisibly returns a data frame with one row per
required package and columns package (<character>) and
installed (<logical>, NA when undeterminable). Side
effect: prints a cli report with a per-package status marker
and, when anything is missing, the exact tlmgr_install()
remedy.
See Also
Companion diagnostic: check_fonts().
Consumes the result: emit().
Examples
# ---- Example 1: Audit the PDF toolchain before emitting ----
#
# Run check_latex() on a fresh machine to confirm every LaTeX
# package the PDF backend needs is present. The call prints a
# status line per package and, if any are missing, the exact
# tinytex::tlmgr_install() command to fix them in one shot. It is
# guarded on tinytex so it is a no-op where TeX is unavailable.
if (requireNamespace("tinytex", quietly = TRUE)) {
check_latex()
}
Per-column display specification
Description
Build a single column's display attributes — usage, label, format,
visibility, width, alignment, NA text. The result feeds cols(),
which stamps the input column name onto the spec from its named-
argument position and attaches it to the parent tabular_spec.
Usage
col_spec(
usage = NULL,
label = NA_character_,
format = NULL,
visible = TRUE,
width = "auto",
group_display = "header_row",
group_skip = NA,
align = NULL,
valign = NULL,
na_text = NA_character_,
indent_by = NA_character_
)
Arguments
usage |
Engine role.
# Two row-label columns and four arm columns. cols( variable = col_spec(usage = "group"), stat_label = col_spec(usage = "group"), placebo = col_spec(), drug_50 = col_spec() ) # Section-band table: the `group_label` column drives section # headers; `stat_label` body rows auto-indent under each header # without an explicit depth column. cols( group_label = col_spec(usage = "group", group_display = "header_row"), stat_label = col_spec(usage = "indent", label = "Response"), placebo = col_spec(align = "decimal") ) # End-to-end ARD → wide → tabular pipeline. The cards ARD
# `cdisc_saf_demo_ard` is the long upstream input; `pivot_across()`
# widens to one column per arm and stamps an internal marker
# so [`sort_rows()`] can reject sort keys on those arm columns.
# `cols()` then attaches per-column display rules.
wide <- pivot_across(
cdisc_saf_demo_ard,
statistic = list(
continuous = c(N = "{N}", "Mean (SD)" = "{mean} ({sd})"),
categorical = "{n} ({p}%)"
)
)
tabular(wide, titles = "Demographics") |>
cols(
variable = col_spec(
usage = "group", label = "Characteristic"
),
stat_label = col_spec(
usage = "group", label = "Statistic"
),
Placebo = col_spec(align = "decimal"),
`Xanomeline High Dose` = col_spec(
label = "High Dose", align = "decimal"
),
`Xanomeline Low Dose` = col_spec(
label = "Low Dose", align = "decimal"
),
Total = col_spec(align = "decimal")
)
|
label |
Display label for the column header.
Restriction: Empty string and whitespace-only labels are
accepted here, unlike Supports glue-style Per-column token. # Two-line header with arm name and BigN from cdisc_saf_n.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
)
|
format |
Post-cell formatter.
Restriction: Character templates are probed with
# sprintf template vs. function form. col_spec(format = "%.1f") col_spec(format = function(x) formatC(x, format = "f", digits = 1, big.mark = ",")) |
visible |
Whether the column renders.
Interaction: Hidden columns are the standard pattern for
sort-key helpers ( Auto-hide. The depth column named by Break-only group column. A hidden |
width |
Column width — auto-sized, pinned, or proportional.
Tip: Mix freely. Pinned and percent widths take priority;
Restriction: Must be positive. Percent values must fall
in Cross-format semantics (gt convention). The width value
is the user's source-of-truth. HTML emits it verbatim into
Note: Merge sentinel. For the field-merge across repeated |
group_display |
How
Composition under multiple group columns. When more than
one # Demographics layout: variable as section header, stat_label # as visible suppressed column. cols( variable = col_spec(usage = "group", group_display = "header_row"), stat_label = col_spec(usage = "group", group_display = "column"), placebo = col_spec(label = "Placebo", align = "decimal") ) |
group_skip |
Insert a blank row between consecutive groups.
Interaction: When two or more columns have an effective
# Default: header_row mode auto-injects blanks between sections. col_spec(usage = "group", group_display = "header_row") # Override: keep the column visible (suppressed-value mode) but # still insert blank-row separators between value changes. col_spec(usage = "group", group_display = "column", group_skip = TRUE) # Override: section headers without the blank-row separator # (denser layout, used when vertical space is tight). col_spec(usage = "group", group_display = "header_row", group_skip = FALSE) # Break-only "spacer": pairs with visible = FALSE to drop a blank # line wherever a hidden marker changes, without rendering the # column or any header row. group_display is ignored when hidden. col_spec(usage = "group", group_skip = TRUE, visible = FALSE) |
align |
Horizontal alignment within the column.
Tip: Default behaviour. When |
valign |
Vertical alignment within the cell.
Tip: Set |
na_text |
Text substituted for Tip: Use a sentinel ( |
indent_by |
Name of a column in
Typical SOC / PT pattern (the bundled cols( label = col_spec(label = "Category", indent_by = "indent_level"), soc = col_spec(visible = FALSE), row_type = col_spec(visible = FALSE) ) Multi-depth nesting works the same way — values
Composes orthogonally with |
Details
Constructor-only. col_spec() does not know which input
column it belongs to until cols() stamps the name. Build
reusable specs as ordinary R objects (e.g.
arm_col <- col_spec(align = "decimal")) and apply them to
multiple inputs without restating the name.
Merge semantics across repeated cols() calls. When
cols() is called twice for the same column, the engine merges
field-by-field: a non-default value on the new spec overrides;
a default-valued field (NA / NULL / "" / TRUE) leaves the
existing field intact. Build a column's spec in stages without
re-stating earlier attributes.
Validation timing. Argument shapes are validated eagerly —
a malformed sprintf template is probed at construction
(sprintf(format, 0)) and fails fast at write time, not at
render time.
Value
A col_spec S7 object. Pass it to cols() keyed by
the input column name; the constructor itself does not stamp
a name.
See Also
Companion verb: cols() attaches col_spec entries to a
tabular_spec keyed by input column name.
Sibling build verbs: headers(), sort_rows(),
style(), paginate(), preset().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Inline label formatting: md(), html().
Examples
# ---- Example 1: Demographics with every col_spec field exercised ----
#
# Demographics table where every `col_spec` field is in play:
# the row-label columns are pinned to a fixed width and aligned
# left, the four arm columns embed BigN inline in the header,
# decimal-align numeric content, and render `NA` cells as "-".
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
),
footnotes = "Percentages based on N per treatment group."
) |>
cols(
variable = col_spec(
usage = "group", label = "Parameter",
width = 2.0, align = "left"
),
stat_label = col_spec(label = "Statistic", align = "left"),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal", na_text = "-"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal", na_text = "-"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal", na_text = "-"
),
Total = col_spec(
label = "Total\nN={n['Total']}",
align = "decimal", na_text = "-"
)
) |>
sort_rows(by = c("variable", "stat_label"))
# ---- Example 2: AE table with indented label + hidden helpers ----
#
# AE-by-SOC/PT table where `label` carries SOC and PT text under
# one column, indented by `indent_level`. Hidden helpers
# (`row_type`, `n_total`) drive the sort while staying off the
# rendered page. Demonstrates `indent_by` plus `visible = FALSE`
# for sort-only columns, fixed width on the wide label column, and
# decimal alignment on all four arm columns.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by SOC and Preferred Term",
"Safety Population"
)
) |>
cols(
label = col_spec(label = "SOC / Preferred Term",
indent_by = "indent_level", width = 2.5),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE))
# ---- Example 3: Format string + na_text for clean numeric display ----
#
# `cdisc_eff_estimates` ships four competing efficacy models with
# pre-computed numeric estimates, 95% CI bounds (NA on the MMRM
# row), and a nominal p-value. `format =` pins the printed
# precision; `na_text` renders the missing CI bounds as a dash
# rather than a literal "NA". `valign = "top"` keeps the multi-
# line cell text aligned to the top.
tabular(cdisc_eff_estimates, titles = "Treatment-effect estimates by model") |>
cols(
model = col_spec(usage = "group", label = "Model", valign = "top"),
estimate = col_spec(label = "Estimate", align = "decimal", format = "%.2f"),
lower_ci = col_spec(
label = "Lower\n95% CI",
align = "decimal",
format = "%.2f",
na_text = "--"
),
upper_ci = col_spec(
label = "Upper\n95% CI",
align = "decimal",
format = "%.2f",
na_text = "--"
),
p_value = col_spec(
label = "p-value",
align = "decimal",
format = "%.4f"
)
)
# ---- Example 4: Per-column width + halign override for vitals ----
#
# `width` accepts a numeric (inches), a CSS-style string ("1.5in",
# "20%"), or `"auto"`. Centering the visit column under a wider
# group-column setup demonstrates the alignment cascade —
# col_spec@align beats the engine default but yields to a more
# specific style() rule downstream.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_vital,
titles = "Vital Signs at Baseline and End of Treatment"
) |>
cols(
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter",
width = "1.6in"),
visit = col_spec(usage = "group", label = "Visit",
width = "1.2in", align = "center"),
stat_label = col_spec(label = "Statistic", width = "1.0in"),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal", width = "0.9in"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal", width = "0.9in"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal", width = "0.9in"
)
)
# ---- Example 5: Non-collapsing `id` stub for a panelled table ----
#
# `usage = "id"` marks `stat_label` ("n", "Mean", "SD", ...) as a
# row identifier: like `display` it shows on every row, but it also
# joins the stub, so it repeats on each horizontal panel created by
# `paginate(panels = 2)`. On HTML / Markdown (no page width) the
# panels collapse into one scrollable table with a "Panel 1 / Panel
# 2" header note; on RTF / Word each panel is its own page with the
# `variable` + `stat_label` stub repeated.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_demo,
titles = c("Table 14.1.1", "Demographics", "Safety Population")
) |>
cols(
variable = col_spec(usage = "group", group_display = "column",
label = "Parameter"),
stat_label = col_spec(usage = "id", label = "Statistic"),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
paginate(panels = 2)
Attach per-column specifications
Description
Add col_spec() entries to a tabular_spec. Each named argument
is one column: the name is the input column in .spec@data and the
value is the col_spec carrying that column's display attributes
(usage, label, format, alignment, width, visibility, NA text).
Columns not mentioned get a default col_spec() (usage = display)
at engine-validate time.
Usage
cols(.spec, ..., .default = NULL)
Arguments
.spec |
The |
... |
Named Restriction: Names must be unique within a single |
.default |
Fallback Interaction: Explicit # Decimal-align every arm column without listing each by name.
tabular(cdisc_saf_demo) |>
cols(
variable = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
.default = col_spec(align = "decimal")
)
|
Details
Sparse declaration. Declare only the columns whose attributes
differ from the default — a typical pipeline uses one cols()
call with one entry per non-default column.
Within-call duplicates warn. A duplicate name inside one
cols() call warns and "last value wins". To intentionally
override an attribute, use a second cols() call downstream and
let the merge rule below apply.
Value
The updated tabular_spec. Continue chaining with
headers(), sort_rows(), style().
Repeat-call merge semantics
When cols() is called more than once for the same column, the
engine merges the new col_spec into the existing one field-by-
field. A non-default value on the new spec overrides; a default-
valued field leaves the existing field intact. This lets you
build a column's spec in stages — declare the label-and-alignment
block up front, add the width once you know it fits, then attach
a sort key, all without re-stating earlier attributes. Essential
when generating specs programmatically (looping over arms,
layering a house-style helper).
Default values that do NOT override the existing field:
| field | default that does not override |
usage | NA_character_ |
label | NA_character_ |
format | NULL |
visible | TRUE |
width | NA_real_ |
align | NA_character_ |
na_text | NA_character_ (inherit preset)
|
# Three-stage build: label/usage first, alignment second, width # third. Each stage leaves earlier fields intact. tabular(cdisc_saf_demo) |> cols(variable = col_spec(usage = "group", label = "Parameter")) |> cols(variable = col_spec(align = "left")) |> cols(variable = col_spec(width = 2.0)) # Result: variable has usage="group", label="Parameter", # align="left", width=2.0 — all four fields set.
See Also
Companion constructor: col_spec() builds the per-column
DSL object that cols() attaches.
Sibling build verbs: headers(), sort_rows(),
style(), paginate(), preset().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: Demographics with arm BigN inline in headers ----
#
# Demographics table where the row-label columns sit on the left
# and the four treatment-arm columns embed BigN in the header
# label (drawn inline from the bundled `cdisc_saf_n` data frame). Every
# arm column is decimal-aligned so mixed-format cells like
# "5 (3.2%)" line up on the decimal mark.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
),
footnotes = "Percentages based on N per treatment group."
) |>
cols(
variable = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("variable", "stat_label"))
# ---- Example 2: BOR table with CDISC factor ordering and hidden helper ----
#
# Best Overall Response table where `stat_label` carries the
# canonical CDISC factor levels (driving the sort) and `row_type`
# is hidden — present in the data for the sort, absent from the
# rendered output via `col_spec(visible = FALSE)`.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}", align = "decimal")
) |>
sort_rows(by = c("groupid", "stat_label"))
# ---- Example 3: AE-by-SOC/PT with indented label + repeat-call merge ----
#
# `label` carries SOC text on SOC rows and PT text on PT rows;
# `indent_by = "indent_level"` indents the PT rows one level under
# their SOC. `soc`, `row_type`, and `n_total` ride along as hidden
# sort keys. A second `cols()` call later in the chain adds widths
# once the user knows the page geometry; the repeat-call merge
# preserves prior attributes (label, indent_by, align, visible)
# without restating them.
ae <- cdisc_saf_aesocpt
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
tabular(
ae,
titles = c("Table 14.3.1", "Adverse Events by SOC and Preferred Term")
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE)) |>
# Second `cols()` call: add widths after the rest of the spec
# is built. Repeat-call merge preserves prior attributes.
cols(
label = col_spec(width = "2.5in"),
placebo = col_spec(width = "0.9in"),
drug_50 = col_spec(width = "0.9in"),
drug_100 = col_spec(width = "0.9in"),
Total = col_spec(width = "0.9in")
)
# ---- Example 4: Compact AE-overall with pre-derived Active column ----
#
# Drop the per-arm columns and surface only the Total. Pre-compute
# the pooled "Active" column upstream (here `paste0(drug_50, " / ",
# drug_100)`) before piping into `tabular()`; `cols()` then just
# declares each column's display role. The same pattern handles
# any post-pivot derivation (`pivot_across() |> mutate(...) |>
# tabular()`).
ae <- cdisc_saf_ae
ae$active <- paste0(ae$drug_50, " / ", ae$drug_100)
tabular(
ae,
titles = c("Table 14.3.0", "Adverse Event Overview"),
footnotes = "Active = pooled Drug 50 + Drug 100 columns."
) |>
cols(
stat_label = col_spec(usage = "group", label = ""),
placebo = col_spec(label = "Placebo", align = "decimal"),
active = col_spec(label = "Active arms"),
drug_50 = col_spec(visible = FALSE),
drug_100 = col_spec(visible = FALSE),
Total = col_spec(label = "Total", align = "decimal")
)
Apply one column spec to many columns
Description
Field-merge a single col_spec() onto every column matched by
name or by a predicate. The vectorized companion to cols() for
the common case of a variable number of treatment-arm columns that
all share the same display rule (decimal alignment, a numeric
format), so you avoid do.call() / !!! splicing one named
argument per arm.
Usage
cols_apply(.spec, .cols, .col_spec)
Arguments
.spec |
The |
.cols |
Columns to match.
Restriction: Named columns must exist in |
.col_spec |
The spec to field-merge onto every match.
|
Details
Field-merge, not replace. cols_apply() reuses the same
field-by-field merge as repeated cols() calls: a non-default
field on .col_spec overrides; a default-valued field leaves any
prior attribute on the matched column intact. Set the shared rule
across arms first, then refine an individual arm with a later
cols() call (or the reverse).
Per-column label token. A label that references {.name} (or
its alias {.col}) inside a {expr} is resolved per matched
column, with .name and .col both bound to that column's name.
This makes a variable-N arm header a single declarative call instead
of a hand-written loop. The rest of the {expr} evaluates in the
calling environment, so a per-arm BigN looked up from a named vector
works directly:
n <- c(placebo = 86, drug_50 = 84, drug_100 = 84)
cols_apply(
spec, c("placebo", "drug_50", "drug_100"),
col_spec(label = "{.name}\n(N={n[.name]})", align = "decimal")
)
# placebo -> "placebo\n(N=86)" ; drug_50 -> "drug_50\n(N=84)" ; ...
The token is a plain-string feature; a label wrapped in md() /
html() is parsed eagerly and does not interpolate. A failing
token expression aborts naming the offending column.
width merge. width's default sentinel for the merge is
"auto": a later cols() / cols_apply() call carrying the default
width = "auto" leaves a previously pinned width intact (only an
explicit non-"auto" width overrides). Apply a shared width last to
broadcast it across arms.
Value
The updated tabular_spec. Continue chaining with
headers(), sort_rows(), style().
See Also
Companion verbs: cols() attaches per-column specs by name;
col_spec() builds the spec.
Sibling build verbs: headers(), sort_rows(),
style(), paginate(), preset().
Examples
# ---- Example 1: Decimal-align every arm column by name vector ----
#
# Demographics table whose treatment-arm columns are selected by a
# name vector (`grep()` against the data) and given one shared
# decimal-alignment spec, while the two row-label columns keep
# their own roles set with `cols()`.
arm_cols <- grep("^placebo$|^drug_|^Total$", names(cdisc_saf_demo), value = TRUE)
tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
)
) |>
cols(
variable = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic")
) |>
cols_apply(arm_cols, col_spec(align = "decimal")) |>
sort_rows(by = c("variable", "stat_label"))
# ---- Example 2: Select arm columns with a predicate ----
#
# Best Overall Response table. The arm columns are matched with a
# predicate over the column names; the hidden sort helpers and the
# response label are declared with `cols()`. The predicate scales
# to any number of arms without editing the call.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
)
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE)
) |>
cols_apply(
\(nm) nm %in% c("placebo", "drug_50", "drug_100"),
col_spec(align = "decimal")
) |>
sort_rows(by = c("groupid", "stat_label"))
Render a tabular_spec to a file
Description
Resolve spec through the engine pipeline, dispatch to the
backend registered for the chosen format, and (optionally) write
a QC data file and a CDISC ARS audit manifest alongside the
rendered artefact. emit() is the package's terminal verb — it
returns file invisibly so the call can sit at the bottom of a
pipe without losing the path.
Usage
emit(
.spec,
file,
format = NULL,
data_file = NULL,
manifest = FALSE,
create_dir = FALSE
)
Arguments
.spec |
The |
file |
Destination path for the rendered artefact.
Tip: Use |
format |
Explicit backend override.
|
data_file |
QC artefact writer.
Restriction: Returned-path extension must be # Three canonical sponsor patterns for the lambda.
data_file = \(f) paste0(tools::file_path_sans_ext(f), "_qc.csv")
data_file = \(f) file.path(
"validation",
paste0("val_", basename(tools::file_path_sans_ext(f)), ".csv")
)
data_file = \(f) file.path(
"rd",
paste0("rd_", basename(tools::file_path_sans_ext(f)), ".rds")
)
|
manifest |
Emit the CDISC ARS audit manifest sidecar.
|
create_dir |
Create the destination directory if it is missing.
|
Details
Validation before I/O. Every argument is validated and the
backend is resolved BEFORE the engine runs. An unsupported
extension, a malformed data_file path, or a missing backend
raises tabular_error_input without writing any file. A spec
that resolves cleanly but whose backend errors mid-write may
leave a partial file behind; this is the only failure mode that
touches disk.
Backend dispatch. The effective backend is resolved from the
file extension via the table below; the format argument always
wins when both are supplied. Each backend lives in its own
R/backend_<fmt>.R file and self-registers at package load time.
| extension(s) | format | backend |
.md, .markdown | md | GFM pipe table (Step 15; shipped) |
.html, .htm | html | self-contained Bootstrap 5 (planned) |
.tex, .latex | latex | tabularray (planned) |
.pdf | pdf | tinytex compile of LaTeX (planned) |
.rtf | rtf | RTF 1.9.1, native (shipped) |
.docx | docx | OOXML native, no JVM (planned) |
Unknown extensions, missing extensions, and formats with no
registered backend all raise tabular_error_input. The error
message lists the currently registered formats so the failure is
actionable.
data_file is sponsor-neutral. Pass an explicit path
("out/qc.csv") for a fixed location, or a lambda
(function(file) -> path) for sponsor-flexible naming. The
lambda receives the resolved render path so it can derive the QC
file from it (suffix, sibling folder, separate sponsor-styled
name). Recognised extensions on the returned path are .csv,
.tsv (alias: .txt), and .rds; anything else raises
tabular_error_input. The written data frame is the post-
sort_rows() / post-engine_decimal() wide grid — exactly
the cell text the backend wrote.
manifest = TRUE writes a sidecar. The audit manifest is
written to <file>.audit.yml next to the render (e.g. out.md
-> out.audit.yml). Keys are CDISC ARS LDM v1.0 Output verbatim:
id, name, programmingCode (best-effort git + R + platform
timestamp),
fileSpecifications(sha256 of every emitted artefact includingdata_file),displays/displaySections(Title / Header / Body / Footnote),referencedAnalyses(empty in v0.1; reserved for the mintverse handoff),x-tabular(rendering geometry, pagination, style trace, input provenance). Determinism contract: two consecutiveemit()calls are byte- identical except for therendered_atparameter timestamp; the YAML round-trips throughyaml::read_yaml()+yaml::write_yaml().
Pure dispatcher. emit() does not do any rendering itself;
it composes as_grid() with a backend writer. To inspect the
resolved grid without writing a file (during development, or to
build a custom downstream consumer), call as_grid() directly.
Value
The file path, invisibly. Use this when chaining
emit() into a downstream consumer that needs the resolved
path (e.g. printing the link in a Quarto chunk, copying the
sidecar manifest into an archive, attaching the render to a
submission folder builder).
See Also
No-I/O sibling: as_grid() returns the resolved grid
without writing a file — use during development to inspect what
emit() would hand a backend.
Build verbs the pipeline feeds from: tabular(),
cols() / col_spec(), headers(), sort_rows(),
style(), paginate(), preset().
Inline formatting helpers: md(), html() (titles,
footnotes, labels, cell text).
Examples
# ---- Example 1: Render demographics to Markdown ----
#
# Smallest possible emit: spec in, .md out. The backend is chosen
# from the file extension; the engine pipeline runs internally,
# then the registered md backend writes a GFM pipe table you can
# preview in any Markdown renderer. tempfile() keeps the example
# clean for `R CMD check`.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
demo <- tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
),
footnotes = "Source: ADSL."
) |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("variable", "stat_label"))
demo_md <- tempfile(fileext = ".md")
emit(demo, demo_md)
# ---- Example 2: Render + QC data + CDISC audit manifest ----
#
# The clinical double-programming pattern: render the table,
# write a QC CSV alongside it for an independent programmer to
# verify cell-for-cell, and emit the CDISC ARS audit manifest
# for submission packaging. The lambda derives the QC path from
# the render path so the sponsor's naming convention lives in one
# place.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
ae_spec <- tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by SOC and Preferred Term",
"Safety Population"
),
footnotes = "Subjects counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE))
ae_md <- tempfile(fileext = ".md")
emit(
ae_spec,
ae_md,
data_file = \(f) paste0(tools::file_path_sans_ext(f), "_qc.csv"),
manifest = TRUE
)
# ---- Example 3: Same spec, four backends — one-loop fan-out ----
#
# `emit()` dispatches by file extension, so the same spec can
# render to every backend in one loop. Useful for visual diffs
# across formats during development and for shipping a build
# artefact set (RTF for submission, HTML for review, PDF for the
# CSR appendix).
eff_spec <- tabular(cdisc_eff_resp, titles = "Best Overall Response") |>
cols(
stat_label = col_spec(usage = "group", label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal")
)
out_dir <- tempfile()
dir.create(out_dir)
for (ext in c(".html", ".rtf", ".tex", ".docx", ".md")) {
emit(eff_spec, file.path(out_dir, paste0("eff", ext)))
}
list.files(out_dir)
# ---- Example 4: QC artefact via data_file alongside the render ----
#
# `emit(data_file = ...)` writes the resolved post-engine wide
# data frame alongside the rendered table. The sponsor's QC
# programmer picks up the side-car .csv (or .rds) and validates
# cell values without parsing the rendered RTF.
rtf_out <- tempfile(fileext = ".rtf")
data_out <- tempfile(fileext = ".csv")
emit(eff_spec, rtf_out, data_file = data_out)
file.exists(rtf_out)
file.exists(data_out)
# ---- Example 5: Render into a not-yet-existing output folder ----
#
# `create_dir = TRUE` builds the destination directory tree on the
# fly, so a submission-folder layout can be written in one pass
# without a separate `dir.create()` step.
nested <- file.path(tempfile(), "tables", "safety", "eff.md")
emit(eff_spec, nested, create_dir = TRUE)
file.exists(nested)
Attach an auto-numbered footnote to a table location
Description
Anchor a footnote to a cell, column header, title line, or any other
cells_*() location. The engine assigns the marker, places a
superscript at every matching anchor, and emits the marked-footnote
line at the foot of the table. Markers are assigned once, in
reading order, deduped by id, and are byte-identical across every
backend (RTF / LaTeX / PDF / HTML / DOCX) and every page, so the
marker at the anchor can never desynchronise from its note.
Usage
footnote(.spec, text, .at = cells_body(), id = NULL, symbol = NULL)
Arguments
.spec |
The |
text |
The footnote text. |
.at |
Where the marker is placed. # data-driven body anchor: mark every high-frequency preferred term
footnote(spec, "Includes events of any severity.",
.at = cells_body(where = n_total >= 50, j = "label"))
# column-header anchor: mark the analysis-population denominator
footnote(spec, "Safety population.",
.at = cells_headers(j = "Total"))
Note: the styling argument is |
id |
Stable identifier for sharing one marker across anchors.
|
symbol |
Pin an explicit marker glyph. |
Details
Engine-assigned, never hand-typed. Unlike a literal ^a^ typed
into both a cell and the footnotes argument, a footnote() marker
is allocated by the resolve engine after decimal alignment, so it
never disturbs column alignment and never drifts out of sync. The
scheme (letters / numbers / symbols) and the block-line format
come from the active preset (footnote_markers, footnote_label).
Dedup by id. Give two anchors the same id to share one marker
and one note line. Without an id, each footnote() call is its own
note.
Coexists with footnotes. Manual footnotes lines render first;
the auto-numbered block follows. The two systems do not cross-dedup,
so do not mix a hand-typed marker with an engine one for the same note.
Value
A tabular_spec. Pipe it onward to more verbs or to
emit().
See Also
Manual footnote lines: the footnotes argument to tabular().
Location helpers: cells_body(), cells_headers(),
cells_title().
Examples
# ---- Example 1: a denominator note on a column header ----
#
# AE-by-SOC/PT table whose Total column header carries the analysis-
# population note. The engine drops a superscript "a" on the header
# and prints "a <text>" beneath the table.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(cdisc_saf_aesocpt) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
footnote(
"Safety population: all randomised subjects who took study drug.",
.at = cells_headers(j = "Total")
)
# ---- Example 2: a data-driven note shared across cells ----
#
# A single note marks every high-frequency preferred term (n >= 50 in
# the Total column) in the SOC/PT stub. Sharing one `id` keeps it to
# one marker and one line; the marker lands on each matching cell.
tabular(cdisc_saf_aesocpt) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo"),
drug_100 = col_spec(label = "Drug 100"),
Total = col_spec(label = "Total")
) |>
footnote(
md("Includes events of *any* severity."),
.at = cells_body(where = n_total >= 50, j = "label"),
id = "anysev"
)
Get the active session-default preset
Description
Return the preset_spec last attached via set_preset(), or
NULL when no session default has been set. The cascade resolver
calls this internally; users call it for diagnostics ("what is my
session inheriting?") or to copy the active default into a
per-spec override via preset().
Usage
get_preset()
Value
A preset_spec, or NULL when no session default is
active.
See Also
Session-scope setter: set_preset().
Per-spec partner: preset().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: Inspect after setting a session default ----
#
# `get_preset()` returns NULL before any session default has been
# attached, then returns the `preset_spec` after `set_preset()`.
get_preset() # NULL
set_preset(font_size = 8, orientation = "landscape")
active <- get_preset()
is_preset_spec(active) # TRUE
active@font_size # 8
active@orientation # "landscape"
# ---- Example 2: Copy the session default into a per-spec override ----
#
# Read the session preset, tweak one knob for a single table, and
# attach as a per-spec override without disturbing the session.
set_preset(font_size = 9, paper_size = "letter")
# Read-tweak-attach without mutating the session default.
base_knobs <- get_preset()
tabular(cdisc_saf_n) |>
preset(
font_size = base_knobs@font_size,
paper_size = base_knobs@paper_size,
orientation = "landscape"
)
# Reset the session default so subsequent examples / R sessions
# are not affected.
set_preset(.reset = TRUE)
Attach multi-level column headers
Description
Build the column-header band(s) above the rendered table. Each named argument is one band; the value is either a character vector of column names (leaf band) or a named list of further bands (inner band). Nesting depth is arbitrary — the engine renders one band row per depth level, with each cell spanning the columns of its leaves.
Usage
headers(.spec, ...)
Arguments
.spec |
The |
... |
Named header bands. Each name is the band label (must be non-blank); each value is either:
Inside a nested-list value, an unnamed character-vector entry declares a passthrough leaf (see the Passthrough section above). Restriction: Every column referenced must exist in
|
Details
Replace, not stack. A second headers() call REPLACES the
prior tree — header structure is a single spec, not a stackable
list. Call with no arguments to clear the tree.
Strict label rule. Every declared band label must carry
visible text — empty strings, NA, and whitespace-only labels are
rejected at every nesting level. This is stricter than
col_spec(), which DOES accept empty labels (a row-label
column with no header text is a legitimate clinical case). A
silently-blank band would be a layout artefact.
Uncovered columns render naked. Columns not referenced under
any band render with their col_spec.label only — no extra band
row above them. This is the canonical pattern for row-label
columns (variable, soc, stat_label).
Multi-line band labels. Embed \n in a band label for a
two-line band cell (arm name on row 1, BigN on row 2).
Spanner underline trim (backend limitation). Each spanner's
underline is trimmed at both ends, booktabs \cmidrule(lr) style,
so adjacent spanners are separated by a visible gap rather than
merging into one continuous line. PDF / LaTeX (tabularray
leftpos/rightpos) and HTML (an inset rule) render the trim
natively. RTF and DOCX cannot inset a cell border horizontally, so
there the spanner underline spans the full band width (adjacent
spanner rules abut). This is a known, documented limitation of the
OOXML / RTF cell-border model, not a bug.
Value
The updated tabular_spec. Continue chaining with
sort_rows(), style().
Passthrough leaves inside a nested band
Inside a nested-list value, a child entry may be unnamed — the entry is then a character vector of column names that sit directly under the parent with no intermediate band at this depth. Use this when one column under a band has no sub-grouping while its siblings do. The strict-label rule still applies to every declared band; an unnamed passthrough is NOT a band with a missing label — it is "no band declared at this depth for this column."
See Also
Companion verb: cols() / col_spec() sets per-column
labels — the leaf-row header text that sits below the band rows
this verb builds.
Sibling build verbs: sort_rows(),
style(), paginate(), preset().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Inline label formatting: md(), html().
Examples
# ---- Example 1: Single "Treatment Group" band over four arms ----
#
# AE-by-SOC/PT table with one flat band labelled "Treatment Group"
# spanning the four arm columns and the Total column. The
# row-label column (`soc`) sits to the left of the band with no
# header covering it — the canonical clinical layout.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = "Subjects are counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
headers(
"Treatment Group" = c("placebo", "drug_50", "drug_100", "Total")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE))
# ---- Example 2: Two-level nested band — Control vs Active arms ----
#
# Efficacy BOR table where the active arms are grouped under an
# "Active" sub-band and the placebo arm under a "Control"
# sub-band, both under a single "Treatment Group" parent.
# Demonstrates the named-list value form for arbitrary-depth
# nesting.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}")
) |>
headers(
"Treatment Group" = list(
"Control" = "placebo",
"Active" = c("drug_50", "drug_100")
)
) |>
sort_rows(by = c("groupid", "stat_label"))
# ---- Example 3: Multiple peer bands side by side ----
#
# Vital-signs summary where the parameter columns (param,
# paramcd, visit, stat_label) sit on the left under a "Variable"
# band, and the arm columns sit on the right under "Treatment
# Group". Demonstrates multiple top-level bands in one call --
# bands render side by side in the order declared.
vit <- cdisc_saf_vital
tabular(vit, titles = c("Table 14.4.1", "Vital Signs Summary")) |>
cols(
param = col_spec(usage = "group", label = "Parameter"),
paramcd = col_spec(visible = FALSE),
visit = col_spec(usage = "group", label = "Visit"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal")
) |>
headers(
"Variable" = c("param", "paramcd", "visit", "stat_label"),
"Treatment Group" = c("placebo", "drug_50", "drug_100")
)
# ---- Example 4: Three-tier band over efficacy arms + Total ----
#
# Demographics-style three-tier nesting: top band labels the
# whole arm strip, middle band splits Active vs Placebo, leaf
# bands carry the per-arm column labels. Each child within a
# `list(...)` may itself be a `list(...)` — bands nest to
# arbitrary depth using nested list literals.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(cdisc_saf_demo, titles = "Demographics, hierarchical headers") |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "N={n['placebo']}"),
drug_50 = col_spec(label = "N={n['drug_50']}"),
drug_100 = col_spec(label = "N={n['drug_100']}"),
Total = col_spec(label = "N={n['Total']}")
) |>
headers(
"Treatment Group" = list(
"Control" = "placebo",
"Active" = list(
"Drug 50" = "drug_50",
"Drug 100" = "drug_100"
),
"Pooled" = "Total"
)
)
Mark a string as HTML for inline formatting
Description
Wrap a length-1 character vector so tabular(), col_spec(),
and similar string slots interpret it as a constrained HTML
subset at render time. Use when CommonMark cannot express the
formatting (custom CSS via <span style="...">, raw destination
codes via <span data-rtf="...">).
Usage
html(text)
Arguments
text |
The HTML fragment. |
Details
Recognised tag whitelist. <p>, <br> / <br/>,
<strong>, <b>, <em>, <i>, <sup>, <sub>, <code>,
<a href>, <span style>. Tags outside this set drop their
wrapper and keep their text content (no arbitrary HTML attack
surface).
Span styles. <span style="color: red; font-weight: bold">x</span>
parses the style attribute into a named character vector
(c(color = "red", "font-weight" = "bold")). Backends translate
CSS keys to destination-specific markup (RTF \cf, LaTeX
\textcolor, DOCX <w:color>, HTML inline style).
Backend-specific raw codes. A span with data-rtf,
data-latex, data-html, or data-docx attributes carries
per-backend raw markup. The matching backend emits its data
value verbatim and ignores the others; non-matching backends
render the span's text content as plain. Use for cases the AST
cannot express portably.
Value
A length-1 character vector classed
c("from_html", "character"). Pass it directly into any
string-bearing slot (tabular() titles / footnotes,
col_spec() label, style() pretext / posttext); the
resolve engine calls parse_inline() internally and backends
walk the resulting inline_ast.
See Also
Sibling helper: md() — Markdown wrapper for the common
case.
String slots that consume the wrapper: tabular()
(titles, footnotes), col_spec() (label), style()
(pretext, posttext).
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: Colour-styled span in a title ----
#
# Demographics table title with the population subset shaded
# red. The HTML wrapper carries an inline CSS style; backends
# translate (RTF: \cf, LaTeX: \textcolor, HTML: inline style).
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_demo,
titles = c(
"Table 14.1.1",
"Demographics",
html(sprintf("Safety Pop <span style='color:red'>(N=%d)</span>", n["Total"]))
)
)
# ---- Example 2: HTML link plus superscript footnote marker ----
#
# AE table footnote with an HTML link and a superscript marker.
# `html()` lets the user write tags directly when CommonMark
# would be awkward (e.g. attributes that Markdown does not
# surface).
tabular(
cdisc_saf_ae,
titles = c("Table 14.3.0", "Overall Adverse Event Summary"),
footnotes = c(
html('See <a href="https://www.meddra.org/">MedDRA</a> coding<sup>1</sup>.')
)
) |>
cols(stat_label = col_spec(usage = "group", label = "Category"))
Mark a string as Markdown for inline formatting
Description
Wrap a length-1 character vector so tabular(), col_spec(),
style() pretext / posttext, and similar string slots interpret
it as CommonMark Markdown at render time. Supports the
GitHub-flavoured plus Pandoc-style superscript (^sup^) and
subscript (~sub~) extensions; raw HTML inside Markdown passes
through to the constrained tag set documented under html().
Usage
md(text)
Arguments
text |
The Markdown string. |
Details
Convention adopted from gt. Marking strings with md() and
html() mirrors the well-tested gt convention. Plain
(unwrapped) strings render as plain text — a stray ** will
NOT silently bold the surrounding span. Wrap explicitly to opt
in.
Recognised Markdown. **bold**, *italic*, `code`,
[link text](url), hard line break (two trailing spaces + \n
or \\ + \n), Pandoc ^sup^ and ~sub~. Single embedded
\n (a "soft break" in CommonMark) renders as a space in HTML;
tabular preserves it as a line break for clinical-table use
where multi-line cells / titles are routine.
HTML pass-through. Raw HTML in Markdown (e.g.
md("Drug A <span style='color:red'>warning</span>")) is parsed
as HTML using the same tag whitelist as html(). Tags outside
the whitelist drop their wrapper and keep their text content.
Composition with plain strings. md() and html() wrap the
input with an internal control-character prefix that survives
c() concatenation, so you can freely mix plain and marked
strings in a single character vector:
c("Table 14.3.1", md("**Drug A**"), "third"). Backends strip
the marker before rendering; users never see it.
Value
A length-1 character vector classed
c("from_markdown", "character"). Pass it directly into any
string-bearing slot (tabular() titles / footnotes,
col_spec() label, style() pretext / posttext); the
resolve engine calls parse_inline() internally and backends
walk the resulting inline_ast.
See Also
Sibling helper: html() — same wrapper pattern for raw
HTML when Markdown cannot express the formatting.
String slots that consume the wrapper: tabular()
(titles, footnotes), col_spec() (label), style()
(pretext, posttext).
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: Italic title qualifier with Pandoc footnote marker ----
#
# AE-by-SOC/PT table. Title lines are bold by default, so the third
# line italicises "Safety Population" via `md("*...*")` for a visible
# contrast; the first footnote carries a Pandoc-style superscript
# marker `^a^` that the backends render as a true superscript.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_aesocpt,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
md("*Safety Population*")
),
footnotes = c(
md("^a^ Subjects counted once per SOC and once per PT.")
)
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
)
# ---- Example 2: Markdown link in a footnote ----
#
# Efficacy BOR table that footnotes the response criteria with
# a Markdown link. HTML / PDF / DOCX render as clickable; RTF /
# LaTeX render the link text with the URL inline (backend
# decides).
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
cdisc_eff_resp,
titles = c(
"Table 14.2.1",
"Best Overall Response",
"Efficacy Evaluable Population"
),
footnotes = c(
md("Response per [RECIST 1.1](https://recist.eortc.org/), investigator assessment.")
)
) |>
cols(
stat_label = col_spec(usage = "group", label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}", align = "decimal")
)
Configure pagination
Description
Attach a pagination_spec to a tabular_spec. The engine uses the
spec at render time to decide where page breaks fall, how wide
tables split into horizontal panels, and what continuation marker
(if any) prints on continued pages. The row budget per page is
computed by the engine from the active preset (paper, orientation,
margins, font size) and the chrome rows consumed by titles, column
headers, and footnotes — you do not set rows-per-page directly.
Usage
paginate(
.spec,
keep_together = character(),
panels = 1,
orphan_floor = 3,
widow_floor = 2,
repeat_content = c("titles", "headers", "footnotes"),
continuation = NULL
)
Arguments
.spec |
The |
keep_together |
Group columns whose runs of identical values
must not be split across a page break.
Interaction: A run too tall to fit in the computed row
budget less # Protect the SOC-level grouping in an AE-by-SOC/PT table. paginate(keep_together = "soc") |
panels |
Number of horizontal panels for wide tables.
Note: |
orphan_floor |
Minimum rows on a continued-from page.
|
widow_floor |
Minimum rows on the final page.
|
repeat_content |
Which page chrome repeats on every page.
The default repeats all three so each page is self-contained per
the submission layout contract. Pass a subset to drop one (e.g.
Note: Footnotes are always anchored to the page foot when present; membership only chooses every-page vs last-page-only, never table-body placement. HTML / MD: ignored. HTML renders one continuous |
continuation |
Marker text appended after a continuing
table's title block. Backend support is uneven — verify against your render target:
|
Details
Replace, not stack. A second paginate() call REPLACES the
prior spec — pagination is a single configuration block, not a
stackable list. Call with all defaults to clear back to the
engine's auto behaviour.
Rows per page are computed, not configured. The engine takes
the paper height for the active orientation (letter, a4) and
subtracts the top + bottom margins, the title block height
(number of title lines + a blank separator), the column-header
band height (max embedded \n line count across visible column
labels, plus any spanning header levels), and the footnote block
height (number of footnote lines + a blank separator). The
remainder, divided by the row height for the active font size,
gives the body-row budget per page. Landscape pages naturally
carry fewer rows than portrait at the same paper size; smaller
fonts carry more.
keep_together protects group runs. When a page break would
fall in the middle of a contiguous run of identical values in a
usage = "group" column listed in keep_together, the engine
moves the break BACK to the start of the run so the whole run
rides on the next page. Single rule of escape: if moving the
break back would leave fewer than orphan_floor rows on the
current page, the engine splits the run anyway (a single group
too tall to fit on one page cannot be kept together).
panels and group stickiness. With panels > 1, the engine
splits the NON-group columns into approximately equal slices and
repeats every usage = "group" column on every panel for row
context. panels = "auto" defers the decision to preset-aware
column-width metrics; until those metrics land in a future
release the engine treats "auto" as 1.
Value
The updated tabular_spec. Continue chaining with
style(), preset(), then render via emit() (or
resolve without I/O via as_grid()).
See Also
Render-geometry partner: preset() / set_preset()
— the preset's paper, orientation, margins, and font size feed
the per-page row budget this verb depends on.
Sibling build verbs: cols() / col_spec(),
headers(), sort_rows(), style().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: AE table paginated by SOC ----
#
# AE-by-SOC/PT table that may run several pages. The SOC column is
# protected by `keep_together` so a page break never lands in the
# middle of one SOC's PT rows. The engine derives the row budget
# from the preset's orientation + font_size + paper size and from
# the title / footnote / header line counts on the spec — no
# manual rows-per-page knob to keep in sync.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = "Subjects are counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(usage = "group", visible = FALSE,
group_display = "column_repeat"),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
headers("Treatment Group" = c("placebo", "drug_50", "drug_100", "Total")) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE)) |>
paginate(
keep_together = "soc",
repeat_content = c("titles", "headers", "footnotes"),
continuation = "(continued)"
)
# ---- Example 2: Wide ACROSS-style efficacy table split across 2 panels ----
#
# BOR table where the four-arm column block is too wide for portrait
# paper. Split into 2 horizontal panels; the group column
# (`stat_label`) repeats on every panel for row context. Vertical
# pagination still applies, so on a tall table you would see panel A
# pages 1-2, then panel B pages 1-2.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(usage = "id", label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}")
) |>
sort_rows(by = c("groupid", "stat_label")) |>
paginate(panels = 2, repeat_content = c("titles", "headers", "footnotes"))
# ---- Example 3: Orphan / widow floors + continuation marker ----
#
# Long vital-signs table with two safeguards: orphan_floor = 4
# prevents fewer than 4 rows of a group landing alone at the
# bottom of a page; widow_floor = 2 prevents fewer than 2 rows of
# a group landing alone at the top of the next page; the
# continuation marker prints on every page after the first.
tabular(
cdisc_saf_vital,
titles = c("Table 14.4.1", "Vital Signs Summary at Each Visit")
) |>
cols(
param = col_spec(usage = "group", label = "Parameter"),
paramcd = col_spec(visible = FALSE),
visit = col_spec(usage = "group", label = "Visit"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal")
) |>
paginate(
keep_together = "param",
orphan_floor = 4L,
widow_floor = 2L,
continuation = "(continued)"
)
# ---- Example 4: Many-arm horizontal pagination via column-fit ----
#
# Wide AE-by-SOC/PT table where the column strip itself does not
# fit on a single page. The engine slices columns into groups
# (each group keeping the `usage = "group"` columns repeated on
# every horizontal page) so the SOC / PT label band re-appears
# alongside whichever arm columns land on each panel.
tabular(
cdisc_saf_aesocpt,
titles = c("Table 14.3.1", "AEs by SOC and PT (wide-page split)")
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level",
width = "2.5in"),
soc = col_spec(usage = "group", visible = FALSE,
group_display = "column_repeat"),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal",
width = "2.0in"),
drug_50 = col_spec(label = "Drug 50", align = "decimal",
width = "2.0in"),
drug_100 = col_spec(label = "Drug 100", align = "decimal",
width = "2.0in"),
Total = col_spec(label = "Total", align = "decimal",
width = "2.0in")
) |>
paginate(keep_together = "soc")
Convert a cards ARD to a wide display data.frame
Description
pivot_across() is tabular's input-side helper: it consumes a
long Analysis Results Data (ARD) data frame (typically produced by
cards::ard_stack() or cards::ard_stack_hierarchical()) and
returns a wide display data.frame ready to pass to tabular().
Usage
pivot_across(
data,
statistic = list(continuous = "{mean} ({sd})", categorical = "{n} ({p}%)"),
column = NULL,
row_group = NULL,
label = NULL,
overall = "Total",
decimals = NULL,
fmt = NULL
)
Arguments
data |
Long ARD input data.
|
statistic |
Format spec for cell composition.
Form 1: single stringOne format string applied to every variable regardless of context. Use when your ARD is homogeneous (e.g. all categorical). # Every variable rendered as "n (p%)" — categorical-only slice.
cat_only <- cdisc_saf_demo_ard[cdisc_saf_demo_ard$context == "categorical", ]
pivot_across(
cat_only,
statistic = "{n} ({p}%)"
)
Form 2: named list by contextDifferent formats per context. This is the typical clinical-table form because demographics mix continuous and categorical variables. The list names must match the values in the ARD's
So an ARD assembled with # AGE (continuous) -> "75.2 (8.59)"; SEX (categorical) -> "53 (62%)"
pivot_across(
cdisc_saf_demo_ard,
statistic = list(
continuous = "{mean} ({sd})",
categorical = "{n} ({p}%)"
)
)
Form 3: named list by variableOverride on a per-variable basis; fall back to # AGE shows just the mean; SEX / RACE keep the categorical default.
pivot_across(
cdisc_saf_demo_ard,
statistic = list(
AGE = "{mean}",
categorical = "{n} ({p}%)",
default = "{mean} ({sd})"
)
)
Multi-row continuous specAny single entry can itself be a named character vector —
each element becomes one display row, with the name as the row
label. Use for pivot_across(
cdisc_saf_demo_ard,
statistic = list(
continuous = c(
N = "{N}",
"Mean (SD)" = "{mean} ({sd})",
Median = "{median}",
"Min, Max" = "{min}, {max}"
),
categorical = "{n} ({p}%)"
)
)
|
column |
Grouping column whose unique values become arms.
|
row_group |
Second, non-column grouping dimension.
Why it is required. cards encodes a crossing factor and a
SOC/PT hierarchy identically (the second group variable appears
in Restriction: Must name a second grouping variable present in
the ARD and must differ from |
label |
Variable-name to display-label map.
|
overall |
Column name for |
decimals |
Per-stat decimal precision.
Built-in defaults apply when neither sets a stat. |
fmt |
Per-stat custom formatter functions.
# p-value formatter: render below-threshold values as "<0.001".
fmt = list(
p.value = function(x) {
ifelse(x < 0.001, "<0.001", sprintf("%.3f", x))
}
)
|
Details
tabular's package boundary is display-only: pre-summarised
data in, rendered file out. pivot_across() is the canonical
bridge between the cards aggregation backend and that boundary.
It does not aggregate — it pivots arms to columns, interpolates
per-cell display strings from the stat values, and applies
decimal precision. Filtering, weighting, and aggregation happen
upstream in cards or your own data-prep step.
Key statistic by the ARD context
statistic (and fmt) are matched against the ARD's context
column verbatim, and that value differs per generating function.
Keying by the wrong name silently drops the format. Inspect
unique(ard$context) first and key to match (or pass a single
format string / default = to cover everything). When an
explicitly-supplied statistic matches no context at all,
pivot_across() warns rather than silently emitting {n}.
| Generating function | context to key on |
cards::ard_summary() | summary |
cards::ard_tabulate() | tabulate |
cards::ard_continuous() | continuous |
cards::ard_categorical() | categorical |
cards::ard_stack_hierarchical() | tabulate + hierarchical |
cardx::ard_categorical_ci() | proportion_ci |
cardx::ard_continuous_ci() | continuous_ci
|
Indentation of stat_label
Categorical levels and the multi-row continuous stat labels come
back already indented with two leading spaces, ready to render as a
plain display column. Do not also set col_spec(usage = "indent") on stat_label — that stacks the engine indent on top of
the string indent (a double indent). Use one or the other.
Zero-suppression (always-on default)
A row whose n value equals zero renders the whole cell as the
bare n value instead of fully interpolating the format string.
For a categorical level with n = 0, the cell shows "0", not
"0 (0.0%)". This is clinical convention — empty cells should
read as a single zero, not advertise a meaningless rate.
How the default fires (chain of events). During cell assembly,
before format-string interpolation, the engine checks the row's
n stat. If it is zero, the engine short-circuits and returns the
formatted n value ("0") as the entire cell — {p} is never
substituted, so the (0.0%) half of the format string is dropped.
How to opt out: supply a custom fmt$n. Setting any function
under fmt$n is the engine's signal that the user owns the n
rendering. The short-circuit is disabled for the whole table; for
every row the full format string interpolates, so {n} becomes
your formatter's output and {p} becomes the standard percentage.
For n = 0, that's "0 (0.0%)".
# Force "0 (0.0%)" for n = 0 rows by attaching a custom n formatter.
# The body of fmt$n can be the default integer rendering — its
# presence alone is what disables the zero-suppression branch.
pivot_across(
cdisc_saf_demo_ard,
statistic = list(
continuous = "{mean} ({sd})",
categorical = "{n} ({p}%)"
),
fmt = list(n = function(x) sprintf("%d", as.integer(x)))
)
Pharma rounding (always-on default)
A percentage that would otherwise round to 0 (when the value
is positive but smaller than the chosen precision) renders as
<0.1; one that would round to 100 (positive but smaller than
100) renders as >99.9. The threshold is precision-aware:
decimals = c(p = 2) produces <0.01 / >99.99. This matches
the pharma convention of never claiming exactly 0% or 100%
when at least one subject contributed.
Override per-stat via fmt:
# Show exact rounded percentages even at the extremes
pivot_across(
data,
statistic = "{n} ({p}%)",
decimals = c(p = 1),
fmt = list(p = function(x) sprintf("%.1f", x * 100))
)
Your fmt$p receives the raw stat value (a proportion between
0 and 1) and returns the displayed string. The pharma-threshold
branch only fires inside the built-in p formatter and the
decimals-driven path, so any custom fmt$p bypasses it.
Value
A wide data.frame ready for tabular(). Schema:
-
variable— variable name (or label afterlabel = ...). -
stat_label— display-row label. One column per arm level (named after the
group1_levelvalues or the renamed arm column).-
Total(or whateveroverallis set to) when applicable. A leading column named after
row_groupwhen set (the second grouping dimension).Hierarchical ARD adds
soc,label,row_typeinstead ofvariable.
Pass the result straight into tabular() to start the
render pipeline.
See Also
Pipeline entry consumer: tabular() — wraps the wide data
frame this helper returns.
Downstream spec-build verbs: cols() / col_spec(),
headers(), sort_rows(), style(),
paginate(), preset().
Terminal verbs: emit(), as_grid().
Examples
# ---- Example 1: Demographics — long ARD to rendered spec ----
#
# Full pipeline from a `cards::ard_stack()`-style long ARD to a
# sorted `tabular_spec`. The multi-row continuous block (N /
# Mean (SD) / Median / Min, Max) sits above each categorical
# block; decimals are set per-stat (mean 1, sd 2, p 1) to match
# the CDISC convention.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
cdisc_saf_demo_ard |>
pivot_across(
statistic = list(
continuous = c(
N = "{N}",
"Mean (SD)" = "{mean} ({sd})",
Median = "{median}",
"Min, Max" = "{min}, {max}"
),
categorical = "{n} ({p}%)"
),
decimals = c(mean = 1, sd = 2, p = 1, median = 1, min = 0, max = 0),
label = c(AGE = "Age (years)", SEX = "Sex", RACE = "Race")
) |>
tabular(
titles = c(
"Table 14.1.1",
"Demographics and Baseline Characteristics",
"Safety Population"
),
footnotes = "Percentages based on N per treatment group."
) |>
cols(
variable = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
Placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
`Xanomeline Low Dose` = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
`Xanomeline High Dose` = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
),
Total = col_spec(
label = "Total\nN={n['Total']}",
align = "decimal"
)
)
# ---- Example 2: Hierarchical SOC/PT AE table ----
#
# Hierarchical `cards::ard_stack_hierarchical()` output threaded
# through `pivot_across()`. The hierarchical ARD emits a
# (soc, label, row_type) triple plus one stat row per (arm, SOC, PT);
# `pivot_across()` folds the arm dimension to columns and preserves
# the hierarchy markers. Derive `indent_level` from `row_type` so
# `col_spec(indent_by = "indent_level")` drives the SOC -> PT
# indent on the `label` column.
wide <- cdisc_saf_aesocpt_ard |>
pivot_across(statistic = "{n} ({p}%)")
wide$indent_level <- as.integer(wide$row_type == "pt")
tabular(
wide,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = c(
"Subjects are counted once per SOC and once per PT.",
"Percentages based on N per treatment group."
)
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
Placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
`Xanomeline Low Dose` = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
`Xanomeline High Dose` = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
)
)
# ---- Example 3: Hierarchical ARD (SOC / PT) ----
#
# `cdisc_saf_aesocpt_ard` carries an `ard_stack_hierarchical` shape with
# two grouping variables (AEBODSYS / AEDECOD). `pivot_across()`
# recognises the hierarchical structure and emits dedicated `soc`,
# `label`, and `row_type` columns so the SOC -> PT nesting survives
# the pivot. The result is ready for `tabular()` plus `sort_rows()`.
head(cdisc_saf_aesocpt_ard, 3)
wide <- cdisc_saf_aesocpt_ard |>
pivot_across(statistic = "{n} ({p}%)")
head(wide, 3)
# ---- Example 4: Multi-row continuous spec + label re-labelling ----
#
# `statistic = c(<label> = <template>, ...)` produces one display
# row per named entry — the canonical "N / Mean (SD) / Median /
# Min, Max" block for continuous variables. `label = c(...)`
# renames the variable headings emitted into the wide output.
cdisc_saf_demo_ard |>
pivot_across(
statistic = list(
continuous = c(
N = "{N}",
"Mean (SD)" = "{mean} ({sd})",
Median = "{median}",
"Q1, Q3" = "{p25}, {p75}",
"Min, Max" = "{min}, {max}"
),
categorical = "{n} ({p}%)"
),
label = c(
AGE = "Age (years)",
WEIGHT = "Weight (kg)",
HEIGHT = "Height (cm)",
BMI = "BMI (kg/m^2)"
)
)
# ---- Example 5: ARD keyed by summary / tabulate contexts ----
#
# The `statistic` list names must match the ARD's `context` column
# verbatim. `cards::ard_summary()` / `ard_tabulate()` emit `"summary"` /
# `"tabulate"` (not the `"continuous"` / `"categorical"` of
# `ard_continuous()` / `ard_categorical()`), so a list keyed
# `continuous`/`categorical` would silently match nothing. Always check
# `unique(ard$context)` first. Here the bundled `cdisc_saf_demo_ard` is
# relabelled to mimic `ard_summary()` + `ard_tabulate()` output; the
# by-variable's own row drops automatically and both the summary and
# the tabulate variables survive.
card_st <- cdisc_saf_demo_ard
card_st$context[card_st$context == "continuous"] <- "summary"
card_st$context[card_st$context == "categorical"] <- "tabulate"
pivot_across(
card_st,
statistic = list(
summary = "{mean} ({sd})",
tabulate = "{n} ({p}%)"
)
)
Override the render preset on a spec
Description
Attach a preset_spec to a tabular_spec, carrying page-geometry
knobs (paper, orientation, margins, body font_size + family, h-rule
policy, decimal metric, typography defaults). The engine consults
the per-spec preset first when computing the per-page row budget,
decimal-aligned column widths, and the chrome that the backend
renders around the body grid.
Usage
preset(.spec, ..., .template = NULL, .style = NULL, .reset = FALSE)
Arguments
.spec |
The | ||||||||||||||||||
... |
Named preset knobs. Any subset of the preset knobs the
Recognised knobs:
# Landscape A4, 8pt body, slim margins for one wide table. preset( orientation = "landscape", paper_size = "a4", font_size = 8, margins = c(0.75, 0.5, 0.75, 0.5) ) | ||||||||||||||||||
.template |
A | ||||||||||||||||||
.style |
A | ||||||||||||||||||
.reset |
Discard the spec's existing preset before applying
|
Details
Per-spec, chained. preset() is the per-spec override — a
verb that returns a modified spec, composable on the pipe alongside
cols() / headers() / paginate(). Use it when a single
table needs a one-off geometry (e.g. landscape A4 for one wide
efficacy summary inside a portfolio of portrait letter tables).
Merge, not replace. A second preset() call merges its scalar
knobs onto the spec's existing preset; unspecified knobs keep
their prior value. The five named-list knobs (alignment /
rules / fonts / colors / padding) lower to style_layer
records on preset@style via .preset_args_to_layers()
(internal) and append in call order; layer order is precedence
within the engine cascade, so a later preset() call's lowered
attribute wins over an earlier one at the cell. Pass .reset = TRUE
to discard the existing knobs and start from preset_spec()
defaults. preset(.spec, .reset = TRUE) with no knobs clears the
per-spec override entirely (the spec then falls through to
set_preset() or preset_spec() defaults at render time).
Direct preset_spec() calls bypass lowering. The five
named-list knobs are no longer slots on the preset_spec S7
class — they exist only as preset() / set_preset() arguments
that lower into @style. preset_spec(rules = list(...))
(and analogous direct calls) raise "unused argument". Wrap such
calls in tabular(...) |> preset(...) so the lowering helper
fires and the layers land on @style.
Cascade with set_preset(). The engine resolves the active
preset in this order: (1) the spec's per-call preset (this verb),
(2) the session default attached via set_preset(),
(3) preset_spec() factory defaults. The first non-NULL layer
wins; layers are not field-merged across the cascade.
Value
The updated tabular_spec. Continue chaining with
paginate(), style(), then render via emit() (or
resolve without I/O via as_grid()).
See Also
Session-scope partners: set_preset(), get_preset().
Render-geometry consumer: paginate() derives the per-page
row budget from the active preset's paper, orientation, margins,
and font size.
Sibling build verbs: cols() / col_spec(),
headers(), sort_rows(), style().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: Landscape A4 for a wide efficacy table ----
#
# BOR table where the four-arm column block fits portrait letter
# with a smaller body font, but the sponsor wants A4 landscape at
# 8pt for visual breathing room. `preset()` attaches the geometry;
# `paginate()` reads it later to size the per-page row budget.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}")
) |>
sort_rows(by = c("groupid", "stat_label")) |>
preset(
orientation = "landscape",
paper_size = "a4",
font_size = 8
) |>
paginate()
# ---- Example 2: Per-spec override with per-page chrome ----
#
# The submission session sets a portrait letter 9pt default (typical
# safety-table geometry). One particular AE table needs landscape
# for a long PT label band; the per-spec `preset()` overrides only
# orientation. The same per-spec call wires the canonical
# per-page header band (protocol on the left, page X of Y on the
# right) and a footer band that auto-resolves the calling
# script's name and the current render timestamp via the
# `{program}` and `{datetime}` tokens.
set_preset(font_size = 9, paper_size = "letter")
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = "Subjects are counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(usage = "group", visible = FALSE,
group_display = "column_repeat"),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
headers("Treatment Group" = c("placebo", "drug_50", "drug_100", "Total")) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE)) |>
preset(
orientation = "landscape",
pagehead = list(
left = "Protocol: ABC-123",
right = "Page {page} of {npages}"
),
pagefoot = list(
left = "{program}",
right = "{datetime}"
)
) |>
paginate(keep_together = "soc")
# Reset the session default so subsequent examples / R sessions
# are not affected.
set_preset(.reset = TRUE)
Minimal theme: one header rule, normal weight throughout
Description
Apply the stripped-down table look in one verb. The column-label
divider (midrule) becomes the only rule drawn, and every
bold-by-default surface renders in normal weight: the title block,
the column-header band, the subgroup banner, and the section-header
rows synthesized for usage = "group" columns. The analogue of
ggplot2's theme_minimal(), composable on the pipe between the build
verbs and the terminal emit() / as_grid().
Usage
preset_minimal(.spec, ...)
Arguments
.spec |
The |
... |
Named preset knobs. Forwarded verbatim to Restriction: the |
Details
What it sets, both at theme (lowest) precedence so an explicit
later style() wins:
-
Rules. Drops the booktabs
topruleandbottomrule(the outer frame), keeping themidruleunder the column labels and the muted column-spannerspanrule. Equivalent topreset(rules = list(toprule = "none", bottomrule = "none")). -
Weight. Sets
bold = FALSEon the title, column-header, subgroup-label, and group-header surfaces. The HTML backend overrides itsfont-weight: 600class default with an inlinefont-weight: normal; the paginated backends (RTF / LaTeX / PDF / DOCX) suppress the surface's bold run.
Last verb wins. Because the weight layers ride the theme tier, a
later explicit style(bold = TRUE, .at = cells_title()) (or any
surface) re-bolds it. Treat preset_minimal() as the theme baseline
and override individual surfaces afterwards.
Markdown. GFM cannot represent colour / background / font on a
surface; rendering a styled surface to .md emits a one-time
tabular_warning_fidelity and degrades gracefully. Weight (bold) and
italic carry through.
Value
The updated tabular_spec. Continue chaining with
paginate() / style(), then render via emit() (or
resolve without I/O via as_grid()).
See Also
Underlying verbs: preset() (the rule presets "booktabs" /
"grid" / "frame" / "none" live there as rules string sugar),
style().
Target the surfaces it touches: cells_title(),
cells_headers(), cells_subgroup_labels(),
cells_group_headers().
Entry / terminal verbs: tabular(), emit(), as_grid().
Examples
# ---- Example 1: Minimal AE overall summary ----
#
# The overall adverse-event summary with a single rule under the
# column labels and no bold anywhere. `preset_minimal()` is the theme
# baseline; the page stays at the session default geometry.
demo_n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_ae,
titles = c(
"Table 14.3.1",
"Overall Summary of Adverse Events",
"Safety Population"
),
footnotes = "Subjects counted once per category."
) |>
cols(
stat_label = col_spec(usage = "group", label = "Category"),
placebo = col_spec(label = "Placebo\nN={demo_n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={demo_n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={demo_n['drug_100']}"),
Total = col_spec(label = "Total\nN={demo_n['Total']}")
) |>
preset_minimal()
# ---- Example 2: Section headers normal, then re-bold the title ----
#
# AE by SOC / PT with the SOC as a section-header row. Under
# `preset_minimal()` the SOC section labels render in normal weight
# (not the default bold); a trailing `style()` re-bolds only the
# title (last verb wins), and `font_size` forwards through `...`.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
tabular(
ae,
titles = c("Table 14.3.2", "Adverse Events by SOC and Preferred Term"),
footnotes = "Subjects counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(usage = "group", group_display = "header_row"),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={demo_n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={demo_n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={demo_n['drug_100']}"),
Total = col_spec(label = "Total\nN={demo_n['Total']}")
) |>
preset_minimal(font_size = 8) |>
style(bold = TRUE, .at = cells_title())
Print a tabular_spec
Description
Renders a tabular_spec interactively. The default behaviour
mirrors gt::gt(): convert the spec to an htmltools tag
list and let htmltools dispatch — RStudio + Positron viewer
panes, Quarto / Rmd notebook inline, Databricks displayHTML,
and plain-console cat() are all handled without any IDE-
specific branching.
Arguments
x |
The |
... |
Forwarded to |
view |
Open the viewer? |
output |
Force a specific preview format. |
Details
Dispatch. print() delegates to as.tags.tabular_spec()
which returns an htmltools::tagList. That tag list is handed
to htmltools's own print method with browse = view:
htmltools opens the IDE viewer when one is registered,
inlines under a Quarto / Rmd chunk when running inside one,
or cat()s the HTML when neither applies. No is_rstudio()
/ is_positron() / is_notebook() heuristics — htmltools
already knows.
view argument. Defaults to interactive(), the same
universal off-switch gt::gt() uses. Non-interactive
contexts (Rscript, R CMD check, CI, devtools::test)
bypass the viewer automatically. Pass view = FALSE
explicitly at an interactive prompt to suppress the viewer
for a single call.
output argument. Forces a specific preview format
instead of the default HTML-via-htmltools path. One of:
-
"html"— same as the default, but explicit. -
"md"/"markdown"—cat()the markdown source to the console (round-trips throughbackend_md). -
"latex"—cat()the markdown source as a temporary placeholder (real LaTeX preview lands withbackend_latex). -
"rtf"/"docx"/"pdf"— render an HTML preview and emit a cli note pointing atemit()for the real artefact. The viewer pane cannot render RTF / OOXML, and we deliberately do not compile through tinytex on every autoprint. -
"cli"— print the structural cli-tree summary (props, headers, sort, pagination, preset). Useful for debugging spec composition without paying the HTML render cost.
Robustness. The HTML render is wrapped in tryCatch; if
rendering fails for any reason the printer falls back to the
cli-tree summary and a cli::cli_warn() describing the
failure, so a broken spec never crashes the REPL.
Tempdir. Preview HTML files live under
getOption("tabular_preview_dir", default = tempdir()).
Override the option to keep them in a stable location (handy
on Linux distros where browsers don't have read access to
/tmp/).
Value
Invisibly returns x. Side effect: opens the
viewer, inlines under a chunk, or cat()s output.
See Also
Tag conversion: as.tags.tabular_spec() — the
htmltools tag list that print() delegates to. Call it
directly to embed the table in a custom htmltools::tagList
or Shiny UI.
Terminal verb: emit() writes the resolved artefact to
disk; print() is for in-session preview only.
Pipeline shape: as_grid() resolves the engine pipeline
to a tabular_grid without I/O.
Examples
# ---- Example 1: Build + autoprint (HTML preview) ----
#
# Build a spec and let autoprint render it. Inside RStudio /
# Positron the HTML lands in the viewer pane; inside a
# Quarto / Rmd chunk it inlines under the chunk; at a plain
# console the HTML source is `cat()`-ed.
tabular(
cdisc_saf_demo,
titles = c("Table 14.1.1", "Demographics"),
footnotes = "Safety Population."
)
# ---- Example 2: Force the cli-tree structural view ----
#
# The cli-tree summary shows props at a glance. Useful for
# debugging spec composition without paying the HTML render
# cost.
spec <- tabular(cdisc_saf_demo, titles = "Demographics") |>
cols(variable = col_spec(usage = "group", label = "Characteristic"))
print(spec, output = "cli")
Set or clear the session default preset
Description
Stash a preset_spec in the package-internal session environment.
Every subsequent tabular() chain that does not attach its own
preset() inherits these knobs at render time. Mirrors ggplot2's
ggplot2::theme_set(): one call up front, many tables downstream.
Usage
set_preset(new = NULL, ..., .template = NULL, .style = NULL, .reset = FALSE)
Arguments
new |
A Mutually exclusive with |
... |
Named preset knobs. Same shape as |
.template |
A |
.style |
A |
.reset |
Discard the existing session preset before applying
|
Details
Persistence. The session preset lives in a package-internal
environment populated when tabular is loaded and emptied when the
namespace unloads. There is no on-disk persistence; set the
default at the top of each analysis script (or in a project-level
.Rprofile) when a sticky house style is needed.
Merge, not replace. A second set_preset() call merges its
knobs onto the existing session preset; unspecified knobs keep
their prior value. Pass .reset = TRUE to discard the existing
session preset and start from preset_spec() defaults.
set_preset(.reset = TRUE) with no knobs clears the session
default back to NULL.
Save and restore. Every call returns the previous session
preset invisibly, the same primitive ggplot2's
ggplot2::theme_set() ships. Capture it once, render, and
restore by passing the saved value back as the positional new
argument:
old <- set_preset(font_size = 10, paper_size = "a4") # ... one renegade render at 10pt A4 ... set_preset(old) # restore
When the prior was NULL (no session preset ever attached), the
restore is set_preset(.reset = TRUE) instead — set_preset(NULL)
is the same shape as set_preset() and falls through to factory
defaults rather than clearing the session.
Cascade with preset(). A per-spec preset() always wins
over the session default. The session default fills in only when
the spec carries no preset of its own.
Value
The previous session preset_spec (invisible). Returns
NULL when no session preset was attached prior to the call.
Capture it to round-trip a temporary override:
old <- set_preset(...); set_preset(old). Mirrors
ggplot2::theme_set() and base::options() — the canonical
tidyverse save/restore primitive.
See Also
Per-spec partner: preset() — overrides the session
default on one chain.
Inspect: get_preset().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: Sticky session default for an analysis script ----
#
# The submission's safety tables all use portrait letter, 9pt
# Times New Roman with 1-inch margins. Set once at the top of the
# analysis script and every `tabular()` chain inherits it — no
# per-table `preset()` call needed unless one table deviates.
set_preset(
font_size = 9,
font_family = "Times New Roman",
orientation = "portrait",
paper_size = "letter",
margins = 1
)
# Subsequent tabular() chains pick up the session preset at render.
demo_n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
cdisc_saf_ae,
titles = c(
"Table 14.3.1",
"Overall Summary of Adverse Events",
"Safety Population"
),
footnotes = "Subjects counted once per category."
) |>
cols(
stat_label = col_spec(usage = "group", label = "Category"),
placebo = col_spec(label = "Placebo\nN={demo_n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={demo_n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={demo_n['drug_100']}"),
Total = col_spec(label = "Total\nN={demo_n['Total']}")
)
# ---- Example 2: Reset the session default mid-script ----
#
# The first half of the script produces safety tables at 9pt; the
# second half produces efficacy tables at 10pt on landscape A4. A
# single `set_preset(.reset = TRUE, ...)` resets the cascade before
# the second batch starts.
set_preset(font_size = 9, paper_size = "letter")
get_preset()@font_size # 9
set_preset(
.reset = TRUE,
font_size = 10,
orientation = "landscape",
paper_size = "a4"
)
get_preset()@orientation # "landscape"
# Reset the session default so subsequent examples / R sessions
# are not affected.
set_preset(.reset = TRUE)
# ---- Example 3: Save and restore around a renegade table ----
#
# Most of the submission renders portrait letter at 9pt. One
# renegade efficacy table needs landscape A4 at 10pt. Capture
# the prior session preset, render the renegade, then restore.
set_preset(font_size = 9, paper_size = "letter")
old <- set_preset(
font_size = 10,
paper_size = "a4",
orientation = "landscape"
)
# ... one renegade render ...
if (is.null(old)) {
set_preset(.reset = TRUE) # was no prior — clear
} else {
set_preset(old) # round-trip via the positional `new` arg
}
get_preset()@paper_size # "letter" — restored
# ---- Example 4: Snapshot current preset, mutate, restore ----
#
# Capture whatever the session preset is right now (may be NULL),
# let a downstream helper mutate it, then put it back when done.
set_preset(font_size = 9, paper_size = "letter")
snapshot <- get_preset()
# Simulate downstream code mutating session state.
set_preset(font_size = 11, orientation = "landscape")
# Restore. The wholesale-install path of `set_preset(new)`
# accepts any `preset_spec` returned by `get_preset()` /
# `set_preset()`.
if (is.null(snapshot)) {
set_preset(.reset = TRUE)
} else {
set_preset(snapshot)
}
get_preset()@font_size # 9 — restored
# Reset for subsequent examples / R sessions.
set_preset(.reset = TRUE)
Sort the display rows
Description
Attach a sort_spec to a tabular_spec. The engine applies the
sort before pagination, so by may reference any column in
spec@data whether or not the column is declared in cols().
Usage
sort_rows(.spec, by = character(), descending = FALSE)
Arguments
.spec |
The |
by |
Ordered column names to sort by, in precedence order.
Restriction: Every entry must be a column in # Two-key clinical sort: row_type ascending, n_total descending.
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE))
|
descending |
Per-key sort direction.
Restriction: No NAs. Length must be 1 or |
Details
Replace, not stack. A second sort_rows() call REPLACES the
prior sort — sort is a single spec, not a stackable list. Call
with no arguments to clear.
NA last, regardless of direction. NA values in a sort key are
placed at the end whether the key is ascending or descending
(matching order(..., na.last = TRUE)).
Factor levels drive the order. Factor columns sort by factor
levels, not by the character label. The CDISC BOR ordering
(CR < PR < SD < NON-CR/NON-PD < PD < NE < MISSING) survives a
tabular pipeline without an explicit mutate() — coerce
stat_label to a factor with the levels in clinical order
upstream, then sort_rows(by = "stat_label") does the rest.
Value
The updated tabular_spec. Continue chaining with
style(), paginate(), preset(), then render via
emit() (or resolve without I/O via as_grid()).
See Also
Sibling build verbs: cols() / col_spec(),
headers(), style(), paginate(), preset().
Entry / terminal verbs: tabular(), emit(),
as_grid().
Examples
# ---- Example 1: AE table sorted by SOC, then by descending subject count ----
#
# AE-by-SOC/PT table where the SOCs and PTs appear in descending
# order of subject count within the row-type hierarchy (overall
# first, then SOCs, then PTs). `cdisc_saf_aesocpt$Total` cells are
# formatted text ("171 (67.3)"), so a lexical sort on `Total`
# would be wrong ("14" < "171" < "29") — attach a numeric rank
# column upstream and sort on (row_type, n_total).
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = "Subjects are counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE))
# ---- Example 2: BOR table in CDISC factor order ----
#
# Efficacy BOR table that must appear in CDISC clinical order
# (CR < PR < SD < NON-CR/NON-PD < PD < NE < MISSING), then the
# derived ORR / CBR / DCR rate rows ordered by `groupid`,
# not alphabetical. `cdisc_eff_resp$stat_label` arrives as character, so
# coerce to a factor with the canonical levels upstream and
# `sort_rows()` uses those levels directly.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}")
) |>
sort_rows(by = c("groupid", "stat_label"))
# ---- Example 3: Mixed-direction multi-key sort with hidden helper ----
#
# Demographics-style table sorted by `variable` ascending and a
# hidden numeric key descending. The `descending` argument takes
# one value per `by` entry so each key can flip direction
# independently. The helper column rides in `spec@data` for the
# sort but never renders (visible = FALSE on its col_spec).
demo <- cdisc_saf_demo
demo$display_order <- match(demo$variable, unique(demo$variable))
tabular(demo, titles = "Demographics, ranked within section") |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
display_order = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
sort_rows(
by = c("display_order", "stat_label"),
descending = c(FALSE, TRUE)
)
# ---- Example 4: Hierarchical SOC -> PT sort with factor outer key ----
#
# A factor outer key locks the SOC display order to the canonical
# interleaved sequence (`overall` first, then `soc` blocks, then
# `pt` detail rows inside each SOC) regardless of input order. The
# numeric inner key sorts PTs within each SOC by descending total
# subject count.
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
tabular(ae, titles = "AE by SOC and PT, ranked within SOC") |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
sort_rows(
by = c("row_type", "soc", "n_total"),
descending = c(FALSE, FALSE, TRUE)
)
Attach a style layer to a tabular_spec or style_template
Description
One verb, one cascade. Each style() call appends a single
style_layer (location + style attributes) to the spec or
template. Layers accumulate in declaration order; the engine
merges them at render time so later layers win per attribute,
NA-valued fields leave the prior layer intact.
Usage
style(.spec, ..., .at = cells_body())
Arguments
.spec |
A |
... |
Named style attributes. At least one required. See the vocabulary list above for recognised names. |
.at |
Location object selecting which surface the layer
targets. |
Details
Locations. The at argument selects which surface the layer
targets. Every region of the rendered page has a cells_*()
constructor:
-
cells_body()— body cells (default) -
cells_headers()— column header band -
cells_group_headers()— synthetic group-header rows -
cells_title()— title block -
cells_subgroup_labels()— subgroup banner row -
cells_footnotes()— footnote block -
cells_pagehead()— page-header band -
cells_pagefoot()— page-footer band -
cells_table()— table-wide regions (outer borders, body-row separators)
Body filters live on cells_body(): i = 1:3 for integer-index
rows, j = "Total" for column-name targeting, where = <expr>
for a quosure-captured predicate evaluated against spec@data.
Attribute vocabulary. Each layer carries a style_node built
from .... Recognised attribute names:
Text —
bold,italic,underline,color,background,font_family,font_sizeAlignment —
halign("left" / "center" / "right"),valign("top" / "middle" / "bottom")Borders —
border(umbrella),border_top,border_bottom,border_left,border_right(each takes abrdr()value or the literal"none"); per-side scalarsborder_<side>_{style,width,color}for finer controlPadding —
padding(a scalar applies to all four sides; a named vectorc(top = , right = , bottom = , left = )sets each side); or the per-side scalarspadding_<side>directlySpacing —
blank_above,blank_below(integer blank lines above / below the block — forcells_title()/cells_footnotes()/cells_subgroup_labels())Inline —
pretext,posttext(literal text prepended / appended around the cell value)
Unknown attribute names emit a cli::cli_warn and drop from
the constructed node; the engine never sees a foreign property.
Value
The updated tabular_spec (or tabular_style_template,
when called against one).
See Also
Companion verbs: cols(), headers(), preset(),
set_preset().
Location constructors: cells_body(), cells_headers(),
cells_group_headers(), cells_title(),
cells_subgroup_labels(), cells_footnotes(),
cells_pagehead(), cells_pagefoot(), cells_table().
Style values: brdr(), style_template().
Examples
# ---- AE table by SOC and PT with per-row indent + styled hierarchy ----
# `cdisc_saf_aesocpt` ships with `indent_level` (0 on overall/SOC rows,
# 1 on PT rows); `col_spec(indent_by = "indent_level")` drives the
# PT indent on the `label` column.
tabular(cdisc_saf_aesocpt, titles = "Adverse Events by SOC / PT",
footnotes = "") |>
cols(
label = col_spec(label = "Category", align = "left",
indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
# SOC summary rows bolded (depth 0 — flush)
style(bold = TRUE,
.at = cells_body(where = row_type == "soc")) |>
# Overall row gets a light background
style(background = "#f0f0f0",
.at = cells_body(where = row_type == "overall"))
# ---- Chrome styling ----
# Each layer changes the surface VISIBLY from its default: a coloured
# rule under the header band, a dark-blue header text, a left-aligned
# title (default is centred), and a blank line above + below the title.
tabular(cdisc_saf_demo) |>
style(color = "#1a5276", .at = cells_headers()) |>
style(border_bottom = brdr("thick", "double", "#1a5276"),
.at = cells_headers()) |>
style(halign = "left", .at = cells_title()) |>
style(blank_above = 1, blank_below = 1,
.at = cells_title())
# ---- Table-wide borders ----
tabular(cdisc_saf_demo) |>
style(border = brdr("medium"),
.at = cells_table(side = "outer")) |>
style(border_top = brdr("hairline", "dotted"),
.at = cells_table(side = "rows"))
# ---- House style via style_template() ----
house <- style_template() |>
style(bold = TRUE, .at = cells_headers()) |>
style(border_top = brdr("thick"), .at = cells_headers()) |>
style(border_bottom = brdr("thick"), .at = cells_headers()) |>
style(border_bottom = brdr("medium"),
.at = cells_table(side = "outer_bottom"))
# Attach once via set_preset(); every tabular() chain then inherits it.
set_preset(.style = house, font_size = 9)
set_preset(.reset = TRUE) # restore the default for later examples
Reusable style template (for house-style presets)
Description
Build a reusable, composable style template by chaining
style() calls against a tabular_style_template. The
template carries an ordered list of style_layer records and
can be attached to preset() / set_preset() as a style =
argument — every downstream tabular() chain then inherits the
template's layers via the engine cascade.
Usage
style_template()
is_style_template(x)
Arguments
x |
Any R object. The predicate inspects the class via
|
Details
One verb, two surfaces. The same style(.spec_or_template, ..., .at = ...) call that attaches a layer to a per-table spec
also accumulates layers onto a template. Symmetric API — no need
to learn a second function for the multi-table use case.
Submission workflow. A submission typically renders 100–200
tables with one visual identity. Build the template once at the
top of the submission script, pass it to set_preset(style = template), and every subsequent tabular() produces output that
inherits the same column-header rules, group-header bolding,
title spacing, and outer-frame borders without a single per-table
style() call.
Cascade order. Engines apply layers low-to-high priority:
backend defaults → session preset's @style → spec preset's
@style → per-spec style() layers. Later layers override prior
ones per attribute; NA fields leave the prior layer's value in
place.
Value
A tabular_style_template — a small S3 list with a
layers slot. Pipe through style() to add layers.
See Also
Style verb: style() — the same verb chains onto a spec or
a template.
Locations: cells_body — locations that name the where
half of every layer.
Examples
# ---- Sponsor "house style" composed once ----
#
# The result becomes the default look for every table rendered
# against this preset. No per-table style() boilerplate.
house <- style_template() |>
style(bold = TRUE, .at = cells_headers(level = -1)) |>
style(bold = TRUE, .at = cells_group_headers()) |>
style(
border_top = brdr("thick", "double"),
border_bottom = brdr("thick", "double"),
.at = cells_headers()
) |>
style(blank_above = 1, blank_below = 1, .at = cells_title())
length(house$layers)
# ---- Verify class ----
is_style_template(house)
Partition the report by a variable
Description
Attach a subgroup_spec to a tabular_spec. At render time the
engine partitions spec@data by the unique values of by,
runs the full resolve pipeline per group, and concatenates the
results. A hard page break is inserted between groups —
every subgroup value starts on its own page. A centred banner
line appears above the column-header rule on every page of the
group (including continuation pages), matching the canonical
submission page-layout convention.
Usage
subgroup(.spec, by, label = NULL, big_n = NULL, big_n_fmt = "\n(N={n})")
Arguments
.spec |
The |
by |
Column name(s) to partition by.
Multi-variable. Pass |
label |
Banner template.
Tip: reference auxiliary columns to inline the BigN or
any qualifier that is constant within group — e.g.
Restriction: Every |
big_n |
Per-page BigN denominators.
# Wide: one column per arm.
wide <- data.frame(
sex = factor(c("F", "M")),
placebo = c(24L, 18L), drug_50 = c(9L, 15L), Total = c(42L, 47L)
)
# Long: count()-style, pivoted internally. Equivalent to `wide`.
long <- data.frame(
sex = factor(rep(c("F", "M"), each = 3)),
arm = rep(c("placebo", "drug_50", "Total"), 2),
n = c(24L, 9L, 42L, 18L, 15L, 47L)
)
spec |> subgroup(by = "sex", big_n = long)
Requirement: band keying needs Note: the per-arm N renders in every backend. The paged
backends (RTF, PDF / LaTeX, DOCX) carry it on the column header
that repeats on every page of the subgroup. HTML and Markdown are
continuous (one stacked table, one header), so they instead emit a
per-arm N row directly under each subgroup banner, the |
big_n_fmt |
Per-page BigN template.
|
Details
Label is a glue-style template. When label carries
{col} placeholders, the engine substitutes each placeholder
against the FIRST ROW of the group's filtered data — so any
column whose value is constant within group (BigN, cohort
descriptor, qualifier text) can ride into the banner. Columns
that vary within group also resolve, but always to the first
row's value; pre-compute aggregates upstream.
Default label (when label = NULL, single var): the engine
generates "<attr(data[[by]], 'label') %||% by>: {<by>}",
so subgroup(by = "cohort") renders banners like "Cohort: A"
and "Cohort: B" without further configuration.
Replace, not stack. A second subgroup() call REPLACES the
prior partition — subgroup is a single spec, not a stackable
list. Passing by = character(0) clears the slot, though
typical clinical pipelines set the partition once up front.
Display-side only. subgroup() partitions a pre-summarised
wide data frame; it does not aggregate, filter, or weight. The
user supplies one summary row per displayed row per group;
tabular's job is solely to lay them out with the per-group
banner and page break.
Multi-variable crossing. by = c("SEX", "AGEGR1") partitions
on every combination present in the data (first variable varies
slowest, matching expand.grid() convention). An explicit
label template is required for multi-var partitions since the
single-var default "<var>: {<var>}" does not generalise; raise
tabular_error_subgroup_label_required otherwise.
Auto-hide of partition + template columns. Every column named
in by, plus every column referenced via a {col} placeholder
in label, automatically flips to visible = FALSE at engine
time. Users do not restate col_spec(visible = FALSE) inside
cols() for these columns — mirroring the
col_spec(indent_by = ...) auto-hide ergonomic.
Value
The updated tabular_spec. Continue chaining or
resolve via as_grid() / emit().
See Also
Pipeline siblings: sort_rows(), paginate().
Resolve / render: as_grid(), emit().
Examples
# ---- Example 1: TEAEs by treatment arm — one set of pages per arm ----
#
# Partition the AE-by-SOC/PT pipeline by treatment arm. Each arm
# value gets its own page set with a centred `Treatment Arm: <value>`
# banner above the column-header rule on every page, separated by
# hard page breaks. The default label uses the variable's `label`
# attribute when present, falling back to the column name.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
ae <- cdisc_saf_aesocpt
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
ae$n_total <- as.integer(sub(" .*", "", ae$Total))
attr(ae$row_type, "label") <- "Row Type"
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by SOC and Preferred Term",
"Safety Population"
),
footnotes = "Subjects counted once per SOC and once per PT."
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE)) |>
subgroup(by = "row_type")
# ---- Example 2: Partition by Sex with inline BigN via template ----
#
# `label` is a glue-style template; any column whose value is
# constant within group can ride into the banner. `cdisc_saf_subgroup`
# ships partition-constant `sex_n` / `agegr_n` BigN columns
# alongside the value cells, so each banner reads
# `"Sex: F (N = 106)"`, etc. `sex` and `sex_n` auto-hide from the
# body (partition `by` and template-referenced columns).
tabular(cdisc_saf_subgroup, titles = "Vital Signs at End of Treatment") |>
cols(
agegr = col_spec(usage = "group", label = "Age Group"),
agegr_n = col_spec(visible = FALSE),
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
subgroup(by = "sex", label = "Sex: {sex} (N = {sex_n})")
# ---- Example 3: Multi-variable crossing (Sex x Age group) ----
#
# Pass two columns to partition on every combination present in
# the data. The label template MUST reference each variable
# explicitly because the single-var auto-default does not
# generalise. expand.grid order: first var (sex) varies slowest,
# second (agegr) fastest, giving banner sequence F/<65, F/>=65,
# M/<65, M/>=65.
tabular(cdisc_saf_subgroup, titles = "Vital Signs by Sex and Age Group") |>
cols(
sex_n = col_spec(visible = FALSE),
agegr_n = col_spec(visible = FALSE),
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
subgroup(
by = c("sex", "agegr"),
label = "Sex: {sex} / Age: {agegr}"
)
# ---- Example 4: Per-page BigN — different (N=) per sex page ----
#
# Each sex page has a different per-arm population, so the `(N=x)`
# in the arm headers must vary by page. `big_n` is a wide table:
# the `by` column plus one column per arm (named as the data
# column), cells are the page-specific Ns. Each arm header then
# reads e.g. `Placebo` over `(N=24)` on the Female page and
# `(N=18)` on the Male page. RTF / PDF / DOCX carry the N on the
# repeating header; HTML and Markdown add a per-arm N row under each
# banner.
big_n <- data.frame(
sex = factor(c("F", "M"), levels = c("F", "M")),
placebo = c(24L, 18L),
drug_50 = c(9L, 15L),
drug_100 = c(9L, 14L),
Total = c(42L, 47L)
)
tabular(cdisc_saf_subgroup, titles = "Vital Signs by Sex") |>
cols(
sex_n = col_spec(visible = FALSE),
agegr = col_spec(usage = "group", label = "Age Group"),
agegr_n = col_spec(visible = FALSE),
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
subgroup(by = "sex", label = "Sex: {sex}", big_n = big_n)
# ---- Example 5: Clear a partition with subgroup(character()) ----
#
# `subgroup(by = character())` (or `subgroup(by = NULL)`)
# explicitly clears any prior partition. Useful in
# programmatically-built pipelines where a downstream branch
# decides not to paginate by group after all — the call resets
# the spec back to a single-page-set render.
tabular(cdisc_saf_subgroup, titles = "Pooled (no sex partition)") |>
cols(
sex_n = col_spec(visible = FALSE),
agegr = col_spec(visible = FALSE),
agegr_n = col_spec(visible = FALSE),
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo", align = "decimal"),
drug_50 = col_spec(label = "Drug 50", align = "decimal"),
drug_100 = col_spec(label = "Drug 100", align = "decimal"),
Total = col_spec(label = "Total", align = "decimal")
) |>
subgroup("sex", label = "Sex: {sex}") |>
# Decide later that the sex split was the wrong default —
# clear it before rendering.
subgroup(character())
Start a tabular display
Description
Wrap a pre-summarised data frame into a tabular_spec ready for
the verb chain. tabular() is the entry verb — it owns the
data, titles, and footnotes slots; every downstream verb
(cols(), headers(), sort_rows(),
style(), paginate(), preset()) returns an updated
spec for further chaining, terminating in emit() (write to
file) or as_grid() (resolve without writing).
Usage
tabular(data, titles = NULL, footnotes = NULL)
Arguments
data |
The display rows.
Restriction: At least one column; column names must be
unique. Zero rows is accepted (engine renders a "No data" stub).
Interaction: The |
titles |
Page-title block, one element per row.
Restriction: No NAs. Each element supports glue-style # Canonical 3-line title block with BigN-qualified population. n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short) titles = c( "Table 14.3.1", "Adverse Events by System Organ Class and Preferred Term", "Safety Population" ) |
footnotes |
Page-footnote block, one element per row.
Restriction: No NAs. Each element supports glue-style # Canonical 3-line footnote block. footnotes = c( "Subjects are counted once per SOC and once per PT.", "Percentages based on N per treatment group.", "TEAE = treatment-emergent adverse event." ) |
Details
Pre-summarised input contract. data is one row per displayed
row of the final table. tabular() does not aggregate, filter,
weight, or generate subtotal rows — those happen upstream in
cards, dplyr, or SAS. If the upstream is a long
cards::ard_stack() ARD, pipe through pivot_across() first
to land in the wide shape tabular() accepts.
Multi-line titles and footnotes by contract. Clinical tables routinely carry 2-4 title rows and 1-4 user footnote rows. Pass each row as one element of the character vector; the backend renders each element on its own line, collapsing unused rows so the column-header band sits flush against the lowest used title.
Value
A tabular_spec S7 object. Pipe it into cols(),
headers(), sort_rows(), style(),
paginate(), and preset() to build the display, then
into emit() to render or as_grid() to resolve without
writing.
See Also
Downstream build verbs: cols() / col_spec(),
headers(), sort_rows(), style(),
paginate(), preset().
Terminal verbs: emit() (write), as_grid() (resolve
without I/O).
Input helper: pivot_across() (cards ARD -> wide).
Demo data: cdisc_saf_demo, cdisc_saf_aesocpt, cdisc_eff_resp, cdisc_saf_n,
cdisc_eff_n.
Examples
# ---- Example 1: Adverse-event table by SOC and Preferred Term ----
#
# The regulatory work-horse layout: AE-by-SOC/PT with the
# canonical 3-line title block (table number, description,
# population qualifier with BigN drawn inline from `cdisc_saf_n`) and a
# two-line footnote block explaining the denominator. The
# downstream pipeline hides the hierarchy markers (`row_type`,
# `soc_n`, `n_total`) but keeps them in the data so `sort_rows()`
# can arrange SOCs and PTs in descending order of subject count.
# The dataset already ships `n_total` and `soc_n`; here we slice to
# the overall row plus the two highest-incidence SOCs to keep the
# preview compact.
ae <- cdisc_saf_aesocpt
keep_soc <- head(unique(ae$soc[ae$row_type == "soc"]), 2L)
ae <- ae[ae$row_type == "overall" | ae$soc %in% keep_soc, ]
ae$row_type <- factor(ae$row_type, levels = c("overall", "soc", "pt"))
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
tabular(
ae,
titles = c(
"Table 14.3.1",
"Adverse Events by System Organ Class and Preferred Term",
"Safety Population"
),
footnotes = c(
"Subjects are counted once per SOC and once per PT.",
"Percentages based on N per treatment group."
)
) |>
cols(
label = col_spec(label = "SOC / PT", indent_by = "indent_level"),
soc = col_spec(visible = FALSE),
soc_n = col_spec(visible = FALSE),
row_type = col_spec(visible = FALSE),
n_total = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={n['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
Total = col_spec(label = "Total\nN={n['Total']}")
) |>
sort_rows(by = c("row_type", "n_total"), descending = c(FALSE, TRUE))
# ---- Example 2: Best overall response with CDISC factor ordering ----
#
# Efficacy table where response categories must appear in CDISC
# clinical order (CR < PR < SD < NON-CR/NON-PD < PD < NE <
# MISSING), then the derived ORR / CBR / DCR rate rows, not
# alphabetical. `groupid` keeps the four sections ordered while the
# `stat_label` factor orders the response block; `sort_rows()` does
# both in one pass. `groupid` / `group_label` ride along hidden.
bor_levels <- c(
"CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING",
"ORR (CR + PR)", "CBR (CR + PR + SD)",
"DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)"
)
eff <- cdisc_eff_resp
eff$stat_label <- factor(eff$stat_label, levels = bor_levels)
ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short)
tabular(
eff,
titles = c(
"Table 14.2.1",
"Best Overall Response and Response Rates",
"Efficacy Evaluable Population"
),
footnotes = "Response per RECIST 1.1, investigator assessment."
) |>
cols(
stat_label = col_spec(label = "Response"),
row_type = col_spec(visible = FALSE),
groupid = col_spec(visible = FALSE),
group_label = col_spec(visible = FALSE),
placebo = col_spec(label = "Placebo\nN={ne['placebo']}"),
drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"),
drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}")
) |>
sort_rows(by = c("groupid", "stat_label"))
# ---- Example 3: Minimal three-line BigN table from cdisc_saf_n ----
#
# The smallest viable `tabular()` call: the bundled `cdisc_saf_n` 4-row
# BigN table, a single-line title, no footnotes. The default
# `col_spec` per column kicks in, giving sensible labels (the
# data frame's column names) and left-aligned text. Useful when
# teaching the core API shape without the clinical-context
# surface noise.
tabular(cdisc_saf_n, titles = "Safety-population BigN per arm")
# ---- Example 4: Vital-signs panel with hidden code column ----
#
# Show the canonical vitals shape: one parameter across four visits
# x four statistics. The CDISC `paramcd` is kept in the data frame
# as the natural sort key but hidden at render via
# `col_spec(visible = FALSE)`, while `param` (the display label)
# drives the group block. Slice to a single `paramcd` for a compact
# preview; the full 4-parameter frame renders identically.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
vs <- cdisc_saf_vital[cdisc_saf_vital$paramcd == cdisc_saf_vital$paramcd[1L], ]
tabular(
vs,
titles = c(
"Table 14.4.1",
"Summary of Vital Signs",
"Safety Population"
),
footnotes = "Statistics computed on observed cases."
) |>
cols(
paramcd = col_spec(visible = FALSE),
param = col_spec(usage = "group", label = "Parameter"),
visit = col_spec(usage = "group", label = "Visit"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(
label = "Placebo\nN={n['placebo']}",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50\nN={n['drug_50']}",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100\nN={n['drug_100']}",
align = "decimal"
)
)
tabular S7 classes
Description
S7 class definitions backing tabular's display-side IR. Users do
not construct these directly except for col_spec(); every
other class is built and chained by the verb pipeline
(tabular() -> cols() -> headers() -> sort_rows()
-> style() -> paginate() -> preset() -> as_grid()
/ emit()).
Details
The class set is intentionally small (~11 concepts) so the IR fits in one mental model:
| class | role | constructor |
tabular_spec | root container; carries data + every other spec slot | tabular() |
col_spec | per-column DSL (usage, label, format, align, ...) | col_spec() |
header_node | one node in the multi-level header tree | internal — built by headers() |
sort_spec | sort keys + per-key direction | internal — built by sort_rows() |
style_node | one resolved style attribute set (per-cell) | internal — built by style() |
style_predicate | (legacy) one where quosure + scope + style_node | internal — built by style() |
style_layer | one tabular_location + style_node | internal — built by style() |
style_spec | the cascade root (defaults + cols + headers + layers) | internal — built by style() |
pagination_spec | page-split policy (keep_together, panels, floors) | internal — built by paginate() |
preset_spec | render geometry (paper, orientation, font, margins) | internal — built by preset() |
inline_ast | parsed inline-formatting AST (runs of bold / sup / …) | internal — built by parse_inline() |
tabular_grid | resolved per-page cells + ASTs + styles + headers | as_grid()
|
Every spec slot is typed: a verb that would mutate a slot to an invalid value fails at construction time (the S7 validator runs as a last-line defense behind the cli-friendly verb-level validators).
Class predicates. Each class has a matching is_<name>()
predicate; see tabular_predicates for the full list.
See Also
Class predicates: tabular_predicates.
Pipeline entry verbs: tabular(), as_grid(),
emit().
Test for tabular S7 class instances
Description
Class predicates returning a single logical indicating whether
x inherits from the corresponding tabular S7 class. Use them
to gate user-side code that branches on what a verb has
returned, to write defensive helpers that wrap tabular pipelines,
or to assert intermediate shapes during pipeline debugging.
Usage
is_tabular_spec(x)
is_tabular_grid(x)
is_col_spec(x)
is_header_node(x)
is_sort_spec(x)
is_style_node(x)
is_style_layer(x)
is_style_spec(x)
is_pagination_spec(x)
is_preset_spec(x)
is_subgroup_spec(x)
is_inline_ast(x)
Arguments
x |
Object to test. Any R value. Each predicate returns
|
Details
Eleven predicates cover the full S7 surface:
| predicate | tests for | produced by |
is_tabular_spec() | tabular_spec | tabular() and every build verb |
is_tabular_grid() | tabular_grid | as_grid() |
is_col_spec() | col_spec | col_spec() |
is_header_node() | header_node | headers() (internal nodes) |
is_sort_spec() | sort_spec | sort_rows() |
is_style_node() | style_node | style() (per-cell style) |
is_style_predicate() | style_predicate | (legacy) style() predicate path |
is_style_layer() | style_layer | style() (one per call) |
is_style_spec() | style_spec | style() (the cascade root) |
is_pagination_spec() | pagination_spec | paginate() |
is_preset_spec() | preset_spec | preset(), set_preset() |
is_subgroup_spec() | subgroup_spec | subgroup() |
is_inline_ast() | inline_ast | parse_inline() (post-format)
|
Predicates never error — they return FALSE for NULL, vectors,
objects of any other class, and S7 objects from other packages.
Use them at any layer of a user's pipeline without a defensive
tryCatch().
Value
A single TRUE / FALSE. Use in if / stopifnot
guards, or chain into validation helpers.
A length-1 logical — TRUE or FALSE. Never NA.
See Also
Class definitions: tabular_classes.
Verbs producing each class: tabular(), col_spec(),
headers(), sort_rows(), style(), paginate(),
preset(), as_grid().
Examples
# ---- Example 1: Gate user-side code on the spec class ----
#
# A user-side helper that pre-validates its input before piping
# into a downstream tabular chain. The predicate returns FALSE
# for any non-spec input without raising, so the helper can emit
# a friendlier error than tabular's own S7 validator would.
add_safety_footnote <- function(spec) {
if (!is_tabular_spec(spec)) {
stop("`spec` must be a tabular_spec; build one with tabular().")
}
spec
}
demo <- tabular(cdisc_saf_demo, titles = "Demographics")
is_tabular_spec(demo) # TRUE
is_tabular_spec("not a spec") # FALSE — does not raise
add_safety_footnote(demo)
# ---- Example 2: Assert intermediate shapes during debugging ----
#
# When chaining many verbs, dropping `stopifnot()` between verbs
# gives a clear stack trace if a verb silently returns the wrong
# type. Predicates are cheap (single S7 dispatch each) and never
# error, so they are safe to leave in pipelines during dev.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
spec <- tabular(
cdisc_saf_demo,
titles = c("Table 14.1.1", "Demographics",
"Safety Population")
) |>
cols(
variable = col_spec(usage = "group", label = "Characteristic"),
stat_label = col_spec(label = "Statistic"),
placebo = col_spec(label = "Placebo\nN={n['placebo']}", align = "decimal"),
drug_50 = col_spec(label = "Drug 50\nN={n['drug_50']}", align = "decimal"),
drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
Total = col_spec(label = "Total\nN={n['Total']}", align = "decimal")
) |>
sort_rows(by = c("variable", "stat_label"))
stopifnot(
is_tabular_spec(spec),
is_col_spec(spec@cols[["placebo"]]),
is_sort_spec(spec@sort)
)
grid <- as_grid(spec)
stopifnot(is_tabular_grid(grid))