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 = NA,
width = "auto",
group_display = NA,
group_skip = NA,
align = NULL,
valign = NULL,
na_text = NA_character_,
indent = NA
)Arguments
- usage
Engine role.
<character(1) | NULL>: default NULL. One of:"display"(default incols()) — pass-through."group"— row-label with repeat-suppression and continuation-page repeat keys. Use forvariable,soc,stat_label. (Cosmetic indent depth is the separateindentargument, not a usage role.)"id"— a row-identifier column. Renders like"display"(one value per row, never collapses) but joins the stub: it repeats on every horizontal panel (paginate(panels = N)) and shows once on the left when a continuous backend (HTML / Markdown) collapses the panels into one table. The PROC REPORTIDrole, orthogonal to grouping. Use for a per-row statistic label ("n","Mean","SD") that must stay legible on every panel of a wide demographics or efficacy table.NULL/NA— the unset sentinel; resolves to"display"at render.NAis mergeable, so an explicit"display"on a latercols()call can override a prior"group"/"id".
# 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(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.
<character(1)>: default NA_character_. Embed\nfor multi-line headers (arm name on row 1, BigN denominator on row 2 is the clinical convention).NA_character_means use the input column name verbatim.Restriction: Empty string and whitespace-only labels are accepted here, unlike
headers()band labels which are strict.Supports glue-style
{expr}interpolation: braces are evaluated as R code in the calling environment at build time, so a BigN value folds inline,label = "Placebo (N={n['placebo']})". Double a brace ({{or}}) for a literal one. Anmd()/html()label is passed through without interpolation.Per-column token.
{.name}(alias{.col}) inside a{expr}is deferred and resolved to the matched column's name when the spec is stamped bycols()/cols_apply(), so one spec can carry a variable-N arm header. Seecols_apply()for the loop-free idiom.- format
Post-cell formatter.
<character(1) | function | NULL>: default NULL. Asprintftemplate applied per cell, OR a unaryfunction(x) -> characterof the same length, ORNULLfor backend default.Restriction: Character templates are probed with
sprintf(format, 0)at construction; malformed templates fail fast. Tip: Use a function for non-sprintfformatting (locale- aware numbers, thousand separators, conditional symbols).- visible
Whether the column renders.
<logical(1)>: default NA.FALSEhides the column from output but keeps it inspec@datasosort_rows()andstyle()predicates can still reference it.NA(default) is the merge "unset" sentinel — it resolves to visible at render and, crucially, is mergeable: a latercols()call withvisible = TRUEcan re-show a column an earlier call hid.Interaction: Hidden columns are the standard pattern for sort-key helpers (
row_type,n_total) and for the numeric counts behind formatted-text percentage cells.Auto-hide. The depth column named by a character
indentand every column named bysubgroup(by = ...)or referenced via a{col}placeholder in the subgroup banner template are flipped tovisible = FALSEautomatically at engine time — restating it here is redundant.Break-only group column. A hidden
usage = "group"column emits no header rows and no in-column text; it contributes only itsgroup_skiptransitions, sogroup_displayis ignored while hidden. This is the canonical "spacer" that drops a blank line wherever a marker value changes (e.g. continuous stats vs. categorical groups inside one characteristic):col_spec(usage = "group", group_skip = TRUE, visible = FALSE).- width
Column width — auto-sized, pinned, or proportional.
<character(1) | numeric(1)>: default "auto"."auto"(default) — engine measures the widest cell (header + body) using bundled Adobe AFM Core 13 glyph metrics and distributes against the available content width. The header is sized to its widest word, so a multi-word header (e.g."n, median") wraps at spaces; a non-breaking space () keeps a run whole. The body is sized to its widest line and never wraps, so numeric values stay intact. Pin a numeric width to wrap the body too.<number>— pinned in inches. Backends wrap content inside the pinned width (tabularrayQ[wd=...], HTMLstyle="width:...", RTF / DOCX after twips conversion)."2.5in"/"60mm"/"4cm"/"30pt"/"5pc"— pinned dimension with an explicit TeX unit. Same behaviour as a bare numeric."30%"— proportional width, percent of available content width. Resolved at engine time against the printable area.
Tip: Mix freely. Pinned and percent widths take priority;
"auto"columns distribute whatever space remains. If pinned widths together exceed the available content width, the engine warns and leaves"auto"columns at their natural fit (layout may overflow).Restriction: Must be positive. Percent values must fall in
[0, 100]. Font-relative units (em,ex,rem) are rejected (no font-size context at parse time).Cross-format semantics (gt convention). The width value is the user's source-of-truth. HTML emits it verbatim into
<col style="width:...">(CSS accepts every unit:%,in,px,pt,cm,mm). Paper backends (LaTeX / RTF / PDF / DOCX) convert to their native unit via the AFM / distribute-widths pipeline. HTML is unconditionally responsive: whenwidth = "auto"(default), the browser auto-sizes the column and cells wrap when the viewport narrows.Note:
NAandNULLare rejected. In pre-v0.1.0 tabularNAdeferred to backend auto-fit; that path was inconsistent across backends and is replaced by the"auto"default, which produces identical widths across RTF / LaTeX / HTML.Merge sentinel. For the field-merge across repeated
cols()/cols_apply()calls,"auto"is treated as the default: a later call carryingwidth = "auto"leaves a previously pinned width intact, and only an explicit non-"auto"width overrides.- group_display
How
usage = "group"values render in the body.<character(1)>: default NA. Active only whenusage = "group"— setting it on a non-group column is ignored and warns.NA(default) is the merge "unset" sentinel and resolves to"header_row"at render; an explicit value is mergeable, so a latercols()call can reset it back to"header_row"."header_row"(default) — each unique value emits as a section header row above its block of data rows, and the body rows beneath are automatically indented one level. The section header itself sits flush left at depth 0; its child rows render one indent level in. Because the section already supplies that indent, the stub column needs noindent— addingindent = 1there overrides (does not stack) the auto-indent, leaving a single level. The source column is hidden from the visible body. Matches the canonical submission shape used by clinical TFL house templates (Disposition, Demographics, Statistical Report sections)."column"— column stays visible; repeated values are suppressed (only the first row of each value shows the label). PROC REPORT's default for grouping variables."column_repeat"— column stays visible; every row repeats the value (no suppression). The shapeR'sprint.data.frameproduces.
Composition under multiple group columns. When more than one
usage = "group"column is declared, the FIRST one encountered incols()order is the outer group; subsequent group columns nest inside it. Each column'sgroup_displaychoice is independent — a common clinical pattern is the outervariableas"header_row"plus the innerstat_labelas"column"(visible row labels under each section header).- group_skip
Insert a blank row between consecutive groups.
<logical(1)>: default NA. Active only whenusage = "group"— setting it on a non-group column is ignored and warns. Three values:TRUE— engine injects one blank row immediately before each value transition on this column (PROC REPORT'sBREAK AFTER var / SKIPsemantics, lifted to per-column control). Never trails the final group.FALSE— never insert a blank row for this column.NA(default) — followgroup_display:TRUEwhengroup_display = "header_row",FALSEwhen"column"or"column_repeat". Picks the canonical shape without an extra knob to set.
Interaction: When two or more columns have an effective
group_skip = TRUEand their value transitions coincide on the same row, the engine emits ONE blank row at that boundary, not one per column. Transition row indices are unioned across all contributing group columns.# 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.
<character(1) | NULL>: default NULL. One of:"left"— character columns; row labels."center"— column-header band; rarely on data cells."right"— numeric content without decimals."decimal"— numeric or mixed-format cells aligned on the decimal mark. Use for"5 (3.2%)"next to"54 (32.1%)".NULL(default) — falls through topreset(alignment = list(body_halign = ...))and then to the baked default"left".
Tip:
"decimal"pads numerics with non-breaking spaces so the decimal mark falls on a single column-wide anchor. Pad counts follow the active preset'sdecimal_metricsknob (seepreset()): the default"afm"measures real glyph widths so the anchor holds in proportional fonts as well as monospace.Default behaviour. When
alignis unset (NULL/NA), every column emits with body left-aligned and header centred, regardless of the column's R data type. tabular's canonical input is pre-summarised wide data frames where numeric content is already formatted as character strings (e.g."52 (60.5)"), sois.numeric()-based auto-detection would mis-classify those columns as text and align them left — the opposite of intent. Use explicitalign = "decimal"for NBSP-padded numeric columns (centred header over the padded centroid) oralign = "right"for plain right-aligned numeric columns. The default cascade is body →preset(alignment = list( body_halign = ...))→ CSStext-align: left; header →preset(alignment = list(header_halign = ...))→ CSStext-align: center.- valign
Vertical alignment within the cell.
<character(1) | NULL>: default NULL. One of"top","middle","bottom".NULLfalls through topreset(alignment = list(body_valign = ...))(baked default"top"). Per-cell overrides viastyle(valign = ...)still win over the column setting.Tip: Set
"middle"on the row-label column of a banded- row table so the label stays centred against the multi-line stat-block in the adjacent cell.- na_text
Text substituted for
NAcells.<character(1) | NA>: default NA. Substituted BEFORE theformatstep, soformatdoes not need to anticipateNA.NA(default) inherits the preset's table-widena_text; any string overrides it for this column, including""to force blank cells even when the preset uses a non-empty token.Tip: Use a sentinel (
"-","NR",".") when blank cells would be ambiguous, e.g. when "not applicable" and "not reported" both render blank.- indent
Cosmetic indent depth on this column.
<numeric(1) | character(1) | NA>: default NA. Two modes by type:A non-negative whole number — every body row of this column is indented that many levels (each level is
preset@indent_sizespace-widths).indent = 1is the common "nudge this stub in one level" case;indent = 0is a real value that flattens children under a"header_row"section.A column name (character) — per-row depth: the engine reads
spec@data[[indent]], coerces each row to a non-negative integer, and prefixes that row's text + AST withstrrep(" ", preset@indent_size * depth). The referenced depth column is auto-hidden — no need to setvisible = FALSEon it.
NA(default) means no indent. Backends with native padding-left (HTML / LaTeX / RTF / DOCX / PDF) emit the depth as cell padding so wrapped continuation lines align with the indented baseline; Markdown carries the literal space-prefix. Synthesised group-header rows are never indented — they are the parent at depth 0.Interaction: an explicit
indenton agroup_display = "header_row"host suppresses that section's automatic one-level child indent (you take control of the depth) — so a stub under a section needs noindentat all, and addingindent = 1there yields a single, not double, indent.Per-row SOC / PT pattern (the bundled
cdisc_saf_aesocptships the canonical depth column, so no upstream construction is needed):cols( label = col_spec(label = "Category", indent = "indent_level"), soc = col_spec(visible = FALSE), row_type = col_spec(visible = FALSE) )Depth-column values
c(0L, 1L, 2L, …)produce0,1,2, … levels. Negative values clamp to 0 (warn); fractional numerics floor (warn); NA → 0 (silent). Works in flat listings too — a characterindentdoes not require anyusage = "group"columns.
Value
A col_spec S7 object. Pass it to cols() keyed by
the input column name; the constructor itself does not stamp
a name.
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: any field set to a non-default value on the new spec
overrides; a field left at its "unset" sentinel (NA / NULL /
"auto") leaves the existing value intact. Because every mergeable
field has a genuine unset sentinel, a later call can also restore a
default — e.g. visible = TRUE re-shows a column an earlier call
hid. 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.
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"))
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: AE table with indented label + hidden helpers ----
#
# AE-by-SOC/PT table where `label` carries both the SOC and the PT
# text in one column, each PT indented one level under its parent
# SOC via `indent_level`. The hidden numeric helpers `soc_n` (the
# parent SOC's count, broadcast across its PT children) and
# `n_total` (each row's own count) drive the sort: ordering by
# `soc_n` descending keeps every SOC cluster together, and the
# `n_total` descending tiebreak floats the SOC summary row above
# its PTs, so the table reads SOC then its PTs, next SOC then its
# PTs. Demonstrates `indent` plus `visible = FALSE` for sort-only
# columns, fixed width on the wide label column, and decimal
# alignment on all four arm columns.
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 / Preferred Term",
indent = "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("soc_n", "n_total"), descending = c(TRUE, TRUE))
Table 14.3.1
Adverse Events by SOC and Preferred Term
Safety Population
SOC / Preferred Term Placebo
N=86 Drug 50
N=96 Drug 100
N=72 Total
N=254 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 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"
)
)
Treatment-effect estimates by model
Estimate Lower
95% CI Upper
95% CI p-value ANCOVA -2.31 -3.42 -1.20 0.0042 MMRM -2.45 -- -- 0.0061 Cox PH 0.81 0.68 0.97 0.0087 Bootstrap (1000 reps) -2.29 -3.50 -1.10 0.0050
# ---- 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"
)
)
Vital Signs at Baseline and End of Treatment
Statistic Placebo
N=86 Drug 50
N=96 Drug 100
N=72 Diastolic Blood Pressure (mmHg) Baseline n 340 384 288 Mean (SD) 77.1 (10.7) 76.6 (9.8) 78.2 (10.3) Median 77.7 76.7 78.8 Min, Max 40 , 110 48 , 108 51 , 108 Week 8 n 292 240 224 Mean (SD) 75.2 (9.1) 75.4 (10.6) 77.4 (9.1) Median 76.0 74.0 78.3 Min, Max 49 , 101 52 , 100 54 , 98 Week 16 n 272 168 148 Mean (SD) 75.1 (10.9) 75.2 (10.0) 76.0 (9.0) Median 76.0 75.7 77.3 Min, Max 49 , 98 55 , 98 50 , 92 End of Treatment n 222 177 168 Mean (SD) 74.4 (10.7) 76.0 (11.2) 76.0 (9.9) Median 73.5 76.0 78.0 Min, Max 49 , 104 50 , 100 56 , 98 Pulse Rate (beats/min) Baseline n 340 384 288 Mean (SD) 73.5 (11.6) 72.1 (10.8) 72.4 (9.7) Median 72.3 70.0 71.7 Min, Max 51 , 134 50 , 104 52 , 100 Week 8 n 292 240 224 Mean (SD) 71.8 (9.0) 72.6 (11.1) 74.0 (8.9) Median 72.0 72.0 73.2 Min, Max 52 , 102 49 , 104 50 , 104 Week 16 n 272 168 148 Mean (SD) 70.6 (8.8) 68.8 (9.4) 73.2 (9.5) Median 70.2 68.0 72.0 Min, Max 50 , 90 48 , 104 51 , 96 End of Treatment n 222 177 168 Mean (SD) 75.2 (11.5) 74.1 (9.4) 73.6 (9.6) Median 74.0 75.0 73.0 Min, Max 51 , 106 50 , 94 50 , 98 Systolic Blood Pressure (mmHg) Baseline n 340 384 288 Mean (SD) 136.8 (17.6) 137.9 (18.5) 137.8 (17.2) Median 136.3 138.0 138.0 Min, Max 80 , 184 100 , 194 100 , 192 Week 8 n 292 240 224 Mean (SD) 136.3 (17.0) 134.9 (17.8) 135.1 (15.5) Median 136.5 132.3 134.0 Min, Max 90 , 189 92 , 200 91 , 198 Week 16 n 272 168 148 Mean (SD) 134.6 (18.3) 132.5 (14.3) 133.7 (16.0) Median 134.0 130.0 132.0 Min, Max 76 , 190 100 , 168 99 , 186 End of Treatment n 222 177 168 Mean (SD) 132.7 (15.4) 133.0 (17.1) 132.3 (15.6) Median 131.0 130.0 131.0 Min, Max 78 , 172 92 , 178 100 , 177 Temperature (C) Baseline n 172 190 144 Mean (SD) 36.6 (0.4) 36.5 (0.4) 36.6 (0.4) Median 36.7 36.6 36.6 Min, Max 35 , 37 35 , 37 36 , 37 Week 8 n 146 118 112 Mean (SD) 36.6 (0.4) 36.6 (0.4) 36.6 (0.4) Median 36.6 36.7 36.7 Min, Max 36 , 37 36 , 37 36 , 37 Week 16 n 136 82 74 Mean (SD) 36.7 (0.3) 36.6 (0.4) 36.6 (0.4) Median 36.7 36.6 36.7 Min, Max 36 , 37 36 , 37 36 , 37 End of Treatment n 74 59 56 Mean (SD) 36.7 (0.4) 36.6 (0.4) 36.6 (0.4) Median 36.8 36.7 36.7 Min, Max 35 , 37 35 , 38 36 , 37
# ---- 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)
Table 14.1.1
Demographics
Safety Population
Panel 1 Panel 2 Parameter Statistic Placebo
N=86 Drug 50
N=96 Drug 100
N=72 Total
N=254 Age (years) n 86 96 72 254 Mean (SD) 75.2 ( 8.59) 76.0 ( 8.11) 73.8 ( 7.94) 75.1 ( 8.25) Median 76.0 78.0 75.5 77.0 Q1, Q3 69.2, 81.8 71.0, 82.0 70.5, 79.0 70.0, 81.0 Min, Max 52 , 89 51 , 88 56 , 88 51 , 89 Sex, n (%) F 53 (61.6 ) 55 (57.3 ) 35 (48.6 ) 143 (56.3 ) M 33 (38.4 ) 41 (42.7 ) 37 (51.4 ) 111 (43.7 ) Race, n (%) WHITE 78 (90.7 ) 90 (93.8 ) 62 (86.1 ) 230 (90.6 ) BLACK OR AFRICAN AMERICAN 8 ( 9.3 ) 6 ( 6.2 ) 9 (12.5 ) 23 ( 9.1 ) ASIAN 0 0 0 0 AMERICAN INDIAN OR ALASKA NATIVE 0 0 1 ( 1.4 ) 1 ( 0.4 )