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,
group_keep = TRUE,
group_style = NULL,
sort_by = NULL,
suppress = NULL,
wrap = FALSE
)Arguments
- spec
An
fr_specobject fromfr_table().- page_by
Page break specification. Accepts two forms:
Simple (character): 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.
List form: A named list with elements:
cols— character vector of column name(s) (required)visible— logical; whether the page-by label is displayed above the column headers (defaultTRUE). SetFALSEto get page breaks at group boundaries without a visible label.
Style page-by labels (bold, alignment, colour) via
fr_styles()withrows = "page_by", or set defaults viafr_theme().- group_by
Group specification. Accepts two forms:
Simple (character): Character vector of column name(s). Rows sharing the same value are visually grouped (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.
List form: A named list with elements:
cols— character vector of column name(s) (required). When multiple columns are supplied, groups are defined by the combination of all column values (e.g.,cols = c("PARAMCD", "direction")groups by each unique PARAMCD + direction pair).label— character scalar; column name into which group header values are injected. A header row is inserted at the start of each group, containing the value from the firstcolscolumn. Whenlabelis set andindent_byis not,indent_byis automatically inferred fromlabel, indenting all detail rows under the bold group header.leaf— character scalar; the lowest-level column in a multi-level hierarchy. Must be one of thecolsvalues. When set, arframe collapses all hierarchy columns into a single__display__column and adds a__row_level__column containing the source column name for each row (e.g.,"soc"or"pt"). Use__row_level__withfr_rows_matches()to style specific levels independently — for example, bold only the SOC header rows without affecting PT rows. Indentation is auto-set from the hierarchy depth. Source columns are auto-hidden.
group_bydoes not affect decimal alignment — values align globally across the entire column regardless of group boundaries. Onlypage_bycreates separate alignment contexts (since pages are physically separate).Style group header rows (bold, etc.) via
fr_styles()by targeting group header rows withfr_rows_matches(). For single-levellabelgroups, match on an empty stat column (e.g.,fr_rows_matches("total", "")). For multi-levelleafhierarchies, match on the__row_level__column (e.g.,fr_rows_matches("__row_level__", "soc")).- 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.
- 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.- group_style
Style properties for group header rows (injected by
group_bywithlabelorleaf). Accepts two forms:Uniform style (all group headers): A named list of style properties:
bold,italic,underline,color,background,font_size,align. Example:group_style = list(bold = TRUE, background = "#F0F0F0").Per-level style (leaf hierarchies only): A named list where names match hierarchy column names and values are style property lists. Example:
group_style = list(soc = list(bold = TRUE), hlt = list(italic = TRUE)). Levels not listed receive no special styling.group_stylehas the lowest precedence — explicitfr_styles()calls override it. Set study-wide defaults viafr_theme()or_arframe.yml.Alternatively, use
rows = "group_headers"infr_row_style()orfr_style()for full styling control viafr_styles().- 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().- suppress
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.Style page-by labels and group header rows via
fr_styles()for full control over bold, alignment, and other formatting.
Parameter Precedence
Settings resolve from four tiers (lowest to highest priority):
package defaults < _arframe.yml < fr_theme() < this function.
Only parameters you explicitly supply override previous tiers.
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 Times New Roman
#> 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 Times New Roman
#> 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 Times New Roman
#> 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 Times New Roman
#> Columns (5 visible of 7):
#> pt "pt" 2.67in left
#> placebo "placebo" 0.52in left
#> zom_50mg "zom_50mg" 0.64in left
#> zom_100mg "zom_100mg" 0.70in left
#> total "total" 0.58in left
#> Header: valign=bottom
#> Rows: page_by=soc, group_by=soc, indent_by=pt
## ── page_by with hidden label (page breaks only, no visible label) ───────
tbl_ae_soc |>
fr_table() |>
fr_rows(page_by = list(cols = "soc", visible = FALSE))
#>
#> ── fr_spec: Table
#> Data: 96 rows x 7 columns
#> Page: landscape letter, 9pt Times New Roman
#> Header: valign=bottom
#> Rows: page_by=soc (visible=FALSE)
## ── 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)
)
)
## ── Multi-level hierarchy with leaf + bold SOC only ─────────────────
data.frame(
soc = c("GI disorders", "GI disorders", "GI disorders",
"Nervous system", "Nervous system"),
pt = c("Nausea", "Vomiting", "Diarrhoea",
"Headache", "Dizziness"),
total = c("24 (17.8)", "18 (13.3)", "12 ( 8.9)",
"30 (22.2)", "15 (11.1)"),
stringsAsFactors = FALSE
) |>
fr_table() |>
fr_rows(
group_by = list(cols = c("soc", "pt"), leaf = "pt"),
blank_after = "soc",
group_style = list(soc = list(bold = TRUE))
)
#>
#> ── fr_spec: Table
#> Data: 7 rows x 5 columns
#> Page: landscape letter, 9pt Times New Roman
#> Header: valign=bottom
#> Rows: group_by=soc, indent_by=__row_level__,__display__,c(soc = 0, pt = 1)
## ── group_by list form: auto-inject group headers ────────────────────
## label injects header rows from group_by values into the named column.
## indent_by is auto-inferred — no need to specify both.
##
## Result:
## stat value
## Sex <- header row (from group column)
## Female 27 (60.0) <- indented detail
## Male 18 (40.0) <- indented detail
##
## Age <- header row
## Mean (SD) 75.0 (6.8) <- indented detail
## Median 74.0
## Min, Max 65, 88
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 = list(cols = "group", label = "stat")
)
#>
#> ── fr_spec: Table
#> Data: 5 rows x 3 columns
#> Page: landscape letter, 9pt Times New Roman
#> Columns (2 visible of 3):
#> stat "stat" 0.62in left
#> value "value" 0.55in left
#> Header: valign=bottom
#> Rows: group_by=group (label=stat), indent_by=stat
## ── group_by with label + bold group headers ──────────────────────
data.frame(
PARAMCD = c("ALB", "ALB", "ALT", "ALT"),
stat_label = c("High", "Low", "High", "Low"),
total = c("6 ( 2.4)", "35 (13.8)", "30 (11.8)", "6 ( 2.4)"),
placebo = c("4 ( 4.7)", "17 (19.8)", "9 (10.5)", "1 ( 1.2)"),
stringsAsFactors = FALSE
) |>
fr_table() |>
fr_cols(PARAMCD = fr_col(visible = FALSE)) |>
fr_rows(
group_by = list(cols = "PARAMCD", label = "stat_label"),
blank_after = "PARAMCD",
group_style = list(bold = TRUE)
)
#>
#> ── fr_spec: Table
#> Data: 4 rows x 4 columns
#> Page: landscape letter, 9pt Times New Roman
#> Columns (3 visible of 4):
#> stat_label "stat_label" 0.58in left
#> total "total" 0.52in left
#> placebo "placebo" 0.52in left
#> Header: valign=bottom
#> Rows: group_by=PARAMCD (label=stat_label), indent_by=stat_label
## ── Per-level styling (leaf hierarchies) ─────────────────────────────
data.frame(
soc = c("GI disorders", "GI disorders",
"Nervous system", "Nervous system"),
pt = c("Nausea", "Vomiting", "Headache", "Dizziness"),
total = c("24 (17.8)", "18 (13.3)", "30 (22.2)", "15 (11.1)"),
stringsAsFactors = FALSE
) |>
fr_table() |>
fr_rows(
group_by = list(cols = c("soc", "pt"), leaf = "pt"),
blank_after = "soc",
group_style = list(soc = list(bold = TRUE, background = "#F0F0F0"))
)
#>
#> ── fr_spec: Table
#> Data: 6 rows x 5 columns
#> Page: landscape letter, 9pt Times New Roman
#> Header: valign=bottom
#> Rows: group_by=soc, indent_by=__row_level__,__display__,c(soc = 0, pt = 1)
## ── 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 Times New Roman
#> Header: valign=bottom
#> Rows: sort_by=USUBJID,ASTDT
## ── suppress: suppress repeated subject IDs in a listing ───────────
adae[1:20, c("USUBJID", "AEBODSYS", "AEDECOD", "AESEV")] |>
fr_table() |>
fr_rows(
sort_by = c("USUBJID", "AEBODSYS"),
suppress = "USUBJID"
)
#>
#> ── fr_spec: Table
#> Data: 20 rows x 4 columns
#> Page: landscape letter, 9pt Times New Roman
#> 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 Times New Roman
#> Header: valign=bottom
#> Rows: wrap
## ── Combined: sort_by + suppress on adverse event listing ──────────
adae[1:30, c("USUBJID", "AEBODSYS", "AEDECOD", "AESEV", "ASTDT")] |>
fr_table() |>
fr_rows(
sort_by = c("USUBJID", "AEBODSYS", "AEDECOD"),
suppress = c("USUBJID", "AEBODSYS"),
wrap = TRUE
)
#>
#> ── fr_spec: Table
#> Data: 30 rows x 5 columns
#> Page: landscape letter, 9pt Times New Roman
#> Header: valign=bottom
#> Rows: sort_by=USUBJID,AEBODSYS,AEDECOD, wrap