Controls how data rows are grouped, paginated, indented, and spaced.
Calling fr_rows() again merges with the previous row configuration:
only the arguments you explicitly supply are changed.
arframe uses a two-level greedy pagination algorithm (identical for RTF and PDF output):
page_byforces a page break whenever the named column(s) change value.group_bykeeps rows with the same value in the named column(s) on the same page via\trkeep(RTF). If a group is too large for a single page, it breaks normally and a "(continued)" header row repeats at the top of the next page.
Usage
fr_rows(
spec,
page_by = NULL,
group_by = NULL,
indent_by = NULL,
blank_after = NULL,
page_by_bold = FALSE,
page_by_align = "left",
page_by_visible = TRUE,
group_label = NULL,
group_keep = TRUE,
sort_by = NULL,
repeat_cols = NULL,
wrap = FALSE
)Arguments
- spec
An
fr_specobject fromfr_table().- page_by
Character vector of column name(s). A new page begins whenever any of these columns change value. The column value is rendered as a group label above the column headers, and the column is automatically hidden from the table body at render time. Use for multi-SOC tables that break by System Organ Class.
- group_by
Character vector of column name(s). Rows sharing the same value are visually grouped (indented detail rows, blank-after spacing) and kept together on the same page when possible. When a group spans pages, a "(continued)" header row repeats at the top of the next page.
- indent_by
Row indentation specification. Accepts two forms:
Simple (single level): A character vector of column name(s). All detail rows (non-header, non-blank) in these columns receive one indent level. Typically used with
group_byfor SOC/PT tables:indent_by = "pt".Multi-level (SAS-style): A named list with three elements:
key— column name containing row type markers (e.g.,"row_type")col— column name(s) to apply indent to (e.g.,"term")levels— named numeric vector mapping key values to indent multipliers (e.g.,c(soc = 0, hlt = 1, pt = 2))
Each indent level = 2 space-character widths (~0.17 in at 9pt). Rows whose key value is not in
levelsreceive no indent.- blank_after
Character vector of column name(s). A blank row is inserted after each group boundary in these columns.
- page_by_bold
Logical. Whether the page-by label is rendered in bold. Default
FALSE. SetTRUEto make the label stand out above the column headers.- page_by_align
Horizontal alignment for the page-by label. One of
"left"(default),"center","right", or"decimal".- page_by_visible
Logical. Whether the page-by group label is displayed above the column headers. Default
TRUE. SetFALSEto get page breaks at group boundaries without a visible label — useful when the grouping column already appears in the table body.- group_label
Character scalar. Column name into which group header values are injected. When
group_byandgroup_labelare both set, a header row is inserted at the start of each group: the group value appears in thegroup_labelcolumn, all other columns are empty, and detail rows are indented underneath. Style the header rows viafr_styles()(e.g., bold). Requiresgroup_by.- group_keep
Logical. Whether
group_bygroups are kept together on the same page via RTF\keepn/ LaTeX keep-with-next. DefaultTRUE. SetFALSEfor visual-only grouping (blank_after, indent) without page-keeping — useful for long groups where you want the renderer to break freely.- sort_by
Character vector of column name(s). Sorts the data by these columns before rendering. For listings, this controls the display order (e.g.,
sort_by = c("USUBJID", "ASTDT")). Sorting is applied infinalize_spec().- repeat_cols
Character vector of column name(s). Suppresses repeated consecutive values in these columns — only the first occurrence in each run is displayed. Standard for listings where subject ID appears once per block. Suppression is applied in
finalize_spec().- wrap
Logical. When
TRUE, enables text wrapping in body cells. DefaultFALSE. For listings with long text fields (e.g., verbatim terms, medical history), wrapping prevents cell content from overflowing.
Regulatory conventions — AE tables
Standard pharma AE tables (MedDRA SOC/PT hierarchy) follow these row conventions:
Sort order: SOC rows sorted by descending incidence across all arms; within each SOC, PT rows sorted by descending incidence (alphabetical as tiebreaker). This is the standard regulatory sort order.
"TOTAL SUBJECTS WITH AN EVENT" is always the first summary row, above the first SOC. Pre-compute this in your data frame.
Subject counting: a subject is counted once per SOC and once per PT, even with multiple events. Document this in
fr_footnotes().Page breaks by SOC: each System Organ Class starts a new page (
page_by = "soc"). PT rows stay with their SOC header viagroup_by.Indentation: PT rows are indented under the SOC header (
indent_by = "pt_label"). Indent: 0.1667 in (2 spaces / 240 twips).
Regulatory conventions — descriptive statistics tables
For demographics and continuous endpoint tables, the standard summary
statistic row order is:
n, Mean, SD (or SE), Median, Q1/Q3, Min/Max
Pre-sort your long-form data to match this order. Use blank_after to
insert visual separation between characteristic blocks (e.g. between Age
and Sex sections).
Orphan and widow control
The pagination engine respects the orphan_min and widow_min settings
in fr_page(). Orphan control prevents a lone row being stranded at the
bottom of a page; widow control prevents a lone row starting a new page.
These apply within group_by groups: if placing a group would leave
fewer than orphan_min rows before the page break, the group is moved to
the next page instead.
Tips
page_bycolumns are automatically hidden at render time — their values appear as group labels in the section header, so they don't need to appear in the table body. Override withfr_cols(col = fr_col(visible = TRUE))if needed (set afterfr_rows()).group_by,indent_by, etc. columns are structural — hide them viafr_cols(col = fr_col(visible = FALSE))if they shouldn't appear.Use
group_by(notpage_by) when you want rows to travel together but not necessarily start on a new page.blank_afteris the simplest way to add visual group separation without bold headers.
Examples
## ── AE table paginated by System Organ Class ─────────────────────────────
tbl_ae_soc |>
fr_table() |>
fr_rows(page_by = "soc")
#>
#> ── fr_spec: Table
#> Data: 96 rows x 7 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
#> Rows: page_by=soc
## ── Demographics table: blank row after each group ────────────────────────
tbl_demog |>
fr_table() |>
fr_rows(blank_after = "group")
#>
#> ── fr_spec: Table
#> Data: 28 rows x 6 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
## ── Indented hierarchy (SOC bold header + indented PT rows) ──────────────
tbl_ae_soc |>
fr_table() |>
fr_rows(
group_by = "soc",
indent_by = "pt"
)
#>
#> ── fr_spec: Table
#> Data: 96 rows x 7 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
#> Rows: group_by=soc, indent_by=pt
## ── Combined: page_by + group_by + indent ──────────────────────────────
tbl_ae_soc |>
fr_table() |>
fr_cols(soc = fr_col(visible = FALSE),
row_type = fr_col(visible = FALSE)) |>
fr_rows(
page_by = "soc",
group_by = "soc",
indent_by = "pt"
)
#> Warning: `page_by` and `group_by` share column: "soc".
#> ℹ `group_by` grouping is applied within each `page_by` page.
#>
#> ── fr_spec: Table
#> Data: 96 rows x 7 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (5 visible of 7):
#> pt "pt" 4.05in left
#> placebo "placebo" 0.82in left
#> zom_50mg "zom_50mg" 0.82in left
#> zom_100mg "zom_100mg" 0.90in left
#> total "total" 0.90in left
#> Header: valign=bottom
#> Rows: page_by=soc, group_by=soc, indent_by=pt
## ── Multi-level indent (SOC / HLT / PT hierarchy) ─────────────────────
## Uses a named list: key column determines indent level per row
# Given data with columns: soc, term, row_type, placebo, ...
# where row_type is "soc", "hlt", or "pt"
spec <- data.frame(
soc = c("GI disorders", "GI disorders", "GI disorders"),
term = c("GI disorders", "GI signs", "Nausea"),
row_type = c("soc", "hlt", "pt"),
result = c("72 (53.3)", "54 (40.0)", "24 (17.8)"),
stringsAsFactors = FALSE
) |>
fr_table() |>
fr_cols(soc = fr_col(visible = FALSE),
row_type = fr_col(visible = FALSE)) |>
fr_rows(
group_by = "soc",
indent_by = list(
key = "row_type",
col = "term",
levels = c(soc = 0, hlt = 1, pt = 2)
)
)
## ── group_label: auto-inject group headers into display column ──────
## When group and display data are in separate columns
data.frame(
group = c("Sex", "Sex", "Age", "Age", "Age"),
stat = c("Female", "Male", "Mean (SD)", "Median", "Min, Max"),
value = c("27 (60.0)", "18 (40.0)", "75.0 (6.8)", "74.0", "65, 88"),
stringsAsFactors = FALSE
) |>
fr_table() |>
fr_cols(group = fr_col(visible = FALSE)) |>
fr_rows(
group_by = "group",
group_label = "stat",
indent_by = "stat"
)
#>
#> ── fr_spec: Table
#> Data: 5 rows x 3 columns
#> Page: landscape letter, 9pt Courier New
#> Columns (2 visible of 3):
#> stat "stat" 0.82in left
#> value "value" 0.90in left
#> Header: valign=bottom
#> Rows: group_by=group, indent_by=stat
## ── sort_by: order a listing by subject and start date ────────────────
adae[1:20, c("USUBJID", "AEBODSYS", "AEDECOD", "ASTDT", "AESEV")] |>
fr_table() |>
fr_rows(sort_by = c("USUBJID", "ASTDT"))
#>
#> ── fr_spec: Table
#> Data: 20 rows x 5 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
#> Rows: sort_by=USUBJID,ASTDT
## ── repeat_cols: suppress repeated subject IDs in a listing ───────────
adae[1:20, c("USUBJID", "AEBODSYS", "AEDECOD", "AESEV")] |>
fr_table() |>
fr_rows(
sort_by = c("USUBJID", "AEBODSYS"),
repeat_cols = "USUBJID"
)
#>
#> ── fr_spec: Table
#> Data: 20 rows x 4 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
#> Rows: sort_by=USUBJID,AEBODSYS
## ── wrap = TRUE: enable text wrapping for long verbatim terms ─────────
adae[1:10, c("USUBJID", "AEBODSYS", "AEDECOD", "AEOUT")] |>
fr_table() |>
fr_rows(wrap = TRUE)
#>
#> ── fr_spec: Table
#> Data: 10 rows x 4 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
#> Rows: wrap
## ── Combined: sort_by + repeat_cols on adverse event listing ──────────
adae[1:30, c("USUBJID", "AEBODSYS", "AEDECOD", "AESEV", "ASTDT")] |>
fr_table() |>
fr_rows(
sort_by = c("USUBJID", "AEBODSYS", "AEDECOD"),
repeat_cols = c("USUBJID", "AEBODSYS"),
wrap = TRUE
)
#>
#> ── fr_spec: Table
#> Data: 30 rows x 5 columns
#> Page: landscape letter, 9pt Courier New
#> Header: valign=bottom
#> Rows: sort_by=USUBJID,AEBODSYS,AEDECOD, wrap