---
title: "Reporting Examples using ksTFL"
author: "ksTFL Team"
output:
rmarkdown::html_vignette:
dev: pdf
css: ksTFL-vignette.css
resource_files:
- images/
vignette: >
%\VignetteIndexEntry{Reporting Examples using ksTFL}
%\VignetteEncoding{UTF-8}
%\VignetteEngine{knitr::rmarkdown}
editor_options:
markdown:
wrap: 80
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(
comment = "", collapse = TRUE, eval = FALSE,
dev = "png",
dev.args = if (capabilities("cairo")) list(type = "cairo") else NULL
)
options(pkgdown.internet = FALSE)
```
This vignette presents a realistic progression of reporting examples —
from minimal tables/listings/figures/text to a full clinical-style table
with multi-level headers, spanning stub columns, multi-line
titles/footnotes, reusable styles, and session defaults. All examples
use exported ksTFL helpers only; set `eval=TRUE` locally to run the R
chunks.
## Category and scope
This is a workflow-first vignette for readers who already know the basic
pipeline and want practical patterns they can adapt directly.
Related reading:
- [Getting Started](Getting_Started_with_ksTFL.html) for the core pipeline.
- [Styling Guide](Styling_Guide_with_ksTFL.html),
[Advanced StyleRows](Advanced_StyleRows.html), and
[Column Width Management](Column_Width_Management.html) for deeper feature
coverage.
## Workflow overview
Each example follows the same end-to-end pattern so you can reproduce the full
pipeline locally:
- Prepare small, self-contained sample data (run the setup chunk with
`eval=TRUE`).
- Build `TFL_spec` objects using `create_table()`, `create_figure()`
or `create_text()`.
- Tune columns and styles via `define_cols()` and `add_style()` (use
`f_combine()` to reference combined styles).
- Assemble specs into a `TFL_report` with `create_report()` (this
consolidates style references and assigns `dataRef`).
- Render to a DOCX with `write_doc()` (one-step: saves JSON metadata
and produces the final document).
Keep in mind:
- Use named styles (`add_style()`) and reference them by id; avoid
inspecting or mutating internal fields.
- Use `print(spec)` for concise, user-facing previews rather than
reading internals, or use the package add-ins for interactive
inspection.
- Use `metaPath = tempdir()` during examples to avoid writing
permanent files while you experiment.
```{r create_sample_data, eval = FALSE}
# Sample datasets used by multiple sections (run locally before executing examples)
set.seed(2025)
demog_tbl <- data.frame(
subject_id = sprintf("S%03d", 1:24),
age = sample(25:80, 24, TRUE),
sex = sample(c("M","F"), 24, TRUE),
trt = sample(c("Placebo","DrugA"), 24, TRUE),
stringsAsFactors = FALSE
)
vitals_tbl <- data.frame(
subject_id = rep(demog_tbl$subject_id, each = 2),
visit = rep(c("Baseline","Week 12"), times = 24),
sbp = round(rnorm(48, 120, 12)),
dbp = round(rnorm(48, 75, 8)),
stringsAsFactors = FALSE
)
labs_tbl <- data.frame(
subject_id = demog_tbl$subject_id,
ALT = round(rnorm(24, 28, 9)),
AST = round(rnorm(24, 26, 8)),
stringsAsFactors = FALSE
)
# Example plot file (created locally when running the vignette)
# type = "cairo" avoids needing an X11 display (headless-safe for pkgdown/CI)
out_dir <- file.path(tempdir(), "ksTFL-reporting-examples")
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
plot_file <- file.path(tempdir(), "example_plot.png")
png(plot_file, width = 600, height = 400, type = "cairo")
plot(demog_tbl$age, vitals_tbl$sbp[1:24], xlab = "Age", ylab = "SBP", main = "Age vs SBP")
dev.off()
## Session-wide package and document settings:
tfl_reset_options()
tfl_set_options(
add_header(c("CRO Example LLC.", "CONFIDENTIAL", "Page {PAGE} of {NUMPAGES}")),
add_header("Study: Miracle Drug 001"),
add_footer(c("Showcase examples", "Program: inst/examples/showcase")),
output_directory = out_dir,
footnotePlace = "repeated",
meta_directory = file.path(out_dir, "meta")
)
```
## 1 — Simple minimal table
```{r example_minimal_table, eval = FALSE}
spec_min_table <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
spec_min_table <- add_title(spec_min_table, "Demographics (minimal)")
# Print shows a compact overview (useful in interactive sessions)
print(spec_min_table)
# close the example chunk
```
print() on a TFL_spec provides a readable overview including titles and
defined columns: 
```{r save_min_table, eval = FALSE}
# End-to-end: wrap single spec into a report and render to DOCX
rpt_min_table <- create_report(spec_min_table)
write_doc(rpt_min_table, name = "tbl_min")
```
Rendered output:

> **A note on the `cols` parameter** — The `cols` argument in
> `create_table()` specifies which columns are rendered in the document
> and their left-to-right order. It acts purely as a *presentation
> directive*: the underlying data frame is stored in full, so columns
> omitted from `cols` are **not** dropped from the spec. They remain
> available for conditional logic in `compute_cols()`. For instance, you
> could exclude a helper column like `flag` from the report
> (`cols = c(subject_id, age, sex, trt)`) yet still reference `flag` in
> a `compute_cols()` condition to drive styling or value transformations.
> This design means you never need to pre-filter or reorder your data
> frame before passing it to ksTFL — `cols` handles column selection and
> ordering in one place.
## 2 — Simple minimal figure
```{r example_minimal_figure, eval = FALSE}
spec_min_fig <- create_figure(plot_file)
spec_min_fig <- add_title(spec_min_fig, "Example: Age vs SBP")
print(spec_min_fig)
# close example chunk
```
```{r save_min_fig, eval = FALSE}
# End-to-end: render the single-figure report to DOCX
rpt_min_fig <- create_report(spec_min_fig)
write_doc(rpt_min_fig, name = "fig_min")
```
Rendered output:

## 3 — Simple minimal text (narrative)
```{r example_minimal_text, eval = FALSE}
## Narrative object:
nartv <- list(
subj = 'ABC-001',
enrldt = '01JAN2022',
compldt = '22JUN2022',
discreas = 'Completed per protocol',
nae = 3,
listae = list('Nausea', 'Vomiting', 'Headache')
)
## Formatted text of the subject narrative:
text <- sprintf('The subject %s entered study %s and discontinued the study %s with a reason "%s".
During the treatment period subject had the following %d AEs:
- %s',
nartv[["subj"]],
nartv[["enrldt"]],
nartv[["compldt"]],
nartv[["discreas"]],
nartv[["nae"]],
paste(nartv[["listae"]], collapse = '
- ')
)
```
```{r save_min_text, eval = FALSE}
# End-to-end: render the narrative report to DOCX
spec_min_text <- create_text() |> add_title("Sample Narrative Text")
spec_min_text <- add_body_text(spec_min_text, text)
rpt_min_text <- create_report(spec_min_text)
write_doc(rpt_min_text, name = "nar_min")
```
Rendered output:

## 4 — Define columns: single, batch, and parameter recycling
### Why `define_cols()`?
After creating a table with `create_table()`, column properties are
auto-detected from the data. However, you often need to: - Customize
column labels for readability - Specify numeric formats (e.g., "%.2f"
for 2 decimal places) - Control visibility, styling, or special
behaviors (ID column, deduplicate, page breaks) - Lock or adjust column
widths for better layout
`define_cols()` is the primary tool for these customizations. It
supports both **single-column** and **batch updates**, with intelligent
**parameter recycling** to keep code concise.
### Example 1: Single column definition
The simplest case — modify one column at a time:
```{r example_define_single, eval = FALSE}
# Define one column
spec_single <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
spec_single <- define_cols(spec_single, subject_id, label = "Subject ID", isID = TRUE)
# Another single column call (chaining is encouraged)
spec_single <- define_cols(spec_single, age,
label = "Age (years)",
type = "numeric",
format = "%.0f")
print(spec_single)
# close example chunk
```
### Example 2: Batch update with single value (recycling to all columns)
Update multiple columns with the **same value** — the value is
automatically recycled:
```{r example_define_batch_single, eval = FALSE}
# Apply single format to multiple columns
spec_batch <- create_table(data = vitals_tbl, cols = c(sbp, dbp))
spec_batch <- define_cols(spec_batch, c(sbp, dbp),
type = "numeric", # Applied to both
format = "%.1f", # Applied to both
valueStyleRef = "b") # Applied to both
print(spec_batch)
# close example chunk
```
**Why this matters**: Instead of writing three separate `define_cols()`
calls, you write one line and the package applies the same values to all
selected columns.
### Example 3: Batch update with per-column values (1-to-N mapping)
Update multiple columns with **different values** — provide a vector
matching the number of columns:
```{r example_define_batch_mapped, eval = FALSE}
# Different label and format for each column
spec_mapped <- create_table(data = demog_tbl, cols = c(age, sex, trt)) |>
define_cols(c(age, sex, trt),
label = c("Age (years)", "Biological Sex", "Treatment Group"),
type = c("numeric", "string", "string"),
format = c("%.0f", NA, NA) #format not applicable for strings -> NA
)
print(spec_mapped)
# close example chunk
```
**Important**: The vector length must match exactly:
\- Length 1: applies to all columns
\- Length N (where N = number of columns): applies one-to-one
\- Any other length: raises an error
### Example 4: Multiple `define_cols()` calls (chaining with `|>`)
Combine `define_cols()` calls to layer customizations — each call merges
with previous settings (last-win strategy):
```{r example_define_chain, eval = FALSE}
# Start with basic table
spec_chain <-
create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt)) |>
# First pass: set all labels
define_cols(c(subject_id, age, sex, trt),
label = c("Subject ID", "Age", "Sex", "Treatment")) |>
# Second pass: set numeric formatting for age
define_cols(age, type = "numeric", format = "%.0f") |>
# Third pass: mark subject_id as ID column (repeats on page breaks)
define_cols(subject_id, isID = TRUE)
print(spec_chain)
# close example chunk
```
This approach makes it easy to:
\- Define labels first (human-readable column names)
\- Then apply formatting (numeric/string types, decimals)
\- Then apply special behavior flags (ID, deduplicate, etc.)
------------------------------------------------------------------------
## 5 — Set document properties (hasData, content width, placement)
**Key function**: `set_document()`
- `hasData`: Whether document contains data (important for Text specs)
\- `footnotePlace`: Control placement of titles, footnotes, and
subtitles
\- `contentWidth`: Content width (e.g., "100%", "6.5in", "16.51cm")
\- `isContinues`: Ignore page breaks between sections
**Example 1: hasData flag**
```{r example_set_document_prefix, eval = FALSE}
spec <- create_table(demog_tbl) #automatically detects if dataframe has any rows
# When data frame does not have any rows, the report will show the default body_text placeholder instead of empty table.
#body text can be manually specified
spec <- set_document(spec,
hasData = FALSE) #override autodetected value
print(spec)
```
**Example 2: Content width and placement**
```{r example_set_document_width, eval = FALSE}
spec <- create_table(demog_tbl)
spec <- set_document(spec,
contentWidth = "95%", # Narrower content (default 100%)
footnotePlace = "repeated") # Footnotes on every page
```
**Notes**:
\- Most defaults are sensible; you typically only need `set_document()`
for content width
\- Multiple calls merge with last-win strategy (later calls override
earlier ones)
------------------------------------------------------------------------
## 6 — Combine table/figure/text into a single report
```{r example_combine_report, eval = FALSE}
report_simple <- create_report(spec_min_table, spec_min_fig, spec_min_text)
```
```{r save_report_simple, eval = FALSE}
# Render combined simple report to DOCX
write_doc(report_simple, name = "report_simple")
```
Notes: `create_report()` preserves input order and consolidates styles.
Additionally, with `toc = TRUE` parameter the table of contents can be
generated [in order this to work the titles in the input specs should be
marked for toc entries - see `?add_title`]
### Passing a named list of specs
When specs are built independently (e.g. in a loop, across separate program
files, or inside a function that returns a list), they can be collected into a
named list and passed as a single argument:
```{r example_list_of_specs, eval = FALSE}
specs <- list(
t_dm = spec_min_table,
t_fig = spec_min_fig,
t_text = spec_min_text
)
# Equivalent to create_report(spec_min_table, spec_min_fig, spec_min_text)
# but the list can be assembled dynamically.
report_from_list <- create_report(specs)
# List names become key prefixes:
# names(report_from_list)
# [1] "t_dm_" "t_fig_" "t_text_"
```
List arguments may be freely mixed with variadic specs and reports:
```{r example_mixed_args, eval = FALSE}
extra_spec <- create_text() |> add_body_text("Appendix note.")
report_mixed <- create_report(extra_spec, specs)
```
Unnamed list elements fall back to `_` (e.g. `specs_1`,
`specs_2`, …). `TFL_report` objects inside a list are flattened with their
original keys preserved.
------------------------------------------------------------------------
## 7 — Column widths: automatic calculation and locking
### How automatic column width works
When you create a table with `create_table()`, ksTFL automatically
analyzes the data and distributes column widths proportionally:
1. **Initial analysis**: For each column, the package examines data
values (length, type, number of decimals)
2. **Width calculation**: Numeric columns typically get wider if they
have many decimals or large values; short text columns get narrower
3. **Proportional distribution**: All widths are normalized to sum to
100% (or remaining available width)
4. **Default**: `autoColWidth = TRUE` in package options (can be
changed with `tfl_set_options()`)
**Example**: A table with columns `id` (short numeric), `description`
(long text), `value` (numeric): - Auto-detected widths might be:
`id = 20%`, `description = 50%`, `value = 30%`
### Example 1: Accept auto-calculated widths
The simplest approach — let the package handle width distribution:
```{r example_colwidth_auto, eval = FALSE}
# Create table; widths are auto-calculated from data characteristics
spec_auto <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
spec_auto <- define_cols(spec_auto, c(subject_id, ALT, AST),
label = c("Subject ID", "ALT (U/L)", "AST (U/L)"))
# Print to see auto-calculated widths
print(spec_auto)
```

### Example 2: Lock one column, auto-adjust others
Lock a specific column width while others recalculate to fill remaining
space:
```{r example_colwidth_lock_one, eval = FALSE}
# Lock subject_id at 15%, let ALT and AST split the remaining 85%
spec_lock1 <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
spec_lock1 <- define_cols(spec_lock1, subject_id,
label = "Subject ID",
colWidth = "15%") # Lock at 15%
# When auto-recalculation runs (automatic), ALT and AST widths are recalculated
# to maintain their initial proportion while filling the remaining 85%
print(spec_lock1)
```

### Example 3: Lock multiple columns with relative widths
Lock several columns and let others auto-adjust:
```{r example_colwidth_lock_multiple, eval = FALSE}
# Lock two columns, let the third auto-adjust
spec_lock_multi <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
spec_lock_multi <- define_cols(spec_lock_multi, subject_id,
label = "Subject ID",
colWidth = "12%")
spec_lock_multi <- define_cols(spec_lock_multi, ALT,
label = "ALT (U/L)",
colWidth = "40%")
# AST automatically fills remaining 48% (100% - 12% - 40%)
print(spec_lock_multi)
```

### Example 4: Mixed units (percentages and absolute)
Combine percentage-based widths with absolute units:
```{r example_colwidth_mixed_units, eval = FALSE}
# Lock subject_id at 2 cm, others in percentages
spec_mixed <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
spec_mixed <- define_cols(spec_mixed, subject_id,
label = "Subject ID",
colWidth = "2cm") # Absolute width
spec_mixed <- define_cols(spec_mixed, ALT,
label = "ALT (U/L)",
colWidth = "35%") # Percentage of remaining
print(spec_mixed)
```

**Important**: When mixing units (cm, in, pt) with percentages, **the
absolute widths are reserved first**, then percentages are calculated
from remaining space.
### Example 5: Width validation and constraints
The package validates column widths to prevent invalid configurations:
```{r example_colwidth_validation, eval = FALSE}
spec_valid <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
# VALID: Set a reasonable relative width
spec_valid <- define_cols(spec_valid, ALT,
colWidth = "30%") # OK: 30% is valid
# INVALID: Cannot set 100% relative width (leaves no space for other columns)
# This will raise an error:
# spec_valid <- define_cols(spec_valid, ALT, colWidth = "100%")
# Error: relative width 100.0% exceeds maximum allowed 75.0%
# (accounting for locked columns and minimum 0.5% for other columns)
# INVALID: Cannot set width below minimum threshold
# spec_valid <- define_cols(spec_valid, AST, colWidth = "0.2%")
# Error: relative width 0.2% is below minimum 0.5%
# Minimum width constraints (can be customized via package options):
# - Relative widths (%): minimum 0.5%
# - Fixed widths (cm, in, pt): minimum 0.2cm (~2mm, ~0.08in)
# Configure minimum width via package options:
tfl_set_options(minColWidth = 1.0) # Set minimum relative width to 1.0%
# Now 0.5% will fail, but 1.0% will succeed
spec_valid <- define_cols(spec_valid, AST,
colWidth = "1.0%") # OK with new minimum
print(spec_valid)
```
**Width validation rules**:
\- **Relative width (%)**: Must be ≥ `minColWidth` (default 0.5%)
\- **Relative width (%)**: Cannot exceed max allowed considering locked
columns and other column minimums
\- **Fixed width**: Must be ≥ 0.2cm (all units: cm, in, pt, mm
automatically converted)
\- **Incompatible widths**: Cannot set width to 100% (would exclude all
other columns)
**Error messages** are detailed and show the maximum allowed width when
you exceed limits:
```
Error: relative width 100.0% exceeds maximum allowed 75.0%
(accounting for 2 locked columns requiring 25% total,
and minimum 0.5% for remaining 1 unlocked column)
```
------------------------------------------------------------------------
## 8 — Table with titles, subtitles and footnotes
```{r example_multilevel_table, eval = FALSE}
spec_multi <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
spec_multi <- add_title(spec_multi, "Laboratory Results")
spec_multi <- add_subtitle(spec_multi, "Selected hepatic enzymes by subject")
spec_multi <- add_footnote(spec_multi, "Values are shown as observed.")
spec_multi <- define_cols(
spec_multi,
c(subject_id, ALT, AST),
label = c("Subject ID", "ALT (U/L)", "AST (U/L)")
)
print(spec_multi)
# close example chunk
```
```{r save_multilevel, eval = FALSE}
# Render the multilevel table to DOCX
rpt_multi <- create_report(spec_multi)
write_doc(rpt_multi, name = "tbl_multi")
```
Rendered output:

## 9 — span columns (spanning headers)
### Why spanning headers?
In clinical tables, you often need to group related columns under a
common header. For example: - "Baseline" spanning columns: `visit`,
`sbp`, `dbp`, `pulse` - "Week 12" spanning columns: `sbp`, `dbp`,
`pulse` (repeated measurements at different visits)
Spanning headers (called "spans" in ksTFL) are separate from column
labels — they sit above the column labels and group multiple columns.
Multiple spans can be stacked at different vertical levels.
### Example 1: Single spanning header
Create one stub that groups related columns:
```{r example_stubs_single, eval = FALSE}
# Start with demographics table
spec_span_simple <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
spec_span_simple <- define_cols(spec_span_simple,
c(subject_id, age, sex, trt),
label = c("Subject","Age","Sex","Treatment"))
# Add a spanning header for "Demographics" above age and sex
spec_span_simple <- add_span_header(spec_span_simple,
cols = c("age", "sex"),
label = "Demographics")
print(spec_span_simple)
```
**Result**: A table with column labels on one row and a "Demographics"
header spanning age+sex columns above it:\

### Example 1b: Using tidyselect helpers with spanning headers
`add_span_header()` supports all tidyselect expressions — use helpers
for flexible column selection:
```{r example_stubs_tidyselect, eval = FALSE}
# Table with mixed column types
mixed_data <- data.frame(
id = 1:10,
age_baseline = rnorm(10, 45, 10),
age_follow = rnorm(10, 46, 10),
weight_baseline = rnorm(10, 70, 10),
weight_follow = rnorm(10, 71, 10)
)
spec_tidysel <- create_table(mixed_data)
# Using starts_with() helper
spec_tidysel <- add_span_header(spec_tidysel,
cols = starts_with("age"),
label = "Age Measurements",
stubOrder = 1) |>
# Using col position
add_span_header(cols = c(4,5),
label = "Weight Measurements",
stubOrder = 1) |>
# Using negation (-) to exclude columns
add_span_header(cols = -id,
label = "Baseline and Follow",
stubOrder = 2)
print(spec_tidysel)
```

**Tidyselect expressions supported**:
\- **Ranges**: `cols = age:weight` (all columns between age and weight)
\- **Helpers**: `cols = starts_with("age")`, `contains("baseline")`,
`matches("^w")`
\- **Negation**: `cols = -id` (all columns except id)
\- **Combinations**: `cols = c(starts_with("age"), weight_baseline)`
### Example 2: Styled spanning headers
Apply styles to stub labels using `labelStyleRef`:
```{r example_stubs_styled, eval = FALSE}
# First, create a style for stub labels
spec_spans_style <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
spec_spans_style <- add_style(spec_spans_style,
id = "span_header",
s_font(bold = TRUE, font_size = "12pt"),
s_paragraph(alignment = "center"),
s_table_style(background_color = "#E8E8E8"))
# Define columns
spec_spans_style <- define_cols(spec_spans_style,
c(subject_id, age, sex, trt),
label = c("Subject ID", "Age", "Sex", "Treatment"),
valueStyleRef = 'ac')
# Add span with style reference
spec_spans_style <- add_span_header(spec_spans_style,
cols = c("age", "sex"),
label = "Demographics",
labelStyleRef = "span_header")
```

Notes:
\- `labelStyleRef` can be a single style name or multiple styles
combined with `f_combine()`
\- Span labels inherit the applied style, making grouped columns
visually distinct
\- Styles must be defined before referencing them in `add_span_header()`
### Example 3: Paragraph borders on spanning headers
When a cell border is applied to a spanning header, it stretches across
the entire merged cell. A paragraph border instead follows the text
width — useful for visually separating groups without a full-width line.
Paragraph borders are also unaffected by structural border overrides
(`header_top_border`, `header_bottom_border`), giving full control on
intermediate header rows.
```{r example_span_para_border, eval = FALSE}
spec_para <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
# Paragraph border style — the underline follows the text, not the cell edge
spec_para <- add_style(spec_para, id = "span_underline",
s_font(bold = TRUE),
s_paragraph(
alignment = "center",
borders = s_borders(
bottom = s_border(color = "#000000", width = "0.5pt", line_style = "single")
)
)
)
# Or use the built-in paragraph border atom
spec_para <- add_span_header(spec_para,
cols = c("age", "sex"),
label = "Demographics",
labelStyleRef = f_combine("b", "ac", "pb_th") # bold + center + thin paragraph bottom border
)
```
```{r save_stub, eval = FALSE}
# Render stubbed table report to DOCX
rpt_stub <- create_report(spec_stubs_style)
write_doc(rpt_stub, name = "tbl_stub", outDir = "./out", metaPath = tempdir())
```
------------------------------------------------------------------------
## 10 — Styles and `f_combine()` for combined style references
### Why named styles?
Styles are foundational in professional reporting:
\- Define once, reference many times — consistency across your document
\- Easy to update: change one style definition and all references
automatically pick up the change
\- Composable: combine base styles (bold, red text) into complex styles
for specific use cases
ksTFL uses a **named style system**: you define styles with
`add_style()` giving each an `id`, then reference them by name wherever
you need them (`labelStyleRef`, `valueStyleRef`, `labelStyleRef` in
stubs, etc.).
### Example 1: Define base styles
Create atomic styles that focus on one aspect (font, alignment, color):
```{r example_styles_base, eval = FALSE}
# Create a table spec
spec_base_styles <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
# Define reusable base styles
spec_base_styles <- add_style(spec_base_styles, id = "bold_header",
s_font(bold = TRUE, font_size = "12pt"))
spec_base_styles <- add_style(spec_base_styles, id = "right_align",
s_paragraph(alignment = "right"))
spec_base_styles <- add_style(spec_base_styles, id = "light_gray_bg",
s_table_style(background_color = "#F5F5F5"))
# Apply to columns
spec_base_styles <- define_cols(spec_base_styles,
c(subject_id, age, sex, trt),
label = c("Subject ID", "Age", "Sex", "Treatment"),
labelStyleRef = c("bold_header", "", "", ""))
print(spec_base_styles)
# close example chunk
```
### Example 2: Combine styles with `f_combine()`
Apply multiple styles to a single element using `f_combine()` — they
merge at render time:
```{r example_styles_fcombine, eval = FALSE}
# Create spec and define base styles
spec_combined <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
# Define atomic styles
spec_combined <- add_style(spec_combined, id = "bold_text", s_font(bold = TRUE))
spec_combined <- add_style(spec_combined, id = "red_color", s_font(color = "#CC0000"))
spec_combined <- add_style(spec_combined, id = "centered", s_paragraph(alignment = "center"))
# Combine styles: bold + red text + centered
spec_combined <- define_cols(spec_combined,
subject_id,
label = "Subject ID",
labelStyleRef = f_combine("bold_text", "red_color", "centered")) |>
define_cols(c(sex, trt),
label=c('Sex', 'Treatment'),
labelStyleRef = f_combine('b','i'),
valueStyleRef = f_combine('ar', 'i')
) |>
# Different columns can use different combinations
define_cols(age, label = "Age",
labelStyleRef = f_combine("bold_text", "centered"),
valueStyleRef = 'ac')
```

**How `f_combine()` works**:
\- Takes multiple style names as arguments
\- Returns a reference object that tells the package to merge those
styles
\- Order matters for last-win conflict resolution (later arguments
override earlier ones)
## 11 — Conditional row actions with `compute_cols()`
**Overview**: While `define_cols()` sets properties globally for all
rows, `compute_cols()` applies **conditional actions** to specific rows
matching a condition.
Common use cases:
\- Style rows where a specific value occurs (e.g., first/last
occurrence, threshold-based)
\- Merge columns in certain rows (e.g., group headers)
\- Insert separator or summary rows programmatically
### Example 1: Conditional styling
Apply a style to columns in rows matching a condition:
```{r compute_basic_style, eval = FALSE}
# Sample data with groups
data <- data.frame(
group = c("A", "A", "B", "B", "C"),
metric = c("Value 1", "Value 2", "Value 3", "Value 4", "Value 5"),
count = c(10, 20, 15, 25, 30)
)
# Create spec and define styles
spec <- create_table(data) |>
add_style(id = "group_header", s_font(bold = TRUE, color = "#0000FF")) |>
add_style(id = "emphasize", s_table_style(background_color = "#FFFFCC"))
# Apply conditional styling: bold+blue for first occurrence of each group
spec <- spec |>
compute_cols(firstOf(group), c_style(c(metric, count), styleRef = "group_header"))
# Apply conditional styling: highlight rows with high count
spec <- spec |>
compute_cols(count > 20, c_style(count, styleRef = "emphasize"))
```

### Example 2: Combining styles in conditional rows
Apply multiple styles to a single column using `f_combine()`:
```{r compute_combined_style, eval = FALSE}
spec <- create_table(data) |>
add_style(id = "bold", s_font(bold = TRUE)) |>
add_style(id = "large", s_font(font_size = "14pt")) |>
add_style(id = "highlight", s_table_style(background_color = "#FFFF00"))
# Apply combined styles to first group occurrence
spec <- spec |>
compute_cols(firstOf(group),
c_style(metric, styleRef = f_combine("bold", "large", "highlight"))
)
```

### Example 3: Column merging in conditional rows
Merge adjacent columns for rows matching a condition:
```{r compute_merge, eval = FALSE}
spec <- create_table(data) |>
add_style(id = "group_label", s_table_style(background_color = "#D9D9D9"))
# Merge metric and count columns for first occurrence (group header style)
spec <- spec |>
compute_cols(firstOf(group),
c_merge(c(metric, count), styleRef = "group_label")
)
```

### Example 4: Inserting separator rows
Insert empty rows (separators) or rows with content from another column:
```{r compute_addrow, eval = FALSE}
spec <- create_table(data) |>
add_style(id = "separator", s_table_style(background_color = "#E8E8E8"))
# Insert empty separator above first group occurrence
spec <- spec |>
compute_cols(firstOf(group),
c_addrow(pos = "above") # Empty row, no value_from
)
# Insert summary row below last group occurrence (using a specific column as content)
spec <- spec |>
compute_cols(lastOf(group),
c_addrow(pos = "below", value_from = "group", styleRef = "separator")
)
```

### Example 4b: Page break insertion
Force a page break when a group ends (useful for long groups):
```{r compute_pagebreak, eval = FALSE}
spec <- spec |>
compute_cols(lastOf(group),
c_pageBreak()
)
```
### Example 5: Multiple actions on same rows
Combine styling, merging, and row insertion in a single `compute_cols()`
call:
```{r compute_multi_action, eval = FALSE}
spec <- create_table(data) |>
add_style(id = "header", s_font(bold = TRUE)) |>
add_style(id = "separator", s_table_style(background_color = "#E8E8E8"))
# For first group occurrence: add separator, style, and merge
spec <- spec |>
compute_cols(firstOf(group),
c_addrow(pos = "above"), # Add empty separator
c_style(metric, styleRef = "header"), # Style metric column
c_merge(c(metric, count)) # Merge adjacent columns
)
```

**Key concepts**:
\- **Conditions** are unevaluated expressions evaluated at report
generation time (during `create_report()`)
\- **Helper functions** (`firstOf()`, `lastOf()`, `firstRow()`,
`lastRow()`, `rowNumber()`, `everyNth()`, `firstOfBlock()`) are only
available inside `compute_cols()` conditions — they are **not**
standalone exported functions. See `vignette("Advanced_StyleRows")` for
full details.
\- **Multiple calls accumulate**: calling `compute_cols()` multiple
times on the same spec appends actions
\- **Multiple actions in one call**: same row can have styling, merging,
and row insertion simultaneously
\- **Overlapping conditions**: if multiple conditions match the same
row, all actions apply (styling aggregates, merging rules apply) -
**value_from**: Optional in `c_addrow()` — omit for empty separator rows
\- **Performance**: Conditions evaluated once per row during report
assembly; style consolidation happens automatically
------------------------------------------------------------------------
## 12 — Render to DOCX with `write_doc()`
### What `write_doc()` does
`write_doc()` is the primary output function. It combines two
lower-level steps into one:
1. **Validates and serializes** the report to JSON, writing metadata
and data files to `metaPath`
2. **Renders** the JSON to a DOCX file in `outDir`
```
create_report() → write_doc() → output.docx
```
### Example: Assemble and render a multi-spec report
> **Tip**: Use `tfl_list_templates()` to discover available template
> names (e.g., `"Navy_Pro"`, `"Carbon_Dark"`). Use `run_styles_editor()`
> to interactively preview and customize templates.
```{r example_write_doc, eval = FALSE}
# Create multiple specs
table_spec <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
table_spec <- add_title(table_spec, "Laboratory Results")
table_spec <- set_page_style(table_spec, docTemplate = "Navy_Pro")
text_spec <- create_text()
text_spec <- add_body_text(text_spec, "All values are from the locked database.")
text_spec <- set_page_style(text_spec, docTemplate = "Carbon_Dark")
# Assemble into report
report_full <- create_report(table_spec, text_spec)
# A) Per-spec templates (default): each spec uses its own docTemplate
write_doc(report_full, name = "example_report", outDir = "./out", metaPath = tempdir())
# B) Global override: force one template for all specs
write_doc(
report_full,
name = "example_report_global_override",
outDir = "./out",
metaPath = tempdir(),
overrideTemplate = system.file("templates", "Navy_Pro.json", package = "ksTFL", mustWork = TRUE)
)
```
In this workflow:
- `write_doc(..., overrideTemplate = NULL)` keeps per-spec
`docTemplate` styling.
- `write_doc(..., overrideTemplate = )` applies a single
global template to all specs.
**Key parameters**:
- `report`: A `TFL_report` object created via `create_report()`.
- `name`: Base name for the output DOCX (`"example_report"` becomes
`example_report.docx`).
- `outDir`: Directory where the output DOCX is written.
- `metaPath`: Directory where intermediate JSON and data files are written.
**Advanced**: If you need to inspect the JSON before rendering (e.g. for
debugging or CI pipelines), you can split the call into
`save_report()` + `replay_report()`. See `?write_doc` and
`?replay_report` for details.
------------------------------------------------------------------------
## 13 — Session-wide `tfl_options`: common headers/footers and default body text
### Why session options?
In a real clinical reporting workflow, many tables/listings share: -
Common headers (study name, database version, confidentiality notice) -
Common footers (company name, page numbers, disclaimers) - Default body
text (standard disclaimers or methodology notes)
Instead of adding these to every spec, use `tfl_set_options()` to set
session defaults once. All specs created afterward inherit these
settings.
### Example 1: Set basic session options
```{r example_session_options_basic, eval = FALSE}
# Set session defaults (applies to all NEW specs created after this call)
tfl_set_options(
add_header("Study ABC", "Phase II Safety Study", "CONFIDENTIAL"),
add_footer("Company Confidential", "Page {PAGE} of {NUMPAGES}")
)
# Create spec — automatically inherits headers/footers from options
spec_with_opts <- create_table(data = demog_tbl, cols = c(subject_id, age, sex, trt))
spec_with_opts <- add_title(spec_with_opts, "Demographics Table")
print(spec_with_opts)
# close example chunk
```
**Key point**: Headers and footers from `tfl_set_options()` are
automatically applied to new specs. No need to call `add_header()`
again.
### Example 2: Override session options in a specific spec
You can override session defaults on individual specs:
```{r example_session_options_override, eval = FALSE}
# Session options are still in effect from previous example
# Create a spec with session defaults
spec_default <- create_table(data = labs_tbl, cols = c(subject_id, ALT, AST))
spec_default <- add_title(spec_default, "Lab Results (using session defaults)")
# Create another spec but override the header
spec_override <- create_table(data = demog_tbl, cols = c(subject_id, age))
spec_override <- add_header(spec_override, "Study XYZ", "Different Study", "CONFIDENTIAL") # Overrides session default
spec_override <- add_title(spec_override, "Demographics (custom header)")
print(spec_default) # Uses session header from tfl_set_options
print(spec_override) # Uses custom header from add_header call
# close example chunk
```
**Rule**: Spec-level settings always take precedence over session
defaults (last-win strategy).
### Example 3: Check and reset session options
Inspect current session options and reset to defaults:
```{r example_session_options_inspect, eval = FALSE}
# Check current options
current_options <- tfl_get_options()
str(current_options)
# Get a single option
current_missings <- tfl_get_option("missings")
cat("Current missings representation:", current_missings, "\n")
# Reset to package defaults
tfl_reset_options()
# Now new specs will use package defaults instead of your custom session options
spec_reset <- create_table(data = demog_tbl, cols = c(subject_id, age))
print(spec_reset)
# close example chunk
```
------------------------------------------------------------------------
## Final notes
This vignette stays deliberately example-heavy. Reuse it when you want a compact
pattern for a minimal spec, a multi-spec report, span headers, width tuning,
or session-level defaults.
**Running examples locally**: Set the top chunk `eval=TRUE` to generate
sample data, then execute examples in order. All code uses exported
functions only — no internal API manipulation needed.
For more details on function parameters, see the roxygen documentation:
`?create_table`, `?define_cols`, `?add_style`, `?write_doc`, etc.