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.
Arguments
- .spec
The
tabular_specto extend.<tabular_spec>: required. Dot-prefixed so R's partial argument matching cannot accidentally bind a short user-supplied name (e.g.s,sp) in...to the spec slot. Pipe input (tabular(...) |> cols(...)) works the normal way — the spec is supplied positionally.- ...
Named
col_specobjects, one per column. Each name is the input column name in.spec@data. Names must match an existing column — pre-compute derived columns upstream withdplyr::mutate()(or equivalent) beforetabular().Restriction: Names must be unique within a single
cols()call (duplicates warn; "last value wins"). Tip: To override an attribute already declared, use a secondcols()call downstream and let the merge rule apply.- .default
Fallback
col_specfor unmentioned columns.<col_spec | NULL>: default NULL. When acol_spec, it is field-merged onto every data column that is NOT named in...and does not already carry a spec from an earliercols()call.NULL(default) leaves unmentioned columns to the engine-time default. Use it to set one alignment / format across a variable number of arm columns in a single call.Interaction: Explicit
...specs always win —.defaultonly fills the gaps. A column carried over from a priorcols()call is treated as already specified and is left untouched.
Value
The updated tabular_spec. Continue chaining with
headers(), sort_rows(), style().
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.
Layout order follows the data frame, not cols(). The
left-to-right column order in the rendered table is the column
order of .spec@data; the order of the named ... arguments here
is irrelevant (they are a lookup keyed by name). To move a column,
reorder the data frame upstream — a column derived with
df$new <- ... is appended last and will render last unless you
reorder.
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.
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 field set to a non-default value on the new spec overrides;
a field left at its "unset" sentinel leaves the existing value
intact. Every mergeable field has a genuine unset sentinel, so a
later call can also restore a default (e.g. visible = TRUE
re-shows a hidden column, group_display = "header_row" resets a
prior "column"). 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).
Unset sentinels — a field left at this value does NOT override the
existing field (every other value, including a default like
visible = TRUE, overrides):
| field | unset sentinel |
usage | NA |
label | NA_character_ |
format | NULL |
visible | NA |
width | "auto" |
group_display | NA |
group_skip | NA |
align | NA_character_ |
valign | NA_character_ |
na_text | NA_character_ (inherit preset) |
indent | NA |
# 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().
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"))
Table 14.1.1
Demographics and Baseline Characteristics
Safety Population
Statistic Placebo
N=86 Drug 50
N=96 Drug 100
N=72 Total
N=254 Age (years) Mean (SD) 75.2 (8.59) 76.0 (8.11) 73.8 (7.94) 75.1 (8.25) Median 76.0 78.0 75.5 77.0 Min, Max 52 , 89 51 , 88 56 , 88 51 , 89 Q1, Q3 69.2, 81.8 71.0, 82.0 70.5, 79.0 70.0, 81.0 n 86 96 72 254 Race, n (%) AMERICAN INDIAN OR ALASKA NATIVE 0 0 1 ( 1.4) 1 ( 0.4) ASIAN 0 0 0 0 BLACK OR AFRICAN AMERICAN 8 ( 9.3) 6 ( 6.2) 9 (12.5) 23 ( 9.1) WHITE 78 (90.7) 90 (93.8) 62 (86.1) 230 (90.6) Sex, n (%) F 53 (61.6) 55 (57.3) 35 (48.6) 143 (56.3) M 33 (38.4) 41 (42.7) 37 (51.4) 111 (43.7)
Percentages based on N per treatment group.
# ---- 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"))
Table 14.2.1
Best Overall Response and Response Rates
Efficacy Evaluable Population
Response Placebo
N=86 Drug 50
N=84 Drug 100
N=84 CR 1 ( 1.2) 1 ( 1.2) 1 ( 1.2) PR 1 ( 1.2) 0 0 SD 1 ( 1.2) 0 0 NON-CR/NON-PD 0 0 1 ( 1.2) PD 0 0 1 ( 1.2) NE 0 1 ( 1.2) 0 MISSING 83 (96.5) 82 (97.6) 81 (96.4) ORR (CR + PR) 2 ( 2.3) 1 ( 1.2) 1 ( 1.2) 95% CI (Clopper-Pearson) ( 0.3, 8.1) ( 0.0, 6.5) ( 0.0, 6.5) CBR (CR + PR + SD) 3 ( 3.5) 1 ( 1.2) 1 ( 1.2) 95% CI (Clopper-Pearson) ( 0.7, 9.9) ( 0.0, 6.5) ( 0.0, 6.5) DCR (CR + PR + SD + NON-CR/NON-PD) 3 ( 3.5) 1 ( 1.2) 2 ( 2.4) 95% CI (Clopper-Pearson) ( 0.7, 9.9) ( 0.0, 6.5) ( 0.3, 8.3)
Response per RECIST 1.1, investigator assessment.
# ---- 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 = "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, align, visible)
# without restating them.
tabular(
cdisc_saf_aesocpt,
titles = c("Table 14.3.1", "Adverse Events by SOC and Preferred Term")
) |>
cols(
label = col_spec(label = "SOC / PT", indent = "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("soc_n", "n_total"), descending = c(TRUE, 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")
)
Table 14.3.1
Adverse Events by SOC and Preferred Term
SOC / PT Placebo Drug 50 Drug 100 Total TOTAL SUBJECTS WITH AN EVENT 52 (60.5) 81 (84.4) 66 (91.7) 199 (78.3) SKIN AND SUBCUTANEOUS TISSUE DISORDERS 19 (22.1) 36 (37.5) 35 (48.6) 90 (35.4) PRURITUS 8 ( 9.3) 21 (21.9) 25 (34.7) 54 (21.3) ERYTHEMA 8 ( 9.3) 14 (14.6) 14 (19.4) 36 (14.2) RASH 5 ( 5.8) 13 (13.5) 8 (11.1) 26 (10.2) HYPERHIDROSIS 2 ( 2.3) 4 ( 4.2) 8 (11.1) 14 ( 5.5) SKIN IRRITATION 3 ( 3.5) 6 ( 6.2) 5 ( 6.9) 14 ( 5.5) GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS 15 (17.4) 36 (37.5) 30 (41.7) 81 (31.9) APPLICATION SITE PRURITUS 6 ( 7.0) 23 (24.0) 21 (29.2) 50 (19.7) APPLICATION SITE ERYTHEMA 3 ( 3.5) 13 (13.5) 14 (19.4) 30 (11.8) APPLICATION SITE DERMATITIS 5 ( 5.8) 9 ( 9.4) 7 ( 9.7) 21 ( 8.3) APPLICATION SITE IRRITATION 3 ( 3.5) 9 ( 9.4) 9 (12.5) 21 ( 8.3) APPLICATION SITE VESICLES 1 ( 1.2) 5 ( 5.2) 5 ( 6.9) 11 ( 4.3) GASTROINTESTINAL DISORDERS 13 (15.1) 12 (12.5) 17 (23.6) 42 (16.5) DIARRHOEA 9 (10.5) 5 ( 5.2) 3 ( 4.2) 17 ( 6.7) VOMITING 3 ( 3.5) 4 ( 4.2) 6 ( 8.3) 13 ( 5.1) NAUSEA 3 ( 3.5) 3 ( 3.1) 6 ( 8.3) 12 ( 4.7) ABDOMINAL PAIN 1 ( 1.2) 3 ( 3.1) 1 ( 1.4) 5 ( 2.0) SALIVARY HYPERSECRETION 0 0 4 ( 5.6) 4 ( 1.6) NERVOUS SYSTEM DISORDERS 6 ( 7.0) 18 (18.8) 17 (23.6) 41 (16.1) DIZZINESS 2 ( 2.3) 9 ( 9.4) 10 (13.9) 21 ( 8.3) HEADACHE 3 ( 3.5) 3 ( 3.1) 5 ( 6.9) 11 ( 4.3) SYNCOPE 0 5 ( 5.2) 2 ( 2.8) 7 ( 2.8) SOMNOLENCE 2 ( 2.3) 3 ( 3.1) 1 ( 1.4) 6 ( 2.4) TRANSIENT ISCHAEMIC ATTACK 0 2 ( 2.1) 1 ( 1.4) 3 ( 1.2) CARDIAC DISORDERS 7 ( 8.1) 12 (12.5) 14 (19.4) 33 (13.0) SINUS BRADYCARDIA 2 ( 2.3) 7 ( 7.3) 8 (11.1) 17 ( 6.7) MYOCARDIAL INFARCTION 4 ( 4.7) 2 ( 2.1) 4 ( 5.6) 10 ( 3.9) ATRIAL FIBRILLATION 1 ( 1.2) 2 ( 2.1) 2 ( 2.8) 5 ( 2.0) SUPRAVENTRICULAR EXTRASYSTOLES 1 ( 1.2) 1 ( 1.0) 1 ( 1.4) 3 ( 1.2) VENTRICULAR EXTRASYSTOLES 0 2 ( 2.1) 1 ( 1.4) 3 ( 1.2) INFECTIONS AND INFESTATIONS 12 (14.0) 6 ( 6.2) 11 (15.3) 29 (11.4) NASOPHARYNGITIS 2 ( 2.3) 4 ( 4.2) 6 ( 8.3) 12 ( 4.7) UPPER RESPIRATORY TRACT INFECTION 6 ( 7.0) 1 ( 1.0) 3 ( 4.2) 10 ( 3.9) INFLUENZA 1 ( 1.2) 1 ( 1.0) 1 ( 1.4) 3 ( 1.2) URINARY TRACT INFECTION 2 ( 2.3) 0 1 ( 1.4) 3 ( 1.2) CYSTITIS 1 ( 1.2) 0 1 ( 1.4) 2 ( 0.8) RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS 5 ( 5.8) 8 ( 8.3) 9 (12.5) 22 ( 8.7) COUGH 1 ( 1.2) 5 ( 5.2) 5 ( 6.9) 11 ( 4.3) NASAL CONGESTION 3 ( 3.5) 1 ( 1.0) 3 ( 4.2) 7 ( 2.8) DYSPNOEA 1 ( 1.2) 1 ( 1.0) 1 ( 1.4) 3 ( 1.2) EPISTAXIS 0 1 ( 1.0) 2 ( 2.8) 3 ( 1.2) PHARYNGOLARYNGEAL PAIN 0 1 ( 1.0) 1 ( 1.4) 2 ( 0.8) PSYCHIATRIC DISORDERS 7 ( 8.1) 9 ( 9.4) 3 ( 4.2) 19 ( 7.5) CONFUSIONAL STATE 2 ( 2.3) 3 ( 3.1) 1 ( 1.4) 6 ( 2.4) AGITATION 2 ( 2.3) 3 ( 3.1) 0 5 ( 2.0) INSOMNIA 2 ( 2.3) 0 2 ( 2.8) 4 ( 1.6) ANXIETY 0 3 ( 3.1) 0 3 ( 1.2) DELUSION 1 ( 1.2) 0 1 ( 1.4) 2 ( 0.8) MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS 3 ( 3.5) 6 ( 6.2) 5 ( 6.9) 14 ( 5.5) BACK PAIN 1 ( 1.2) 1 ( 1.0) 3 ( 4.2) 5 ( 2.0) ARTHRALGIA 1 ( 1.2) 2 ( 2.1) 1 ( 1.4) 4 ( 1.6) SHOULDER PAIN 1 ( 1.2) 2 ( 2.1) 0 3 ( 1.2) MUSCLE SPASMS 0 1 ( 1.0) 1 ( 1.4) 2 ( 0.8) ARTHRITIS 0 0 1 ( 1.4) 1 ( 0.4) INVESTIGATIONS 5 ( 5.8) 4 ( 4.2) 3 ( 4.2) 12 ( 4.7) ELECTROCARDIOGRAM ST SEGMENT DEPRESSION 4 ( 4.7) 1 ( 1.0) 0 5 ( 2.0) ELECTROCARDIOGRAM T WAVE INVERSION 2 ( 2.3) 1 ( 1.0) 1 ( 1.4) 4 ( 1.6) BLOOD GLUCOSE INCREASED 0 1 ( 1.0) 1 ( 1.4) 2 ( 0.8) ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED 1 ( 1.2) 1 ( 1.0) 0 2 ( 0.8) BIOPSY 0 0 1 ( 1.4) 1 ( 0.4)
# ---- 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()`).
#
# Column LAYOUT order follows the data frame, not the `cols()`
# argument order. A derived column appended with `ae$active <- ...`
# would render last, so reorder the frame to place it where it
# should display (here between Placebo and Total).
ae <- cdisc_saf_ae
ae$active <- paste0(ae$drug_50, " / ", ae$drug_100)
ae <- ae[c("stat_label", "placebo", "active", "Total", "drug_50", "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(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")
)
Table 14.3.0
Adverse Event Overview
Placebo Active arms Total Any TEAE 65 (75.6) 84 (87.5) / 68 (94.4) 217 (85.4) Any Serious AE (SAE) 0 2 (2.1) / 1 (1.4) 3 ( 1.2) Any AE Related to Study Drug 43 (50.0) 77 (80.2) / 64 (88.9) 184 (72.4) Any AE Leading to Death 2 ( 2.3) 1 (1.0) / 0 (0.0) 3 ( 1.2) Any AE Recovered / Resolved 47 (54.7) 61 (63.5) / 49 (68.1) 157 (61.8) Maximum severity: Mild 36 (41.9) 21 (21.9) / 20 (27.8) 77 (30.3) Maximum severity: Moderate 24 (27.9) 47 (49.0) / 40 (55.6) 111 (43.7) Maximum severity: Severe 5 ( 5.8) 16 (16.7) / 8 (11.1) 29 (11.4)
Active = pooled Drug 50 + Drug 100 columns.