Guidance for AI agents (and humans) working in this repository. For the full contributor policy, defer to .github/CONTRIBUTING.md; this file summarizes the conventions an agent needs to make correct changes.
Project overview
gtsummary is an R package for presentation-ready summary and analytic result tables. It builds tidy summary objects (tbl_summary(), tbl_regression(), etc.) that print to several engines (as_gt(), as_flex_table(), as_kable_extra(), …). Requires R (>= 4.2).
Repository layout
-
R/— package source. S3 methods are split one-per-file by table type (e.g.add_p.tbl_summary.R,add_p.tbl_svysummary.R). Add a newmethod.tbl_type.Rfile rather than branching inside one function. -
R/import-standalone-*.R— vendored helpers; do not edit by hand (see below). -
tests/testthat/—test-*.Rfiles plus snapshot output in_snaps/. -
man/— generated by roxygen2; never edit.Rdfiles directly. -
vignettes/,pkgdown/— docs/website. -
data-raw/— scripts that buildR/sysdata.rdaanddata/. -
NEWS.md,DESCRIPTION,NAMESPACE— package metadata (NAMESPACE is generated).
Build, document & test
# regenerate man/*.Rd and NAMESPACE after changing roxygen or exports
Rscript -e 'devtools::document()'
# run the full test suite
Rscript -e 'devtools::test()'
# run a single test file
Rscript -e 'devtools::load_all(); testthat::test_file("tests/testthat/test-modify_source_note.R")'
# accept intended snapshot changes
Rscript -e 'testthat::snapshot_accept("modify_source_note")'Notes: - Many tests use skip_on_cran(). Set NOT_CRAN=true (or run via devtools::test()) to exercise them. - Some suites need optional Suggests packages (e.g. broom.helpers, survey, flextable, officer (Word export via save_flex_docx()), cardx); they skip if the package is absent. - testthat edition 3, parallel enabled. Dev container image: ghcr.io/rocker-org/devcontainer/tidyverse:4.
Theme elements / internal data
The set of valid theme elements (used by set_gtsummary_theme() / with_gtsummary_theme()) is data-driven, not hand-coded. It lives in data-raw/gtsummary_theme_elements.csv and is baked into the package as the df_theme_elements object inside R/sysdata.rda.
CSV columns: deprecated (logical), fn (the function the element applies to), name (the theme-element name users set, e.g. tbl_summary-arg:missing_text), argument (logical — is it a function argument), eval (logical), desc (description), example.
To add, update, or delete a theme element:
Edit
data-raw/gtsummary_theme_elements.csv(add/modify/remove the row).-
Regenerate
R/sysdata.rdaby sourcing the build script:This re-reads the CSV (plus the tests/translations data) and rewrites
R/sysdata.rdaviausethis::use_data(..., internal = TRUE, overwrite = TRUE). Run it from the repo root so the relative CSV paths resolve. Commit both the edited CSV and the regenerated
R/sysdata.rda.Update the corresponding docs/vignette (
vignettes/articles/themes.Rmd) and any affected code inR/if you added/removed an element name.
Never edit R/sysdata.rda by hand — it is generated. The same script also rebuilds df_add_p_tests, lst_translations, and special_char from their respective data-raw/ sources, so expect those to be re-serialized too.
Conventions
- Follow the tidyverse style guide. Use
styler, but don’t restyle unrelated code. - Edit roxygen in
.Rfiles (markdown syntax enabled), then rundevtools::document(). Never edit generatedman/*.Rd. - Add a
NEWS.mdbullet under# gtsummary (development version)for any user-facing change, referencing the issue/PR number, e.g.(#1987). - New code ships with tests. Place them in the existing
test-*.Rfile for the function being changed rather than creating a new standalone test file. - File an issue before substantial PRs; PRs are merged with “Squash and merge”.
Namespacing / imports
-
rlangis imported in full (@import rlanginR/gtsummary-package.R), so its functions are called bare — norlang::prefix: e.g.set_cli_abort_call(),arg_match(),enquo(),expr(),inject(),.data,%||%. -
All other packages are called namespaced as
pkg::fn():dplyr::mutate(),gt::md(),glue::glue(), etc. (dplyr/glue/utils are selectively re-exported via@importFrom; everything else uses::).
Standalone (vendored) files
R/import-standalone-*.R are copies of tidyverse-style helpers (the rlang/usethis “standalone” pattern) that let gtsummary use familiar idioms without taking the dependency. They provide the check_*() input validators and map()/imap(), str_*(), fct_*(), and tibble helpers used throughout the codebase.
-
Do not hand-edit them — each starts with
# Standalone file: do not edit by hand. - Refresh/add one with the command in its header, e.g.
usethis::use_standalone("insightsengineering/standalone", "checks"). - Upstream sources:
insightsengineering/standalone(checks, check_pkg_installed, cli_call_env, forcats, stringr, tibble) andr-lib/rlang(purrr). Fix bugs upstream, not here.
Code patterns
Exported functions follow this skeleton — note set_cli_abort_call() first, the check_*() validators, and the call_list bookkeeping around the return:
modify_source_note <- function(x, source_note, text_interpret = c("md", "html")) {
set_cli_abort_call()
updated_call_list <- c(x$call_list, list(modify_source_note = match.call()))
# check inputs
check_not_missing(x)
check_not_missing(source_note)
check_class(x, "gtsummary")
check_string(source_note)
text_interpret <- arg_match(text_interpret, error_call = get_cli_abort_call())
# ... modify x$table_body / x$table_styling ...
# return table
x$call_list <- updated_call_list
x
}Key object structure: x$table_body, x$table_styling (header, footnotes, source notes, etc.), x$call_list, x$inputs.
Performance-critical paths
Table-assembly and styling internals (the brdg_*() bridges, the modify_*()/bold_*() styling functions, tbl_stack()/tbl_merge()/ add_overall(), the sort_hierarchical()/filter_hierarchical() helpers, and the as_*() converters) were heavily optimized in the 2.6.0 cycle. When editing these, preserve the established patterns: prefer vectorized map()/base-R subsetting over dplyr::rowwise() and per-row/per-table dplyr pipelines in hot loops; skip work that is a no-op (e.g. literal-NULL/TRUE row selections, constant glue::glue() text); and compute shared values (visible-column sets, parsed interpreters, data masks) once rather than per row/column/table. These refactors are guaranteed to be output-preserving (“no change to the returned tables”), and the snapshot tests in tests/testthat/_snaps/ are what enforce that — do not accept snapshot changes for a change advertised as performance-only.
Pull requests
- Honor
.github/PULL_REQUEST_TEMPLATE.md;.github/CONTRIBUTING.mdis the source of truth. -
Performance-sensitive PRs: if a change may affect package performance, begin the PR title with
perf. The “Performance Benchmark” workflow (.github/workflows/benchmark.yaml) only runs when the PR title starts withperf; it runs.github/scripts/benchmark.R(in a memory-profilingrocker/r-vercontainer) to compare PR-vs-maintime and memory across the full set of hot paths — thestyle_*()functions, thetbl_summary()/tbl_hierarchical()/tbl_strata()pipelines, thebrdg_*()assembly steps,add_overall(),tbl_merge(),tbl_stack(), themodify_*()family, thesort_hierarchical()/filter_hierarchical()helpers, and theas_*()converters — and posts a benchmark report as a PR comment. Add a newbench::mark()block there when you add a hot path. Longer-term trends across releases are tracked in ddsjoberg/tracking-gtsummary-cards-efficiency.
