From be00d301ec6b158e094c9fc5aac696f8cb73bad1 Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Sat, 9 May 2026 16:53:54 -0400 Subject: [PATCH] Reorganize tfl_table vignettes; document rowspan flow + partial-width rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v02 (intro) was a flat list of sections. Group them into three nested parents — Columns, Rows, Pagination — and move tfl_colspec() under Columns where it belongs. Add a new "Suppression and multi-line group labels" subsection under Rows that documents the HTML-rowspan-style flow introduced for issue 29: when a multi-line group label sits above suppressed cells, it flows downward through them rather than inflating just the labelled row. Cross-reference the v03 sections that describe how rules adapt to the flow. Update the summary table to (a) add previously-missing arguments — row_rule, fill_by, the sub_tfl family — and (b) regroup rows by purpose so the table is scannable. v03 (styling) had a flat numbered 1..14 outline. Drop the manual numbering and group the sections under five logical parents: Typography, Rules and separators, Cell padding, Cell background shading, Multi-page accessories, Sub-tables, Complete examples. Move the column-continuation message under Multi-page accessories where it sits next to the row-continuation marker styling. Update the Group rules subsection to describe the partial-width rendering (rule starts at the column whose value actually changed at the transition, with a worked example for nested group_vars). Update the Row rules subsection to mention the within-span suppression (a rule that would slice a flowing label is omitted, the same way HTML rowspan cells have no internal borders). Both vignettes still render to HTML cleanly via rmarkdown::render(). No code changes; full test suite still passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- vignettes/v02-tfl_table_intro.Rmd | 541 +++++++++++++++------------- vignettes/v03-tfl_table_styling.Rmd | 360 +++++++++--------- 2 files changed, 487 insertions(+), 414 deletions(-) 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