Skip to contents

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 in cols()) — pass-through.

  • "group" — row-label with repeat-suppression and continuation-page repeat keys. Use for variable, soc, stat_label. (Cosmetic indent depth is the separate indent argument, 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 REPORT ID role, 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. NA is mergeable, so an explicit "display" on a later cols() 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 \n for 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. An md() / 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 by cols() / cols_apply(), so one spec can carry a variable-N arm header. See cols_apply() for the loop-free idiom.

# 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. <character(1) | function | NULL>: default NULL. A sprintf template applied per cell, OR a unary function(x) -> character of the same length, OR NULL for backend default.

Restriction: Character templates are probed with sprintf(format, 0) at construction; malformed templates fail fast. Tip: Use a function for non-sprintf formatting (locale- aware numbers, thousand separators, conditional symbols).

# 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. <logical(1)>: default NA. FALSE hides the column from output but keeps it in spec@data so sort_rows() and style() predicates can still reference it. NA (default) is the merge "unset" sentinel — it resolves to visible at render and, crucially, is mergeable: a later cols() call with visible = TRUE can 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 indent and every column named by subgroup(by = ...) or referenced via a {col} placeholder in the subgroup banner template are flipped to visible = FALSE automatically 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 its group_skip transitions, so group_display is 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 (tabularray Q[wd=...], HTML style="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: when width = "auto" (default), the browser auto-sizes the column and cells wrap when the viewport narrows.

Note: NA and NULL are rejected. In pre-v0.1.0 tabular NA deferred 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 carrying width = "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 when usage = "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 later cols() 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 no indent — adding indent = 1 there 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 shape R's print.data.frame produces.

Composition under multiple group columns. When more than one usage = "group" column is declared, the FIRST one encountered in cols() order is the outer group; subsequent group columns nest inside it. Each column's group_display choice is independent — a common clinical pattern is the outer variable as "header_row" plus the inner stat_label as "column" (visible row labels under each section header).

# 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. <logical(1)>: default NA. Active only when usage = "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's BREAK AFTER var / SKIP semantics, lifted to per-column control). Never trails the final group.

  • FALSE — never insert a blank row for this column.

  • NA (default) — follow group_display: TRUE when group_display = "header_row", FALSE when "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 = TRUE and 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 to preset(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's decimal_metrics knob (see preset()): the default "afm" measures real glyph widths so the anchor holds in proportional fonts as well as monospace.

Default behaviour. When align is 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)"), so is.numeric()-based auto-detection would mis-classify those columns as text and align them left — the opposite of intent. Use explicit align = "decimal" for NBSP-padded numeric columns (centred header over the padded centroid) or align = "right" for plain right-aligned numeric columns. The default cascade is body → preset(alignment = list( body_halign = ...)) → CSS text-align: left; header → preset(alignment = list(header_halign = ...)) → CSS text-align: center.

valign

Vertical alignment within the cell. <character(1) | NULL>: default NULL. One of "top", "middle", "bottom". NULL falls through to preset(alignment = list(body_valign = ...)) (baked default "top"). Per-cell overrides via style(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 NA cells. <character(1) | NA>: default NA. Substituted BEFORE the format step, so format does not need to anticipate NA. NA (default) inherits the preset's table-wide na_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_size space-widths). indent = 1 is the common "nudge this stub in one level" case; indent = 0 is 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 with strrep(" ", preset@indent_size * depth). The referenced depth column is auto-hidden — no need to set visible = FALSE on 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 indent on a group_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 no indent at all, and adding indent = 1 there yields a single, not double, indent.

Per-row SOC / PT pattern (the bundled cdisc_saf_aesocpt ships 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, …) produce 0, 1, 2, … levels. Negative values clamp to 0 (warn); fractional numerics floor (warn); NA → 0 (silent). Works in flat listings too — a character indent does not require any usage = "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.

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"))

 

Table 14.1.1

Demographics and Baseline Characteristics

Safety Population

 

StatisticPlacebo
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)
Median76.0       78.0       75.5        77.0       
Min, Max52  , 89   51  , 88   56  , 88    51  , 89   
Q1, Q369.2, 81.8 71.0, 82.0 70.5, 79.0  70.0, 81.0 
n86         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)  
WHITE78 (90.7)  90 (93.8)  62 (86.1)  230 (90.6)  
 
Sex, n (%)
F53 (61.6)  55 (57.3)  35 (48.6)  143 (56.3)  
M33 (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 TermPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Total
N=254
TOTAL SUBJECTS WITH AN EVENT52 (60.5)81 (84.4)66 (91.7)199 (78.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS19 (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 CONDITIONS15 (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 DISORDERS13 (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 INFESTATIONS12 (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

 

EstimateLower
95% CI
Upper
95% CI
p-value
ANCOVA
-2.31-3.42-1.200.0042
 
MMRM
-2.45--   --   0.0061
 
Cox PH
 0.81 0.68 0.970.0087
 
Bootstrap (1000 reps)
-2.29-3.50-1.100.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

 

StatisticPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Diastolic Blood Pressure (mmHg)
Baseline
n340         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
n292         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
n272         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
n222         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
n340         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
n292         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
n272         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
n222         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
n340         384         288         
Mean (SD)136.8 (17.6)137.9 (18.5)137.8 (17.2)
Median136.3       138.0       138.0       
Min, Max 80  , 184  100  , 194  100  , 192  
 
Week 8
n292         240         224         
Mean (SD)136.3 (17.0)134.9 (17.8)135.1 (15.5)
Median136.5       132.3       134.0       
Min, Max 90  , 189   92  , 200   91  , 198  
 
Week 16
n272         168         148         
Mean (SD)134.6 (18.3)132.5 (14.3)133.7 (16.0)
Median134.0       130.0       132.0       
Min, Max 76  , 190  100  , 168   99  , 186  
 
End of Treatment
n222         177         168         
Mean (SD)132.7 (15.4)133.0 (17.1)132.3 (15.6)
Median131.0       130.0       131.0       
Min, Max 78  , 172   92  , 178  100  , 177  
 
Temperature (C)
Baseline
n172         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
n146         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
n136          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 1Panel 2
ParameterStatisticPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Total
N=254
Age (years)n86          96          72          254          
Mean (SD)75.2 ( 8.59)76.0 ( 8.11)73.8 ( 7.94) 75.1 ( 8.25)
Median76.0        78.0        75.5         77.0        
Q1, Q369.2, 81.8  71.0, 82.0  70.5, 79.0   70.0, 81.0  
Min, Max52  , 89    51  , 88    56  , 88     51  , 89    
Sex, n (%)F53   (61.6 )55   (57.3 )35   (48.6 )143   (56.3 )
M33   (38.4 )41   (42.7 )37   (51.4 )111   (43.7 )
Race, n (%)WHITE78   (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 )