diff --git a/vignettes/v02-tfl_table_intro.Rmd b/vignettes/v02-tfl_table_intro.Rmd
index 3d5c66f..50a2407 100644
--- a/vignettes/v02-tfl_table_intro.Rmd
+++ b/vignettes/v02-tfl_table_intro.Rmd
@@ -14,10 +14,10 @@ knitr::opts_chunk$set(
)
```
-`writetfl` can render data frames as paginated, publication-quality tables inside
-multi-page PDFs. The two key functions are `tfl_table()`, which builds a table
-configuration object from a data frame, and `export_tfl()`, which renders it to a
-PDF file.
+`writetfl` can render data frames as paginated, publication-quality tables
+inside multi-page PDFs. The two key functions are `tfl_table()`, which builds
+a table configuration object from a data frame, and `export_tfl()`, which
+renders it to a PDF file.
If you are working with `gt` tables, see `vignette("v05-gt_tables")` for
direct gt integration via `export_tfl()`.
@@ -47,16 +47,18 @@ export_tfl(
)
```
-`tfl_table()` returns a table configuration object. It does not draw anything or
-open any device. All drawing happens inside `export_tfl()`.
+`tfl_table()` returns a table configuration object. It does not draw anything
+or open any device. All drawing happens inside `export_tfl()`.
---
-## Column labels
+## Columns
+
+### Labels
By default, column names are used as column headers. Supply `col_labels` to
-override them — either as a named character vector (match by column name) or as a
-positional vector the same length as `cols`.
+override them — either as a named character vector (match by column name) or
+as a positional vector the same length as `cols`.
```{r col-labels, fig.width = 11, fig.height = 8.5, out.width = "100%"}
# Subset columns and rename them for the report
@@ -80,11 +82,10 @@ export_tfl(
Embedding `\n` in a label creates a multi-line column header. The header row
height is sized automatically to fit the tallest label.
----
-
-## Column widths
+### Widths
-Three width modes are available and can be mixed freely within the same table.
+Three width modes are available and can be mixed freely within the same
+table.
| Mode | How to specify | Effect |
|------|----------------|--------|
@@ -107,12 +108,12 @@ tbl <- tfl_table(
export_tfl(tbl, file = "column_widths.pdf")
```
-When relative widths are used, they are scaled proportionally among themselves
-after all fixed and auto columns have claimed their space.
-
----
+When relative widths are used, they are scaled proportionally among
+themselves after all fixed and auto columns have claimed their space.
+`min_col_width` (default `unit(0.5, "inches")`) is a floor applied to
+auto-sized columns.
-## Column alignment
+### Alignment
Numeric columns default to right-aligned; character columns default to
left-aligned. Override per column with `col_align`.
@@ -149,14 +150,105 @@ export_tfl(
Valid alignment values are `"left"`, `"right"`, and `"center"`.
+### Word-wrapping
+
+`wrap_cols` accepts column names (or `TRUE`/`FALSE`) marking columns as
+eligible for greedy word-wrapping. Wrap-eligible columns are sized to fit
+within their assigned width, with cell text wrapped on word boundaries.
+This is useful for free-text columns (narrative descriptions, verbatim
+terms) that would otherwise force very wide pages or illegible small
+fonts.
+
+```{r wrap-cols, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+ae_verbatim <- data.frame(
+ subject_id = c("001-001", "001-002", "001-003", "002-001", "002-002"),
+ ae_term = c(
+ "Nausea and vomiting, mild, considered possibly related to study treatment",
+ "Headache, moderate, considered unlikely related",
+ "Fatigue, mild, relationship to study drug uncertain",
+ "Abdominal pain, moderate, considered probably related",
+ "Dizziness, mild, considered possibly related"
+ ),
+ onset_day = c(3L, 7L, 2L, 14L, 5L),
+ stringsAsFactors = FALSE
+)
+
+tbl <- tfl_table(
+ ae_verbatim,
+ col_labels = c(
+ subject_id = "Subject ID",
+ ae_term = "Adverse Event (Verbatim)",
+ onset_day = "Onset\n(Day)"
+ ),
+ col_widths = list(
+ subject_id = unit(0.8, "inches"),
+ ae_term = unit(3.5, "inches"),
+ onset_day = NULL
+ ),
+ wrap_cols = "ae_term"
+)
+
+export_tfl(
+ tbl,
+ preview = TRUE,
+ header_left = "Listing 1. Adverse Event Verbatim Terms",
+ header_rule = TRUE
+)
+```
+
+### Per-column specification with `tfl_colspec()`
+
+For complex tables it can be cleaner to specify each column separately
+using `tfl_colspec()` and collect the results into a list. This avoids
+long parallel vectors for labels, widths, and alignments.
+
+```{r tfl-colspec, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+pk_summary <- data.frame(
+ param = rep(c("Cmax", "AUC0-inf", "t1/2"), each = 3),
+ treatment = rep(c("Placebo", "Active 10 mg", "Active 20 mg"), 3),
+ geo_mean = c(0.00, 145.2, 210.8, 0.00, 4820, 7340, 0.00, 8.4, 9.1),
+ cv_pct = c(NA, 28.4, 31.2, NA, 22.7, 25.8, NA, 15.3, 17.9),
+ stringsAsFactors = FALSE
+)
+
+tbl <- pk_summary |>
+ group_by(param) |>
+ tfl_table(
+ cols = list(
+ tfl_colspec("param", label = "Parameter", width = unit(1.2, "inches"), align = "left"),
+ tfl_colspec("treatment", label = "Treatment", width = unit(1.5, "inches"), align = "left"),
+ tfl_colspec("geo_mean", label = "Geometric\nMean", width = unit(1.2, "inches"), align = "right"),
+ tfl_colspec("cv_pct", label = "CV%", width = unit(0.8, "inches"), align = "right")
+ ),
+ na_string = "--"
+ )
+
+export_tfl(
+ tbl,
+ preview = TRUE,
+ header_left = "Table 3. PK Parameters — Geometric Mean (CV%)",
+ footnote = c(
+ "CV% = coefficient of variation.",
+ "-- = not applicable (placebo)."
+ )
+)
+```
+
+`tfl_colspec()` accepts `col`, `label`, `width`, `align`, `wrap`, and `gp`.
+It provides no functionality beyond what the parallel-vector approach
+offers; the choice is stylistic.
+
---
-## Row grouping
+## Rows
+
+### Row grouping
-Mark grouping columns with `dplyr::group_by()` before passing the data to
-`tfl_table()`. Group columns must appear first in the column list; they act as
-row headers and their values are suppressed on repeated consecutive rows, giving
-the indented-group appearance common in clinical tables.
+Mark grouping columns with `dplyr::group_by()` before passing the data
+to `tfl_table()`. Group columns must appear first in the column list;
+they act as row headers and their values are suppressed on repeated
+consecutive rows, giving the indented-group appearance common in
+clinical tables.
```{r grouping, fig.width = 11, fig.height = 8.5, out.width = "100%"}
# Demographic summary with visit and treatment group as row headers
@@ -180,7 +272,7 @@ tbl <- pk_data |>
visit = "Visit",
treatment = "Treatment",
n = "n",
- mean_auc = "Mean AUC\n(ng\u00b7h/mL)",
+ mean_auc = "Mean AUC\n(ng·h/mL)",
sd_auc = "SD"
),
col_widths = list(
@@ -195,31 +287,113 @@ tbl <- pk_data |>
export_tfl(
tbl,
preview = TRUE,
- header_left = "Table 3. PK Summary by Visit and Treatment",
+ header_left = "Table 4. PK Summary by Visit and Treatment",
footnote = "AUC = area under the concentration-time curve."
)
```
-The `suppress_repeated_groups` argument (default `TRUE`) controls whether
-repeated group values are hidden. Set it to `FALSE` to show every row's group
-value explicitly.
+Group values within a block are kept together on the same page wherever
+possible (see [Pagination] below). Multiple group columns are honored —
+the leftmost is the outermost grouping, with each subsequent column
+nested inside its parent.
+
+### Suppression and multi-line group labels
+
+`suppress_repeated_groups` (default `TRUE`) blanks group-column cells
+whose value matches the previous rendered row. The first row on each
+page always shows the group value, so a reader can identify the group
+of any visible block of rows.
+
+When the group label itself spans multiple lines (because of an
+embedded `\n` or word-wrapping), the label flows downward through the
+blanked cells like an HTML `
` instead of inflating only
+the labelled row. The example below has a two-line group label
+("Treatment\nArm A") that flows across all three rows of the group; no
+row needs to grow taller than its data content.
+
+```{r rowspan-flow, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+flow_demo <- data.frame(
+ group = rep("Treatment\nArm A", 3L),
+ visit = c("Day 1", "Day 8", "Day 15"),
+ value = c("12.3", "15.7", "18.1"),
+ stringsAsFactors = FALSE
+) |> group_by(group)
+
+export_tfl(
+ tfl_table(flow_demo),
+ preview = TRUE,
+ header_left = "Multi-line group label flows across rows"
+)
+```
+
+Two consequences flow from this layout choice:
+
+- **Row heights are computed per page.** The same data row may render at
+ different heights on different pages when a group is split: the first
+ row on each page re-shows the label and may need to grow alone if the
+ rest of the group landed on a different page.
+- **Rules adapt to the flow.** Row rules (see
+ `vignette("v03-tfl_table_styling")`) are suppressed inside a multi-row
+ group span so the flowing label isn't visually sliced. Group rules
+ draw at every transition and start at the column whose value actually
+ changed at that boundary, so unchanged outer columns through which the
+ label is flowing aren't visually divided.
+
+If you would rather render every group cell explicitly on every row
+(no flow, no blanking), set `suppress_repeated_groups = FALSE`. Each
+row's height then becomes the per-row max over every cell, including
+the multi-line group label.
+
+### Missing values
-Group values within a block are kept together on the same page wherever possible
-(see [Multi-page tables] below).
+`na_string` controls how `NA` values are displayed in the table. The
+default is `""` (an empty cell). Supply any character string to
+substitute a visible token.
+
+```{r na-string, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+labs_data <- data.frame(
+ subject_id = c("001", "001", "002", "002", "003"),
+ visit = c("Baseline", "Week 4", "Baseline", "Week 4", "Baseline"),
+ ALT = c(28, 31, NA, 45, 22),
+ AST = c(19, NA, 24, 38, 17),
+ stringsAsFactors = FALSE
+)
+
+tbl <- labs_data |>
+ group_by(subject_id) |>
+ tfl_table(
+ col_labels = c(
+ subject_id = "Subject",
+ visit = "Visit",
+ ALT = "ALT\n(U/L)",
+ AST = "AST\n(U/L)"
+ ),
+ na_string = "NC" # NC = not collected
+ )
+
+export_tfl(
+ tbl,
+ preview = TRUE,
+ header_left = "Table 5. Laboratory Values",
+ footnote = "NC = not collected."
+)
+```
---
-## Multi-page tables
+## Pagination
### Row pagination
When a table has more rows than fit on one page, `tfl_table` paginates
-automatically. Groups are kept together: if the rows belonging to one group value
-do not fit on the current page, the whole group moves to the next page.
+automatically. Groups are kept together: if the rows belonging to one
+group value do not fit on the current page, the whole group moves to the
+next page wherever possible.
-Continuation markers are appended to the last column header on intermediate pages
-and to the first data row of a continuation page so the reader can follow the
-table across page breaks.
+When a group genuinely cannot fit on a single page, it is split, and
+continuation markers are appended to the last column header on
+intermediate pages and to the first data row of a continuation page so
+the reader can follow the table across page breaks.
```{r row-pagination, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- iris |>
@@ -240,7 +414,7 @@ tbl <- iris |>
export_tfl(
tbl,
preview = c(1, 2),
- header_left = "Table 4. Iris Measurements by Species",
+ header_left = "Table 6. Iris Measurements by Species",
header_rule = TRUE,
footer_rule = TRUE
)
@@ -248,10 +422,10 @@ export_tfl(
### Column pagination
-If the total column width exceeds the printable area, `tfl_table` splits the
-columns across additional pages. Row-header columns (i.e., the grouped columns)
-are repeated at the left of every column page so the reader always knows which
-group they are in.
+If the total column width exceeds the printable area, `tfl_table` splits
+the columns across additional pages. Row-header columns (i.e., the
+grouped columns) are repeated at the left of every column page so the
+reader always knows which group they are in.
```{r col-pagination, fig.width = 11, fig.height = 8.5, out.width = "100%"}
# Lab safety panel: 6 parameters × 13 timepoints — too wide for one page.
@@ -281,33 +455,27 @@ tbl <- lab_wide |>
tfl_table(
col_labels = c(
parameter = "Lab Parameter",
- scr = "Screen-\ning",
- bl = "Base-\nline",
- wk2 = "Week 2",
- wk4 = "Week 4",
- wk6 = "Week 6",
- wk8 = "Week 8",
- wk12 = "Week 12",
- wk16 = "Week 16",
- wk20 = "Week 20",
- wk24 = "Week 24",
- wk28 = "Week 28",
- wk32 = "Week 32",
- eot = "End of\nTreatment"
+ scr = "Screen-\ning", bl = "Base-\nline",
+ wk2 = "Week 2", wk4 = "Week 4",
+ wk6 = "Week 6", wk8 = "Week 8",
+ wk12 = "Week 12", wk16 = "Week 16",
+ wk20 = "Week 20", wk24 = "Week 24",
+ wk28 = "Week 28", wk32 = "Week 32",
+ eot = "End of\nTreatment"
)
)
export_tfl(
tbl,
preview = c(1, 2),
- header_left = "Table 5. Mean Lab Safety Values by Timepoint",
+ header_left = "Table 7. Mean Lab Safety Values by Timepoint",
header_rule = TRUE
)
```
-By default `allow_col_split = TRUE`. Set it to `FALSE` if you want an error
-rather than an automatic split — useful during development to confirm that your
-column widths fit within the target page dimensions.
+By default `allow_col_split = TRUE`. Set it to `FALSE` if you want an
+error rather than an automatic split — useful during development to
+confirm that your column widths fit within the target page dimensions.
```{r col-split-error, error = TRUE}
# This will error if the columns are too wide for the page
@@ -321,16 +489,18 @@ export_tfl(tbl_no_split, preview = TRUE)
### Balancing columns across pages
-By default the greedy algorithm packs as many columns as possible onto each
-page, which can leave the last page looking sparse — for example, 8 columns on
-page 1 and only 2 on page 2. Set `balance_col_pages = TRUE` to redistribute
-the data columns so that each page carries approximately the same number.
+By default the greedy algorithm packs as many columns as possible onto
+each page, which can leave the last page looking sparse — for example,
+8 columns on page 1 and only 2 on page 2. Set `balance_col_pages = TRUE`
+to redistribute the data columns so that each page carries
+approximately the same number.
-The greedy pass still runs first to determine the minimum number of pages
-required. The data columns are then divided as evenly as possible across those
-pages (pages that cannot be divided exactly get one extra column on the earlier
-pages). Each candidate balanced group is verified to fit within the available
-width; if any group would overflow, the greedy layout is used as a fallback.
+The greedy pass still runs first to determine the minimum number of
+pages required. The data columns are then divided as evenly as
+possible across those pages (pages that cannot be divided exactly get
+one extra column on the earlier pages). Each candidate balanced group
+is verified to fit within the available width; if any group would
+overflow, the greedy layout is used as a fallback.
```{r col-pagination-balanced, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl_balanced <- lab_wide |>
@@ -338,19 +508,13 @@ tbl_balanced <- lab_wide |>
tfl_table(
col_labels = c(
parameter = "Lab Parameter",
- scr = "Screen-\ning",
- bl = "Base-\nline",
- wk2 = "Week 2",
- wk4 = "Week 4",
- wk6 = "Week 6",
- wk8 = "Week 8",
- wk12 = "Week 12",
- wk16 = "Week 16",
- wk20 = "Week 20",
- wk24 = "Week 24",
- wk28 = "Week 28",
- wk32 = "Week 32",
- eot = "End of\nTreatment"
+ scr = "Screen-\ning", bl = "Base-\nline",
+ wk2 = "Week 2", wk4 = "Week 4",
+ wk6 = "Week 6", wk8 = "Week 8",
+ wk12 = "Week 12", wk16 = "Week 16",
+ wk20 = "Week 20", wk24 = "Week 24",
+ wk28 = "Week 28", wk32 = "Week 32",
+ eot = "End of\nTreatment"
),
balance_col_pages = TRUE
)
@@ -358,176 +522,19 @@ tbl_balanced <- lab_wide |>
export_tfl(
tbl_balanced,
preview = c(1, 2),
- header_left = "Table 5b. Mean Lab Safety Values by Timepoint (balanced columns)",
+ header_left = "Table 7b. Mean Lab Safety Values (balanced columns)",
header_rule = TRUE
)
```
---
-## Word-wrapping columns
-
-`wrap_cols` accepts a named numeric vector of column indices or names, specifying
-the maximum number of characters before wrapping. This is useful for free-text
-columns (narrative descriptions, verbatim terms) that would otherwise force
-very wide pages or illegible small fonts.
-
-```{r wrap-cols, fig.width = 11, fig.height = 8.5, out.width = "100%"}
-ae_verbatim <- data.frame(
- subject_id = c("001-001", "001-002", "001-003", "002-001", "002-002"),
- ae_term = c(
- "Nausea and vomiting, mild, considered possibly related to study treatment",
- "Headache, moderate, considered unlikely related",
- "Fatigue, mild, relationship to study drug uncertain",
- "Abdominal pain, moderate, considered probably related",
- "Dizziness, mild, considered possibly related"
- ),
- onset_day = c(3L, 7L, 2L, 14L, 5L),
- stringsAsFactors = FALSE
-)
-
-tbl <- tfl_table(
- ae_verbatim,
- col_labels = c(
- subject_id = "Subject ID",
- ae_term = "Adverse Event (Verbatim)",
- onset_day = "Onset\n(Day)"
- ),
- col_widths = list(
- subject_id = unit(0.8, "inches"),
- ae_term = unit(3.5, "inches"),
- onset_day = NULL
- ),
- wrap_cols = "ae_term"
-)
-
-export_tfl(
- tbl,
- preview = TRUE,
- header_left = "Listing 1. Adverse Event Verbatim Terms",
- header_rule = TRUE
-)
-```
-
----
-
-## Handling missing values
-
-`na_string` controls how `NA` values are displayed in the table. The default is
-`""` (an empty cell). Supply any character string to substitute a visible token.
-
-```{r na-string, fig.width = 11, fig.height = 8.5, out.width = "100%"}
-labs_data <- data.frame(
- subject_id = c("001", "001", "002", "002", "003"),
- visit = c("Baseline", "Week 4", "Baseline", "Week 4", "Baseline"),
- ALT = c(28, 31, NA, 45, 22),
- AST = c(19, NA, 24, 38, 17),
- stringsAsFactors = FALSE
-)
-
-tbl <- labs_data |>
- group_by(subject_id) |>
- tfl_table(
- col_labels = c(
- subject_id = "Subject",
- visit = "Visit",
- ALT = "ALT\n(U/L)",
- AST = "AST\n(U/L)"
- ),
- na_string = "NC" # NC = not collected
- )
-
-export_tfl(
- tbl,
- preview = TRUE,
- header_left = "Table 6. Laboratory Values",
- footnote = "NC = not collected."
-)
-```
-
----
-
-## Per-column specification with `tfl_colspec()`
-
-For complex tables it can be cleaner to specify each column separately using
-`tfl_colspec()` and collect the results into a list. This avoids long parallel
-vectors for labels, widths, and alignments.
-
-```{r tfl-colspec, fig.width = 11, fig.height = 8.5, out.width = "100%"}
-pk_summary <- data.frame(
- param = rep(c("Cmax", "AUC0-inf", "t1/2"), each = 3),
- treatment = rep(c("Placebo", "Active 10 mg", "Active 20 mg"), 3),
- geo_mean = c(0.00, 145.2, 210.8, 0.00, 4820, 7340, 0.00, 8.4, 9.1),
- cv_pct = c(NA, 28.4, 31.2, NA, 22.7, 25.8, NA, 15.3, 17.9),
- stringsAsFactors = FALSE
-)
-
-tbl <- pk_summary |>
- group_by(param) |>
- tfl_table(
- cols = list(
- tfl_colspec("param", label = "Parameter", width = unit(1.2, "inches"), align = "left"),
- tfl_colspec("treatment", label = "Treatment", width = unit(1.5, "inches"), align = "left"),
- tfl_colspec("geo_mean", label = "Geometric\nMean", width = unit(1.2, "inches"), align = "right"),
- tfl_colspec("cv_pct", label = "CV%", width = unit(0.8, "inches"), align = "right")
- ),
- na_string = "--"
- )
-
-export_tfl(
- tbl,
- preview = TRUE,
- header_left = "Table 7. PK Parameters — Geometric Mean (CV%)",
- footnote = c(
- "CV% = coefficient of variation.",
- "-- = not applicable (placebo)."
- )
-)
-```
-
-`tfl_colspec()` accepts `col`, `label`, `width`, `align`, `wrap`, and `gp`.
-It provides no functionality beyond what the parallel-vector approach offers;
-the choice is stylistic.
-
----
-
-## Typography with `gp`
-
-The `gp` argument to `tfl_table()` controls cell typography. Pass a single
-`gpar()` for a uniform style, or a named list for per-section control. For a
-full reference of all `gp` keys and their effects, see
-`vignette("v03-tfl_table_styling")`.
-
-```{r gp, eval = FALSE}
-tbl <- tfl_table(
- head(mtcars, 15)[, c("mpg", "cyl", "hp", "wt")],
- col_labels = c(
- mpg = "MPG",
- cyl = "Cylinders",
- hp = "Horsepower",
- wt = "Weight"
- ),
- gp = list(
- header = gpar(fontsize = 9, fontface = "bold"),
- body = gpar(fontsize = 9)
- )
-)
-
-export_tfl(
- tbl,
- file = "typed_table.pdf",
- gp = gpar(fontsize = 9) # page annotation text
-)
-```
-
----
-
## Page layout and annotations
-Page dimensions, margins, header/footer text, separator rules, and page numbering
-are all arguments to `export_tfl()`, not `tfl_table()`. This keeps the table
-structure independent from the output format, allowing the same `tfl_table`
-object to be used with different page layouts.
+Page dimensions, margins, header/footer text, separator rules, and page
+numbering are all arguments to `export_tfl()`, not `tfl_table()`. This
+keeps the table structure independent from the output format, allowing
+the same `tfl_table` object to be used with different page layouts.
```{r annotations, fig.width = 8.5, fig.height = 11, out.width = "100%"}
tbl <- tfl_table(
@@ -558,9 +565,31 @@ export_tfl(
)
```
-See `vignette("v01-figure_output")` for a full reference of all `export_tfl()`
-layout arguments, including typography (`gp`), padding, rules, and overlap
-detection.
+See `vignette("v01-figure_output")` for a full reference of all
+`export_tfl()` layout arguments, including typography (`gp`), padding,
+rules, and overlap detection.
+
+---
+
+## Typography (preview)
+
+The `gp` argument to `tfl_table()` controls cell typography. Pass a single
+`gpar()` for a uniform style, or a named list for per-section control.
+
+```{r gp, eval = FALSE}
+tbl <- tfl_table(
+ head(mtcars, 15)[, c("mpg", "cyl", "hp", "wt")],
+ col_labels = c(mpg = "MPG", cyl = "Cylinders", hp = "Horsepower", wt = "Weight"),
+ gp = list(
+ table = gpar(fontsize = 9),
+ header_row = gpar(fontface = "bold")
+ )
+)
+```
+
+For a full reference of all `gp` keys and their effects (typography,
+rules, fills, sub-tables, and the complete list of styling arguments),
+see `vignette("v03-tfl_table_styling")`.
---
@@ -569,6 +598,7 @@ detection.
| Argument | Default | Purpose |
|----------|---------|---------|
| `x` | — | Data frame or grouped tibble |
+| **Columns** | | |
| `cols` | `NULL` (all columns) | `NULL` or a list of `tfl_colspec()` objects. To display a column subset, pre-select columns in `x` before passing to `tfl_table()`. |
| `col_widths` | `NULL` (auto) | Named list of `unit()`, plain numeric, or `NULL` per column |
| `col_labels` | column names | Named character vector of header labels; supports `\n` |
@@ -576,17 +606,26 @@ detection.
| `wrap_cols` | `NULL` | Names of columns to word-wrap |
| `min_col_width` | `unit(0.5, "inches")` | Floor applied to auto-sized columns |
| `allow_col_split` | `TRUE` | If `FALSE`, error when columns exceed page width |
-| `balance_col_pages` | `FALSE` | If `TRUE`, redistribute columns evenly across column-split pages instead of packing left-to-right |
-| `suppress_repeated_groups` | `TRUE` | Hide repeated group values in consecutive rows |
-| `col_cont_msg` | `"Columns continue on other pages"` | Rotated side-label text on column-split pages: clockwise 90° to the right when columns continue on a later page; counter-clockwise 90° to the left when columns continue from a prior page |
+| `balance_col_pages` | `FALSE` | Redistribute columns evenly across column-split pages |
+| **Rows and grouping** | | |
+| `suppress_repeated_groups` | `TRUE` | Blank repeated group values; multi-line labels flow into the blanked cells (HTML-rowspan style) |
+| `na_string` | `""` | Replacement for `NA` values |
+| **Pagination** | | |
+| `col_cont_msg` | side labels | Rotated text on column-split pages |
| `row_cont_msg` | `c("(continued)", "(continued on next page)")` | `[1]` shown at top of continuation page; `[2]` shown at bottom of page before continuation |
+| **Rules and separators** | | |
| `show_col_names` | `TRUE` | Whether to render the column header row at all |
| `col_header_rule` | `TRUE` | Rule below column headers |
-| `group_rule` | `TRUE` | Rule above each new group block |
+| `group_rule` | `TRUE` | Rule at each new group block (partial width — see `vignette("v03-tfl_table_styling")`) |
| `group_rule_after_last` | `FALSE` | Rule after the last group block |
+| `row_rule` | `FALSE` | Rule between data rows (suppressed inside multi-row group spans) |
| `row_header_sep` | `FALSE` | Vertical rule after row-header columns |
-| `na_string` | `""` | Replacement for `NA` values |
-| `gp` | `list()` | Typography for headers and body cells |
+| **Sub-tables** | | |
+| `sub_tfl` | `NULL` | One self-identifying sub-table per unique combination of values; see `vignette("v03-tfl_table_styling")` |
+| `sub_tfl_sep` / `sub_tfl_collapse` / `sub_tfl_prefix` | `": "` / `"; "` / `"\n"` | Caption-suffix formatting for sub-tables |
+| **Typography and spacing** | | |
+| `gp` | `list()` | Typography for headers and body cells (see `vignette("v03-tfl_table_styling")`) |
+| `fill_by` | `"row"` | `"row"` or `"group"` for cell fill cycling |
| `cell_padding` | `unit(c(0.2, 0.5), "lines")` | Vertical and horizontal padding inside each cell |
| `line_height` | `1.05` | Inter-line spacing multiplier for word-wrapped cells |
| `max_measure_rows` | `Inf` | Number of rows sampled when measuring auto column widths |
diff --git a/vignettes/v03-tfl_table_styling.Rmd b/vignettes/v03-tfl_table_styling.Rmd
index 97ee1e6..3fe99fd 100644
--- a/vignettes/v03-tfl_table_styling.Rmd
+++ b/vignettes/v03-tfl_table_styling.Rmd
@@ -24,8 +24,8 @@ library(dplyr) # for group_by()
# treatment is the first column so it can serve as the group column.
clinical <- data.frame(
treatment = c(rep("Active (n=120)", 3), rep("Placebo (n=118)", 3)),
- subgroup = c("All patients", "Age < 65", "Age \u2265 65",
- "All patients", "Age < 65", "Age \u2265 65"),
+ subgroup = c("All patients", "Age < 65", "Age ≥ 65",
+ "All patients", "Age < 65", "Age ≥ 65"),
n = c(120L, 74L, 46L, 118L, 71L, 47L),
responders = c( 68L, 44L, 24L, 31L, 18L, 13L),
rate_pct = c(56.7, 59.5, 52.2, 26.3, 25.4, 27.7),
@@ -43,12 +43,12 @@ col_spec <- list(
---
-## 1. Overview
+## Overview
-`tfl_table()` accepts a `gp` argument that is a **named list of `gpar()` objects**.
-Each key targets a specific visual element of the rendered table. Keys that are
-not supplied fall back to sensible clinical defaults; you only need to specify the
-elements you want to change.
+`tfl_table()` accepts a `gp` argument that is a **named list of `gpar()`
+objects**. Each key targets a specific visual element of the rendered table.
+Keys that are not supplied fall back to sensible clinical defaults; you only
+need to specify the elements you want to change.
The full set of recognized keys is:
@@ -72,12 +72,24 @@ Page-level typography — the text in the page header, caption, footnote, and
footer zones — is controlled by the `gp` argument of `export_tfl_page()` /
`export_tfl()`, not by `tfl_table()`'s `gp`.
+This vignette covers the styling arguments grouped into:
+
+- [Typography]: font, weight, family, color of each cell category.
+- [Rules and separators]: horizontal/vertical lines that delineate sections.
+- [Cell padding]: whitespace inside each cell.
+- [Cell background shading]: per-row, per-header, and per-group fills.
+- [Multi-page accessories]: continuation messages and side labels.
+- [Sub-tables]: splitting a table into per-group sub-tables.
+- [Complete examples]: clinical-default vs. publication-style end-to-end.
+
---
-## 2. Base font — `gp$table`
+## Typography
-`gp$table` is the typographic root for the whole table. Every other `gp` key
-inherits from it unless overridden.
+### Base font — `gp$table`
+
+`gp$table` is the typographic root for the whole table. Every other `gp`
+key inherits from it unless overridden.
```{r gp-table, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- tfl_table(
@@ -93,9 +105,7 @@ export_tfl(tbl, preview = TRUE, header_left = "Base font: serif 8pt")
Changing `gp$table` propagates to all rows and rules unless you selectively
override a more specific key.
----
-
-## 3. Column header row style — `gp$header_row` and `show_col_names`
+### Column header row — `gp$header_row` and `show_col_names`
The column header row renders the column names (or labels supplied via
`tfl_colspec()`). By default headers are **bold** at the base font size.
@@ -113,9 +123,9 @@ tbl <- tfl_table(
export_tfl(tbl, preview = TRUE, header_left = "Header row: italic 10pt")
```
-Set `show_col_names = FALSE` to suppress the header row entirely — useful when
-you are stacking multiple `tfl_table` objects on one page and only the first
-needs column labels.
+Set `show_col_names = FALSE` to suppress the header row entirely — useful
+when you are stacking multiple `tfl_table` objects on one page and only the
+first needs column labels.
```{r show-col-names, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl_no_header <- tfl_table(
@@ -127,9 +137,7 @@ export_tfl(tbl_no_header, preview = TRUE,
header_left = "show_col_names = FALSE")
```
----
-
-## 4. Data row style — `gp$data_row`
+### Data row — `gp$data_row`
`gp$data_row` controls the appearance of every non-header, non-group-column
cell. It inherits `gp$table` automatically.
@@ -146,12 +154,11 @@ tbl <- tfl_table(
export_tfl(tbl, preview = TRUE, header_left = "Data row: grey text")
```
----
-
-## 5. Group column style — `gp$group_col` and per-column override
+### Group column — `gp$group_col` and per-column override
-Row-header (group) columns — those designated via `dplyr::group_by()` — receive
-their own style key, `gp$group_col`, which also inherits `gp$table`.
+Row-header (group) columns — those designated via `dplyr::group_by()` —
+receive their own style key, `gp$group_col`, which also inherits
+`gp$table`.
```{r gp-group-col, fig.width = 11, fig.height = 8.5, out.width = "100%"}
# Bold group column to distinguish it from data columns
@@ -172,8 +179,8 @@ tbl <- clinical |>
export_tfl(tbl, preview = TRUE, header_left = "Group column: bold")
```
-To override a **single** group column without touching the others, pass `gp`
-directly to `tfl_colspec()`:
+To override a **single** group column without touching the others, pass
+`gp` directly to `tfl_colspec()`:
```{r colspec-gp, fig.width = 11, fig.height = 8.5, out.width = "100%"}
# The treatment group column gets bold via its tfl_colspec gp;
@@ -197,13 +204,11 @@ export_tfl(tbl, preview = TRUE,
The `gp` on `tfl_colspec()` takes precedence over `gp$group_col` for that
specific column; all other group columns still inherit `gp$group_col`.
----
-
-## 6. Continuation marker style — `gp$continued` and `row_cont_msg`
+### Continuation marker — `gp$continued` and `row_cont_msg`
-When a table spans multiple pages, `tfl_table()` injects a continuation marker
-at the bottom of each non-final page. By default the marker text is
-`"(continued)"` and is rendered in italic.
+When a table spans multiple pages, `tfl_table()` injects a continuation
+marker at the bottom of each non-final page. By default the marker text
+is `"(continued)"` and is rendered in italic.
```{r gp-continued, eval = FALSE}
# Smaller continuation marker, explicit message
@@ -217,18 +222,19 @@ tbl <- tfl_table(
```
`row_cont_msg` replaces the default `"(continued)"` string. `gp$continued`
-controls the visual rendering of whatever text `row_cont_msg` provides. The
-continuation marker only appears on tables with more rows than fit on one page;
-see `vignette("v02-tfl_table_intro")` for an example.
+controls the visual rendering of whatever text `row_cont_msg` provides.
+The continuation marker only appears on tables with more rows than fit on
+one page; see `vignette("v02-tfl_table_intro")` for an example.
---
-## 7. Horizontal rules — `col_header_rule`, `group_rule`, `group_rule_after_last`
+## Rules and separators
-Three boolean arguments switch rules on or off; their corresponding `gp` keys
-control line appearance.
+Three boolean arguments switch horizontal rules on or off; their
+corresponding `gp` keys control line appearance. A vertical separator
+between row-headers and data columns is also available.
-### Column header rule
+### Column header rule — `col_header_rule`
A horizontal rule drawn immediately below the column header row.
@@ -254,11 +260,26 @@ export_tfl(tbl_no_rule, preview = TRUE,
header_left = "col_header_rule = FALSE")
```
-### Between-group rules
+### Group rules — `group_rule`, `group_rule_after_last`
+
+A rule drawn at the boundary between adjacent groups (defined by changes
+in any group column). `group_rule_after_last` controls whether a rule
+also appears after the final group.
-A rule drawn after each group of rows (defined by changes in the first group
-column). `group_rule_after_last` controls whether a rule also appears after the
-final group.
+The line is drawn as a **partial width**: it starts at the column whose
+value actually changed at this transition and extends to the right edge
+of the table. So with nested groups
+`group_vars = c("Cohort", "Visit")`:
+
+| Transition | Outermost change | Rule columns |
+|------------|------------------|--------------|
+| Visit changes within the same Cohort | Visit (level 2) | Visit + data columns |
+| Cohort changes | Cohort (level 1) | Cohort + Visit + data columns |
+
+This keeps the rule from visually slicing through an outer group's label
+that is flowing across multiple rows
+(see [Multi-line group labels and rowspan flow](v02-tfl_table_intro.html)
+in the intro vignette).
```{r group-rule, fig.width = 11, fig.height = 8.5, out.width = "100%"}
# Solid thin rules between groups, including after the last one
@@ -288,17 +309,19 @@ export_tfl(tbl_no_grp, preview = TRUE,
header_left = "group_rule = FALSE")
```
-The default `gp$group_rule` is `gpar(lwd = 0.5, lty = "dotted")`. Any valid
-`lty` value accepted by `grid` (e.g. `"dashed"`, `"solid"`, `"dotted"`) works
-here.
-
----
+The default `gp$group_rule` is `gpar(lwd = 0.5, lty = "dotted")`. Any
+valid `lty` value accepted by `grid` (e.g. `"dashed"`, `"solid"`,
+`"dotted"`) works here.
-## 8. Data row rules — `row_rule` and `gp$row_rule`
+### Row rules — `row_rule`
-A horizontal rule drawn between every pair of consecutive data rows. Enabled
-with `row_rule = TRUE`. Unlike `group_rule` (which only appears at group
-boundaries), `row_rule` draws a line after every row except the last.
+A horizontal rule drawn between every pair of consecutive data rows.
+Enabled with `row_rule = TRUE`. Unlike `group_rule` (which only fires at
+group boundaries), `row_rule` draws after every row except the last —
+**unless** the row below it is part of a multi-row group span. A line
+that would slice through a label flowing downward through suppressed
+cells is automatically suppressed, the same way HTML rowspan cells have
+no internal borders.
```{r row-rule, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- tfl_table(
@@ -316,71 +339,10 @@ export_tfl(tbl, preview = TRUE,
The default `gp$row_rule` is `gpar(lwd = 0.5)`. Set `row_rule = FALSE`
(the default) to suppress inter-row rules entirely.
----
-
-## 9. Cell background shading — `gp$fill` and `fill_by`
-
-Background colors for the header row and data rows are controlled through
-the `fill` field in existing gp keys.
-
-### Header row background
-
-```{r header-fill, fig.width = 11, fig.height = 8.5, out.width = "100%"}
-tbl <- tfl_table(
- clinical,
- gp = list(
- header_row = gpar(fontface = "bold", fill = "lightblue")
- )
-)
-
-export_tfl(tbl, preview = TRUE,
- header_left = "Header row with fill = 'lightblue'")
-```
-
-### Alternating row colors (zebra striping)
-
-Pass a vector of colors to `gp$data_row$fill` to alternate between them:
-
-```{r zebra, fig.width = 11, fig.height = 8.5, out.width = "100%"}
-tbl <- tfl_table(
- clinical,
- gp = list(
- header_row = gpar(fontface = "bold", fill = "steelblue4", col = "white"),
- data_row = gpar(fill = c("grey95", "white"))
- )
-)
-
-export_tfl(tbl, preview = TRUE,
- header_left = "Alternating row colors")
-```
-
-### Alternating by group — `fill_by = "group"`
-
-By default, `fill_by = "row"` cycles through the color vector for each data
-row. Setting `fill_by = "group"` advances the color index only at group
-boundaries, so all rows in the same group share one background color.
-
-```{r fill-by-group, fig.width = 11, fig.height = 8.5, out.width = "100%"}
-tbl <- clinical |>
- group_by(treatment) |>
- tfl_table(
- cols = col_spec,
- fill_by = "group",
- gp = list(
- data_row = gpar(fill = c("grey95", "white"))
- )
- )
-
-export_tfl(tbl, preview = TRUE,
- header_left = "fill_by = 'group': banded groups")
-```
-
----
-
-## 10. Vertical row-header separator — `row_header_sep` and `gp$row_header_sep`
+### Vertical row-header separator — `row_header_sep`
-A vertical rule drawn to the right of the last row-header (group) column,
-separating the row labels from the data columns. Enabled with
+A vertical rule drawn to the right of the last row-header (group)
+column, separating the row labels from the data columns. Enabled with
`row_header_sep = TRUE`.
```{r row-header-sep, fig.width = 11, fig.height = 8.5, out.width = "100%"}
@@ -417,10 +379,10 @@ export_tfl(tbl_no_sep, preview = TRUE,
---
-## 11. Cell padding — `cell_padding`
+## Cell padding
-`cell_padding` is a `grid::unit` object that controls the whitespace between
-cell content and cell boundaries. It accepts two forms:
+`cell_padding` is a `grid::unit` object that controls the whitespace
+between cell content and cell boundaries. It accepts two forms:
**Scalar** — the same padding is applied on all four sides:
@@ -433,8 +395,9 @@ tbl <- tfl_table(
export_tfl(tbl, preview = TRUE, header_left = "Uniform padding: 0.15 lines")
```
-**Two-element vector** — separate vertical and horizontal padding. Use this
-when you want tighter horizontal spacing but more vertical breathing room:
+**Two-element vector** — separate vertical and horizontal padding. Use
+this when you want tighter horizontal spacing but more vertical
+breathing room:
```{r cell-padding-vh, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- tfl_table(
@@ -446,22 +409,85 @@ export_tfl(tbl, preview = TRUE,
header_left = "Asymmetric padding: 0.3v / 0.1h lines")
```
-The first element controls top and bottom padding; the second controls left and
-right. Reducing horizontal padding allows more columns to fit on a page without
-reducing font size.
+The first element controls top and bottom padding; the second controls
+left and right. Reducing horizontal padding allows more columns to fit
+on a page without reducing font size.
---
-## 12. Column continuation message — `col_cont_msg`
+## Cell background shading
+
+Background colors for the header row and data rows are controlled
+through the `fill` field in existing gp keys.
+
+### Header row background
+
+```{r header-fill, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+tbl <- tfl_table(
+ clinical,
+ gp = list(
+ header_row = gpar(fontface = "bold", fill = "lightblue")
+ )
+)
+
+export_tfl(tbl, preview = TRUE,
+ header_left = "Header row with fill = 'lightblue'")
+```
-When the table has more columns than fit on one page, `tfl_table()` splits
-across multiple column-pages. `col_cont_msg` is a character string displayed as
-rotated side labels:
+### Alternating row colors (zebra striping)
-- **Clockwise 90°** (reading downward) to the **right** of the table on pages
- where columns continue on a subsequent page.
-- **Counter-clockwise 90°** (reading upward) to the **left** of the full table
- (including row-label columns) on pages where columns continue from a prior page.
+Pass a vector of colors to `gp$data_row$fill` to alternate between them:
+
+```{r zebra, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+tbl <- tfl_table(
+ clinical,
+ gp = list(
+ header_row = gpar(fontface = "bold", fill = "steelblue4", col = "white"),
+ data_row = gpar(fill = c("grey95", "white"))
+ )
+)
+
+export_tfl(tbl, preview = TRUE,
+ header_left = "Alternating row colors")
+```
+
+### Alternating by group — `fill_by = "group"`
+
+By default, `fill_by = "row"` cycles through the color vector for each
+data row. Setting `fill_by = "group"` advances the color index only at
+group boundaries, so all rows in the same group share one background
+color.
+
+```{r fill-by-group, fig.width = 11, fig.height = 8.5, out.width = "100%"}
+tbl <- clinical |>
+ group_by(treatment) |>
+ tfl_table(
+ cols = col_spec,
+ fill_by = "group",
+ gp = list(
+ data_row = gpar(fill = c("grey95", "white"))
+ )
+ )
+
+export_tfl(tbl, preview = TRUE,
+ header_left = "fill_by = 'group': banded groups")
+```
+
+---
+
+## Multi-page accessories
+
+### Column continuation message — `col_cont_msg`
+
+When the table has more columns than fit on one page, `tfl_table()`
+splits across multiple column-pages. `col_cont_msg` is a character
+string displayed as rotated side labels:
+
+- **Clockwise 90°** (reading downward) to the **right** of the table on
+ pages where columns continue on a subsequent page.
+- **Counter-clockwise 90°** (reading upward) to the **left** of the
+ full table (including row-label columns) on pages where columns
+ continue from a prior page.
One line-height of spacing separates the table edge from the text. Set
`col_cont_msg = NULL` to suppress the labels entirely.
@@ -480,18 +506,22 @@ tbl_no_msg <- tfl_table(
)
```
+The corresponding `row_cont_msg` for row-pagination markers is covered
+under [Continuation marker — `gp$continued` and `row_cont_msg`] in the
+typography section above.
+
---
-## 13. Sub-tables — `sub_tfl`
+## Sub-tables — `sub_tfl`
-`sub_tfl` splits a single `tfl_table` into one self-identifying sub-table per
-unique combination of values in the named columns. The columns named in
-`sub_tfl` are **removed from the rendered table body** and instead appear in
-the caption as `"label: value; label: value"`.
+`sub_tfl` splits a single `tfl_table` into one self-identifying
+sub-table per unique combination of values in the named columns. The
+columns named in `sub_tfl` are **removed from the rendered table body**
+and instead appear in the caption as `"label: value; label: value"`.
-This is the idiomatic way to produce by-group tables (e.g. one table per
-treatment arm, per visit) without manually splitting the data and stitching
-the page lists together.
+This is the idiomatic way to produce by-group tables (e.g. one table
+per treatment arm, per visit) without manually splitting the data and
+stitching the page lists together.
```{r sub-tfl-basic, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl_by_arm <- tfl_table(
@@ -507,15 +537,15 @@ export_tfl(
)
```
-The first preview page shows `Table 1. Response by Subgroup` followed on a
-new line by `Treatment Arm: Active (n=120)` (the colspec label is reused, not
-the raw column name). The `treatment` column itself is no longer in the
-table body. A second page contains the placebo arm.
+The first preview page shows `Table 1. Response by Subgroup` followed
+on a new line by `Treatment Arm: Active (n=120)` (the colspec label is
+reused, not the raw column name). The `treatment` column itself is no
+longer in the table body. A second page contains the placebo arm.
### Multiple sub_tfl columns
-Naming more than one column produces the Cartesian product, with the first
-column varying outermost:
+Naming more than one column produces the Cartesian product, with the
+first column varying outermost:
```{r sub-tfl-multi, eval = FALSE}
tbl <- tfl_table(
@@ -549,23 +579,25 @@ tfl_table(
# Caption per page: "Table 1 — Treatment Arm = Active (n=120) | Subgroup = ..."
```
-When the global `caption` is `NULL`, the suffix becomes the entire caption
-(no leading prefix).
+When the global `caption` is `NULL`, the suffix becomes the entire
+caption (no leading prefix).
### Ordering
Sub-tables are produced in this order:
-- **Factor columns** drive their own ordering by `levels()` (only levels that
- appear in the data are emitted). Use a factor when you need a clinically
- meaningful order such as `Active` before `Placebo`.
-- **Character / numeric columns** use first-appearance order in the data.
+- **Factor columns** drive their own ordering by `levels()` (only
+ levels that appear in the data are emitted). Use a factor when you
+ need a clinically meaningful order such as `Active` before `Placebo`.
+- **Character / numeric columns** use first-appearance order in the
+ data.
### Overlap with `group_vars()`
-When a column listed in `sub_tfl` is *also* a `dplyr::group_by()` variable
-(a row-header column), it is promoted to the caption — i.e. removed from
-both the rendered body and from `group_vars`. This is a common case:
+When a column listed in `sub_tfl` is *also* a `dplyr::group_by()`
+variable (a row-header column), it is promoted to the caption — i.e.
+removed from both the rendered body and from `group_vars`. This is a
+common case:
```{r sub-tfl-overlap, eval = FALSE}
clinical |>
@@ -577,17 +609,18 @@ clinical |>
### Sub-figures via `ggtibble`
-`export_tfl.ggtibble()` accepts the same four arguments. The named columns
-are appended to each row's caption (using the raw column names as labels, as
-ggtibbles have no colspec system). This is the recommended way to build
-by-group sub-figure decks.
+`export_tfl.ggtibble()` accepts the same four arguments. The named
+columns are appended to each row's caption (using the raw column names
+as labels, as ggtibbles have no colspec system). This is the
+recommended way to build by-group sub-figure decks.
---
-## 14. Complete example: clinical default vs. publication style
+## Complete examples
-The following pair of examples contrasts the out-of-the-box clinical appearance
-with a more compact publication-style variant. Both render using `preview = TRUE`.
+The following pair of examples contrasts the out-of-the-box clinical
+appearance with a more compact publication-style variant. Both render
+using `preview = TRUE`.
### Default clinical look
@@ -657,7 +690,8 @@ export_tfl(
The two outputs differ visibly in:
-- Font family and weight (sans-serif bold headers vs. serif plain headers)
+- Font family and weight (sans-serif bold headers vs. serif plain
+ headers)
- Presence of group rules and the vertical row-header separator
- Cell padding (uniform vs. asymmetric v/h form)
- Header rule weight
|