Configures the display label, width, alignment, visibility, N-counts, and
spanning groups for table columns. fr_cols() is the single source of
truth for all column structure; fr_header() owns header
presentation only (bold, colours, font size).
Columns not explicitly named here receive auto-generated defaults: the
column name as the label (optionally transformed by .label_fn),
alignment inferred from the R column type (numeric → "right",
everything else → "left"), and width from .width.
Calling fr_cols() again replaces the entire column configuration.
Usage
fr_cols(
spec,
...,
.list = NULL,
.width = NULL,
.align = NULL,
.label_fn = NULL,
.spaces = NULL,
.split = NULL,
.n = NULL,
.n_format = NULL
)Arguments
- spec
An
fr_specobject fromfr_table().- ...
Column specifications. Can be provided in two ways:
Named arguments: The argument name must match a column name in the data frame. The value can be an
fr_col()object or a character scalar (label).Formulas (tidyselect): Use
lhs ~ rhswherelhsis a set of columns selected via tidyselect::language (e.g.,starts_with("col")), andrhsis anfr_col()object or character scalar applied to all selected columns. Powered by thetidyselectpackage.
Any data column not listed receives auto-generated defaults.
- .list
Named list or character vector of pre-built labels. Allows you to construct column labels externally (e.g., using
sprintf()) and pass them in programmatically. Example:list(zom_50mg = "Zomerane 50 mg").- .width
Default column width applied to all columns that do not have an explicit width set. Accepts:
Numeric — width in inches (e.g.
1.5). Default whenNULL."auto"— auto-calculate widths from content and header text using the page font metrics. The layout engine measures the widest cell value and the header label for each column, adds padding, and scales down if the total exceeds the printable page width. Columns that already fit are left as-is."fit"— like"auto", but always scales (up or down) so the total width exactly fills the printable page width, preserving the content-based ratio between columns. Use this when you want every table to span the full page width without manual tuning."equal"— distribute the printable page width equally among all columns that do not have a fixed width. Columns with an explicit numericwidthinfr_col()keep their size; the remaining space is divided equally.Percentage string — e.g.
"25%". Sets every column's default width as a fraction of the printable page width. Must be between"0%"(exclusive) and"100%"(inclusive). Columns with an explicitwidthinfr_col()are not affected.NULL(default) — same as"auto". Columns auto-size from content and header text.
- .align
Default alignment applied to all columns that do not have an explicit alignment set. One of
"left","center","right","decimal".NULLauto-detects:"right"for numeric columns,"left"for everything else.- .label_fn
A function (or rlang-style lambda) applied to auto-generated column labels. Only affects columns whose labels were not explicitly set via
...or.list. Receives the column name as input, returns the display label.Common transforms:
~ gsub("_", " ", .x)— replace underscores with spaces.~ tools::toTitleCase(gsub("_", " ", .x))— title case.toupper— all caps.
- .spaces
How to handle leading spaces in cell data for all columns that do not have an explicit
spacesset infr_col(). One of:"indent"(default) — convert leading spaces to paragraph-level indent. The indent width is measured from the page font metrics, so it renders correctly in both proportional and monospace fonts."preserve"— keep leading spaces as literal characters.NULL— same as"indent".
- .split
Logical or
NULL. Column splitting for wide tables that exceed the printable page width:NULL(default) — no splitting. All columns on one page.TRUE— split across multiple panels, with stub columns repeated in each panel. Panel width behaviour follows.width:.width = "auto"→ panels keep natural column widths..width = "fit"→ panels scale to fill the page width..width = "equal"→ unfixed columns share remaining space equally.
FALSE— explicitly no splitting.
Stub columns (repeated in every panel) are designated via
fr_col(stub = TRUE). When.splitis enabled but no columns havestub = TRUE, stubs are auto-inferred fromgroup_by/indent_bycolumns or the first column.- .n
Bulk N-counts applied across columns and spanning groups. Names are matched case-insensitively using a two-step lookup: first by column display label, then by data column name as fallback. This means you can use either form:
c("Placebo" = 45)— matches by labelc(placebo = 45)— matches by column name (no label repetition)
Accepts:
Named numeric vector — names = display labels or column names, matched case-insensitively to column labels first, then column names. Also matches spanning group names. Example:
c(placebo = 45, zom_50mg = 44).Data frame (2-column) — column 1 = display labels, column 2 = counts. Same N on every page. Example:
data.frame(trt = c("Placebo", "Zom 50mg"), n = c(45, 44)).Data frame (3-column) — column 1 =
page_bygroup values, column 2 = display labels, column 3 = counts. Different N perpage_bygroup.Named list — keys =
page_bygroup values, values = named numeric vectors (names = display labels). Example:list("Systolic BP" = c("Placebo" = 42, "Zom" = 40)).
Auto-routing: when a label matches a
groupname (spanning header), N goes on the span. When it matches a column label, N goes on the column. Group matches take priority (no double-apply).Per-column
fr_col(n = ...)always takes highest priority.- .n_format
A glue-style format string for N-count labels. Available tokens:
{label}(column display label) and{n}(count). DefaultNULLinherits from config YAMLcolumns.n_formator theme. Example:"{label}\\n(N={n})".
Regulatory conventions — column ordering
Standard pharma house styles and FDA/EMA submissions require:
Active treatment arm(s) left, in order of dose escalation (lowest to highest dose, left to right).
Comparator / placebo arm(s) rightmost among the study arm columns.
Total column (if included): optional; covers active arms only (does not pool active + placebo). Placed immediately after the rightmost active arm column, before the placebo column.
Example column order for a 2-dose + placebo study:
Label resolution order
Column labels are resolved in this priority (highest wins):
N-count formatting (
fr_col(n=)or.n+.n_format) — dynamic label with N counts, applied at render time.Explicit
fr_col(label = ...)in...arguments..list— programmatic label map..label_fn— transform function applied to the column name.Column name — the raw data frame column name as-is.
Tips
Column order in the rendered table matches the data frame column order, not the order you list them in
fr_cols(). To reorder columns, reorder the data frame first.Use
visible = FALSEto suppress a column from rendering while keeping it available as a grouping key forfr_rows().align = "decimal"aligns decimal points in numeric columns.Width is in inches. Landscape Letter with 1 in margins gives 9 in printable width. A common pharma layout: stub column 2.5 in + 4–5 data columns at 1.3–1.5 in each.
.width = "auto"is the fastest way to get a working table — the layout engine calculates widths from content. Use fixed widths only when you need exact control..label_fntransforms only auto-generated labels (column names that were not explicitly relabelled). This lets you set a baseline transform and override specific columns as needed.
See also
fr_col() for the column spec constructor, fr_header() for
header presentation (bold, colours, alignment), fr_spans() for
advanced multi-level spanning headers, fr_rows() for pagination.
Examples
## ── Per-column N-counts (80% case) ───────────────────────────────────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
zom_50mg = fr_col("Zomerane 50 mg", n = 45),
zom_100mg = fr_col("Zomerane 100 mg", n = 45),
placebo = fr_col("Placebo", n = 45),
total = fr_col("Total", n = 135),
.n_format = "{label}\n(N={n})"
) |>
fr_header(bold = TRUE, align = "center")
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "Zomerane 50 mg" 1.20in left
#> zom_100mg "Zomerane 100 mg" 1.27in left
#> total "Total" 0.97in left
#> group "group" 0.90in left
#> Header: bold, valign=bottom, align=center
## ── Bulk N by column name (no label repetition) ────────────────────────
# .n keys match column names — labels only defined once in fr_col()
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
placebo = fr_col("Placebo"),
zom_50mg = fr_col("Zomerane 50 mg"),
zom_100mg = fr_col("Zomerane 100 mg"),
total = fr_col("Total"),
.n = c(placebo = 45, zom_50mg = 45, zom_100mg = 45, total = 135),
.n_format = "{label}\n(N={n})"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "Zomerane 50 mg" 1.20in left
#> zom_100mg "Zomerane 100 mg" 1.27in left
#> total "Total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Bulk N from a data frame ─────────────────────────────────────────────
adsl_n <- data.frame(
trt = c("Placebo", "Zomerane 50 mg", "Zomerane 100 mg", "Total"),
n = c(45L, 45L, 45L, 135L)
)
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
placebo = fr_col("Placebo"),
zom_50mg = fr_col("Zomerane 50 mg"),
zom_100mg = fr_col("Zomerane 100 mg"),
total = fr_col("Total"),
.n = adsl_n,
.n_format = "{label}\n(N={n})"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "Zomerane 50 mg" 1.20in left
#> zom_100mg "Zomerane 100 mg" 1.27in left
#> total "Total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Spanning groups via group= ───────────────────────────────────────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
zom_50mg = fr_col("50 mg", group = "Zomerane"),
zom_100mg = fr_col("100 mg", group = "Zomerane"),
placebo = fr_col("Placebo"),
total = fr_col("Total")
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "50 mg" 0.97in left
#> zom_100mg "100 mg" 0.97in left
#> total "Total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Bulk N auto-routes to spans and columns ──────────────────────────────
n_df <- data.frame(
trt = c("Zomerane", "Placebo", "Total"),
n = c(90L, 45L, 135L)
)
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
zom_50mg = fr_col("50 mg", group = "Zomerane"),
zom_100mg = fr_col("100 mg", group = "Zomerane"),
placebo = fr_col("Placebo"),
total = fr_col("Total"),
.n = n_df,
.n_format = "{label}\n(N={n})"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "50 mg" 0.97in left
#> zom_100mg "100 mg" 0.97in left
#> total "Total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Auto-width: let the engine calculate ─────────────────────────────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic"),
.width = "auto"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.70in left
#> placebo "placebo" 0.97in left
#> zom_50mg "zom_50mg" 0.97in left
#> zom_100mg "zom_100mg" 0.97in left
#> total "total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Percentage widths: responsive to page size ───────────────────────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = "30%"),
zom_50mg = fr_col("Zomerane 50mg", width = "17.5%", align = "right"),
zom_100mg = fr_col("Zomerane 100mg", width = "17.5%", align = "right"),
placebo = fr_col("Placebo", width = "17.5%", align = "right"),
total = fr_col("Total", width = "17.5%", align = "right")
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 30% left
#> placebo "Placebo" 18% right
#> zom_50mg "Zomerane 50mg" 18% right
#> zom_100mg "Zomerane 100mg" 18% right
#> total "Total" 18% right
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Fit mode: fill the full page width proportionally ────────────────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic"),
.width = "fit"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 3.24in left
#> placebo "placebo" 1.17in left
#> zom_50mg "zom_50mg" 1.17in left
#> zom_100mg "zom_100mg" 1.17in left
#> total "total" 1.17in left
#> group "group" 1.08in left
#> Header: valign=bottom
## ── Equal-width distribution ─────────────────────────────────────────────
# Stub column fixed at 2.5in; remaining columns share the rest equally
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
.width = "equal"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "placebo" 1.30in left
#> zom_50mg "zom_50mg" 1.30in left
#> zom_100mg "zom_100mg" 1.30in left
#> total "total" 1.30in left
#> group "group" 1.30in left
#> Header: valign=bottom
## ── Label transform: underscores to spaces + title case ──────────────────
tbl_demog |>
fr_table() |>
fr_cols(.label_fn = ~ tools::toTitleCase(gsub("_", " ", .x)))
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.70in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "Zom 50mg" 0.97in left
#> zom_100mg "Zom 100mg" 0.97in left
#> total "Total" 0.97in left
#> group "Group" 0.90in left
#> Header: valign=bottom
## ── Tidyselect formula (apply config to multiple columns) ────────────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
c(zom_50mg, zom_100mg) ~ fr_col(width = 1.5, align = "right"),
starts_with("t") ~ fr_col(width = 1.5, align = "right")
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "placebo" 0.97in left
#> zom_50mg "" 1.50in right
#> zom_100mg "" 1.50in right
#> total "" 1.50in right
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Pre-formatted labels via .list ───────────────────────────────────────
labels_vec <- c(placebo = "Placebo", zom_50mg = "Zom 50mg")
tbl_demog |>
fr_table() |>
fr_cols(.list = labels_vec)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "characteristic" 2.70in left
#> placebo "Placebo" 0.97in left
#> zom_50mg "Zom 50mg" 0.97in left
#> zom_100mg "zom_100mg" 0.97in left
#> total "total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Leading spaces → paragraph-level indent (default) ──────────────────
# tbl_demog has leading spaces (" Mean (SD)", " <65", etc.)
# Default .spaces = "indent" converts them to real paragraph indent
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
.spaces = "indent"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "placebo" 0.97in left
#> zom_50mg "zom_50mg" 0.97in left
#> zom_100mg "zom_100mg" 0.97in left
#> total "total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Preserve leading spaces as literal characters ─────────────────────
# Use "preserve" for pre-formatted content with exact spacing
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5),
.spaces = "preserve"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "placebo" 0.97in left
#> zom_50mg "zom_50mg" 0.97in left
#> zom_100mg "zom_100mg" 0.97in left
#> total "total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Per-column override: preserve one column, indent the rest ─────────
tbl_demog |>
fr_table() |>
fr_cols(
characteristic = fr_col("Characteristic", width = 2.5, spaces = "preserve"),
.spaces = "indent"
)
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (6 visible of 6):
#> characteristic "Characteristic" 2.50in left
#> placebo "placebo" 0.97in left
#> zom_50mg "zom_50mg" 0.97in left
#> zom_100mg "zom_100mg" 0.97in left
#> total "total" 0.97in left
#> group "group" 0.90in left
#> Header: valign=bottom
## ── Column splitting: split + fit to fill page ─────────────────────────
tbl_vs |>
fr_table() |>
fr_cols(
param = fr_col("Parameter", stub = TRUE),
statistic = fr_col("Statistic", stub = TRUE),
.split = TRUE,
.width = "fit"
)
#>
#> ── fr_spec: Table
#> Data: 60 rows x 12 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (12 visible of 12):
#> param "Parameter" 1.06in left
#> timepoint "timepoint" 0.55in left
#> statistic "Statistic" 0.55in left
#> placebo_base "placebo_base" 0.75in left
#> placebo_value "placebo_value" 0.75in left
#> placebo_chg "placebo_chg" 0.65in left
#> zom_50mg_base "zom_50mg_base" 0.75in left
#> zom_50mg_value "zom_50mg_value" 0.80in left
#> ... and 4 more
#> Header: valign=bottom
## ── Column splitting with natural widths (no stretching) ────────────────
tbl_vs |>
fr_table() |>
fr_cols(.split = TRUE)
#>
#> ── fr_spec: Table
#> Data: 60 rows x 12 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (12 visible of 12):
#> param "param" 1.57in left
#> timepoint "timepoint" 0.82in left
#> statistic "statistic" 0.82in left
#> placebo_base "placebo_base" 1.12in left
#> placebo_value "placebo_value" 1.12in left
#> placebo_chg "placebo_chg" 0.97in left
#> zom_50mg_base "zom_50mg_base" 1.12in left
#> zom_50mg_value "zom_50mg_value" 1.20in left
#> ... and 4 more
#> Header: valign=bottom