--- title: "Exporting Clinical Tables with derrick" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Exporting Clinical Tables with derrick} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} derrick_examples_available <- requireNamespace("derrick", quietly = TRUE) && requireNamespace("dplyr", quietly = TRUE) && requireNamespace("gtsummary", quietly = TRUE) && requireNamespace("reporter", quietly = TRUE) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, eval = derrick_examples_available ) output_dir <- file.path(tempdir(), "derrick-vignette-outputs") dir.create(output_dir, showWarnings = FALSE, recursive = TRUE) path_for_ext <- function(paths, ext) { hit <- paths[tolower(tools::file_ext(paths)) == tolower(ext)] if (length(hit) == 0L) return(NA_character_) unname(hit[[1]]) } escape_html_attr <- function(x) { x <- gsub("&", "&", x, fixed = TRUE) x <- gsub("<", "<", x, fixed = TRUE) x <- gsub(">", ">", x, fixed = TRUE) x <- gsub("\"", """, x, fixed = TRUE) x } print_text_output <- function(path) { if (is.na(path) || !file.exists(path)) { cat("TXT output was not generated in this environment.\n") return(invisible(NULL)) } text <- readChar(path, nchars = file.info(path)$size[[1]], useBytes = TRUE) text <- gsub("\r\n", "\n", text, fixed = TRUE) text <- gsub("\r", "\n", text, fixed = TRUE) cat(text) } embed_html_output <- function(path, height = 420) { if (is.na(path) || !file.exists(path)) { cat("> HTML output was not generated in this environment.\n") return(invisible(NULL)) } html_doc <- paste(readLines(path, warn = FALSE), collapse = "\n") cat( '', sep = "" ) } output_manifest <- function(paths) { data.frame( format = toupper(tools::file_ext(paths)), file = basename(paths), bytes = unname(file.info(paths)$size), row.names = NULL ) } ``` ```{r availability-note, echo = FALSE, results = "asis", eval = !derrick_examples_available} cat( "> The executable examples in this vignette require derrick, dplyr, ", "gtsummary, and reporter. Install the suggested packages to run the ", "workflow end to end.\n", sep = "" ) ``` ## Overview `derrick` converts analysis-ready clinical tables into files written by `reporter`. It accepts either a `gtsummary` object or an already assembled plain data frame, then applies report metadata, column labels, spanning headers, widths, pagination controls, and output-format selection in one call to `derrick::gtsummary_reporter()`. ## Relationship to reporter `derrick` uses `reporter` as the rendering engine. The examples in this vignette continue to rely on `reporter` for: - report setup, including paper size, orientation, units, margins, font, and font size; - table rendering, including column definitions, column widths, alignment, borders, header styling, and spanning headers; - titles, footnotes, page footers, automatic pagination, and file writing; - output writers for RTF, TXT, DOCX, PDF, and HTML. The main difference is the level of abstraction. With `reporter`, the caller usually builds the report and table specification directly. `derrick` builds that specification from a `gtsummary` object or a plain data frame, then passes it to `reporter`. `derrick` adds a clinical-table workflow layer around `reporter`: - it reads displayed columns, labels, indentation, p-values, and spanning headers from `gtsummary` objects; - it lets plain data frames use the same export path by supplying `column_labels` and `spanning_headers`; - it centralizes clinical report metadata through `title1`, `footnote1`, `progname`, and related numbered variables; - it adds table-width controls such as `max_table_width`, `column_widths`, and `max_chars_per_line`; - it can write several output formats from the same table specification and optionally save the processed data used for reporting. A typical workflow is: 1. Build or assemble a table. 2. Define report metadata such as `title1`, `footnote1`, and `progname`. 3. Call `derrick::gtsummary_reporter()` with the table, output path, layout arguments, and requested `output_types`. 4. Inspect the generated files and add manual widths or pagination only when needed. The examples below write TXT and HTML outputs so the results can be displayed inside this HTML vignette. Use the same table specification with `output_types = c("RTF", "DOCX", "PDF")`, or any supported subset, when final delivery files are needed. ## Setup ```{r setup} library(dplyr) library(gtsummary) library(derrick) ``` ## Example 1: gtsummary summary input The most common input is a `gtsummary` object. In this example, the analysis layer creates a demographic summary with a p-value column and a spanning header. `derrick` then exports the displayed table structure. ```{r demog-table} demog_tbl <- gtsummary::trial |> dplyr::select(trt, age, grade) |> gtsummary::tbl_summary(by = trt) |> gtsummary::add_p() |> gtsummary::modify_spanning_header( gtsummary::all_stat_cols() ~ "**Treatment Group**" ) demog_tbl ``` For `gtsummary` inputs, `derrick` reads displayed columns, column labels, indentation rules, p-values, and spanning headers from the object. ```{r demog-source-preview} preview_cols <- intersect( c("label", "stat_1", "stat_2", "p.value"), names(demog_tbl$table_body) ) utils::head(demog_tbl$table_body[preview_cols]) ``` ```{r demog-metadata} title1 <- "Table 14.1.1 Summary of Demographics" title2 <- "Safety Population" footnote1 <- "Percentages are based on subjects in each treatment group." progname <- "programs/t-demog.R" ``` The `file_path` argument supplies the output stem. The actual formats written are controlled by `output_types`, and the function returns the generated file paths. ```{r demog-export} demog_paths <- gtsummary_reporter( gts_obj = demog_tbl, file_path = file.path(output_dir, "t-demog.rtf"), output_types = c("TXT", "HTML"), save_rds = TRUE ) output_manifest(demog_paths) ``` ### TXT preview TXT output is useful for checking fixed-width alignment, indentation, page titles, footnotes, and line wrapping. ```{r demog-txt-preview, echo = FALSE, comment = ""} demog_txt <- path_for_ext(demog_paths, "txt") print_text_output(demog_txt) ``` ### HTML preview HTML output is useful for a quick browser review of proportional-width table layout and header styling. ```{r demog-html-preview, echo = FALSE, results = "asis"} demog_html <- path_for_ext(demog_paths, "html") embed_html_output(demog_html, height = 500) ``` ## Example 2: gtsummary cross-table input Different `gtsummary` table types use the same export function. This example uses `tbl_cross()` to create a treatment-by-grade frequency table. ```{r cross-table} cross_tbl <- gtsummary::trial |> dplyr::select(trt, grade) |> gtsummary::tbl_cross( row = grade, col = trt, percent = "row" ) |> gtsummary::modify_spanning_header( gtsummary::all_stat_cols() ~ "**Treatment Group**" ) cross_tbl ``` ```{r cross-export} title1 <- "Table 14.1.2 Grade by Treatment" title2 <- "Safety Population" footnote1 <- "Percentages are row percentages." progname <- "programs/t-grade-cross.R" cross_paths <- gtsummary_reporter( gts_obj = cross_tbl, file_path = file.path(output_dir, "t-grade-cross.rtf"), output_types = c("TXT", "HTML"), save_rds = FALSE ) output_manifest(cross_paths) ``` ### TXT preview ```{r cross-txt-preview, echo = FALSE, comment = ""} cross_txt <- path_for_ext(cross_paths, "txt") print_text_output(cross_txt) ``` ### HTML preview ```{r cross-html-preview, echo = FALSE, results = "asis"} cross_html <- path_for_ext(cross_paths, "html") embed_html_output(cross_html, height = 420) ``` ## Example 3: plain data frame input Use a plain data frame when the table has already been assembled outside `gtsummary`, for example from a validated analysis data set or a custom summary program. In this mode, provide report labels and spanning headers explicitly, because there is no `gtsummary` styling metadata to read. ```{r ae-data-frame} ae_tbl <- data.frame( label = c( "Serious adverse events", " Subjects with at least one event", " Events leading to study drug interruption", "Treatment-emergent adverse events", " Headache", " Nausea", " Alanine aminotransferase increased" ), placebo = c("", "2 (10.0%)", "1 (5.0%)", "", "6 (30.0%)", "4 (20.0%)", "1 (5.0%)"), active_low = c("", "1 (5.0%)", "0", "", "5 (25.0%)", "3 (15.0%)", "2 (10.0%)"), active_high = c("", "3 (15.0%)", "2 (10.0%)", "", "7 (35.0%)", "5 (25.0%)", "3 (15.0%)"), total = c("", "6 (10.0%)", "3 (5.0%)", "", "18 (30.0%)", "12 (20.0%)", "6 (10.0%)"), stringsAsFactors = FALSE ) ae_tbl ``` ```{r ae-export} title1 <- "Table 14.3.1 Overview of Adverse Events" title2 <- "Safety Population" footnote1 <- "Events are counted once per subject within each row." progname <- "programs/t-ae-overview.R" ae_paths <- gtsummary_reporter( gts_obj = ae_tbl, file_path = file.path(output_dir, "t-ae-overview.rtf"), column_labels = c( label = "System Organ Class / Preferred Term", placebo = "Placebo", active_low = "Active Low", active_high = "Active High", total = "Total" ), spanning_headers = data.frame( from = "placebo", to = "total", label = "Treatment Group" ), output_types = c("TXT", "HTML"), save_rds = FALSE ) output_manifest(ae_paths) ``` ### TXT preview ```{r ae-txt-preview, echo = FALSE, comment = ""} ae_txt <- path_for_ext(ae_paths, "txt") print_text_output(ae_txt) ``` ### HTML preview ```{r ae-html-preview, echo = FALSE, results = "asis"} ae_html <- path_for_ext(ae_paths, "html") embed_html_output(ae_html, height = 440) ``` ## Example 4: controlling column widths The same table can look very different depending on the width controls. Start with reporter-managed automatic widths, then use `max_table_width` or `column_widths` when the report shell requires a specific layout. ### Automatic widths With no width overrides, `derrick` passes no column widths to `reporter`. `reporter` then estimates the display columns from the target output format, font settings, headers, table content, and available page width. ```{r ae-width-auto} title1 <- "Table 14.3.1 Overview of Adverse Events" title2 <- "Automatic column widths" footnote1 <- "This version lets reporter estimate the width of each column." progname <- "programs/t-ae-overview.R" ae_auto_paths <- gtsummary_reporter( gts_obj = ae_tbl, file_path = file.path(output_dir, "t-ae-auto.rtf"), column_labels = c( label = "System Organ Class / Preferred Term", placebo = "Placebo", active_low = "Active Low", active_high = "Active High", total = "Total" ), spanning_headers = data.frame( from = "placebo", to = "total", label = "Treatment Group" ), output_types = "TXT", save_rds = FALSE ) basename(ae_auto_paths) ``` ```{r ae-width-auto-preview, echo = FALSE, comment = ""} print_text_output(path_for_ext(ae_auto_paths, "txt")) ``` ### Narrow max_table_width `max_table_width` caps the whole table by passing a table-width constraint to `reporter`. A narrower width forces long labels to wrap sooner and leaves less room for treatment columns. ```{r ae-width-narrow} title1 <- "Table 14.3.1 Overview of Adverse Events" title2 <- "Narrow table width" footnote1 <- "This version caps the total table width at 5.5 inches." progname <- "programs/t-ae-overview.R" ae_narrow_paths <- gtsummary_reporter( gts_obj = ae_tbl, file_path = file.path(output_dir, "t-ae-narrow.rtf"), column_labels = c( label = "System Organ Class / Preferred Term", placebo = "Placebo", active_low = "Active Low", active_high = "Active High", total = "Total" ), spanning_headers = data.frame( from = "placebo", to = "total", label = "Treatment Group" ), max_table_width = 5.5, output_types = "TXT", save_rds = FALSE ) basename(ae_narrow_paths) ``` ```{r ae-width-narrow-preview, echo = FALSE, comment = ""} print_text_output(path_for_ext(ae_narrow_paths, "txt")) ``` ### Manual column_widths `column_widths` assigns widths in display-column order. For this data frame, the order is `label`, `placebo`, `active_low`, `active_high`, and `total`. Manual widths are useful when the label column needs more space than reporter's automatic estimate provides. ```{r ae-width-manual} title1 <- "Table 14.3.1 Overview of Adverse Events" title2 <- "Manual column widths" footnote1 <- "This version gives more room to the label column." progname <- "programs/t-ae-overview.R" ae_manual_paths <- gtsummary_reporter( gts_obj = ae_tbl, file_path = file.path(output_dir, "t-ae-manual.rtf"), column_labels = c( label = "System Organ Class / Preferred Term", placebo = "Placebo", active_low = "Active Low", active_high = "Active High", total = "Total" ), spanning_headers = data.frame( from = "placebo", to = "total", label = "Treatment Group" ), column_widths = "3.8|1.1|1.3|1.3|1.1", output_types = "TXT", save_rds = FALSE ) basename(ae_manual_paths) ``` ```{r ae-width-manual-preview, echo = FALSE, comment = ""} print_text_output(path_for_ext(ae_manual_paths, "txt")) ``` ## Example 5: manual pagination By default, `rows_per_page = NULL` leaves pagination to `reporter`. Set `rows_per_page` when a table shell requires predictable manual chunks before the report is written. The example below expands the adverse event table and starts a new page after every seven data rows. ```{r ae-pagination-data} paged_ae_tbl <- rbind( ae_tbl, data.frame( label = c( "Skin and subcutaneous tissue disorders", " Rash", " Pruritus", "Respiratory, thoracic and mediastinal disorders", " Cough", " Oropharyngeal pain", "Gastrointestinal disorders", " Diarrhea", " Abdominal pain", "Investigations", " Blood creatine phosphokinase increased" ), placebo = c( "", "2 (10.0%)", "1 (5.0%)", "", "3 (15.0%)", "1 (5.0%)", "", "2 (10.0%)", "1 (5.0%)", "", "1 (5.0%)" ), active_low = c( "", "3 (15.0%)", "1 (5.0%)", "", "2 (10.0%)", "2 (10.0%)", "", "4 (20.0%)", "2 (10.0%)", "", "2 (10.0%)" ), active_high = c( "", "4 (20.0%)", "2 (10.0%)", "", "5 (25.0%)", "2 (10.0%)", "", "5 (25.0%)", "3 (15.0%)", "", "4 (20.0%)" ), total = c( "", "9 (15.0%)", "4 (6.7%)", "", "10 (16.7%)", "5 (8.3%)", "", "11 (18.3%)", "6 (10.0%)", "", "7 (11.7%)" ), stringsAsFactors = FALSE ) ) nrow(paged_ae_tbl) ``` ```{r ae-pagination-export} title1 <- "Table 14.3.2 Adverse Events by Body System" title2 <- "Manual pagination" footnote1 <- "This version starts a new report page after every seven data rows." progname <- "programs/t-ae-paged.R" paged_paths <- gtsummary_reporter( gts_obj = paged_ae_tbl, file_path = file.path(output_dir, "t-ae-paged.rtf"), column_labels = c( label = "System Organ Class / Preferred Term", placebo = "Placebo", active_low = "Active Low", active_high = "Active High", total = "Total" ), spanning_headers = data.frame( from = "placebo", to = "total", label = "Treatment Group" ), rows_per_page = 7, output_types = c("TXT", "HTML"), save_rds = FALSE ) output_manifest(paged_paths) ``` ### TXT preview TXT output repeats the title, table header, footnote, and footer after each manual page break. ```{r ae-pagination-txt-preview, echo = FALSE, comment = ""} print_text_output(path_for_ext(paged_paths, "txt")) ``` ### HTML preview ```{r ae-pagination-html-preview, echo = FALSE, results = "asis"} paged_html <- path_for_ext(paged_paths, "html") embed_html_output(paged_html, height = 520) ``` ## Common layout controls These arguments are the main controls to review when moving from a draft table to a production report shell: - `output_types` chooses the files to write. This vignette runs TXT and HTML because they can be rendered directly inside an HTML page. - `column_labels` overrides displayed headers for plain data frames or special sponsor wording. - `spanning_headers` adds or overrides multi-column headers. - `max_table_width` passes a total table-width constraint to `reporter`. - `column_widths` controls exact column allocation. - `max_chars_per_line` constrains TXT output to a known character budget. - `rows_per_page` forces manual chunks before `reporter` writes the report; leave it `NULL` when reporter's automatic pagination is sufficient. ```{r layout-template, eval = FALSE} gtsummary_reporter( gts_obj = ae_tbl, file_path = file.path(output_dir, "t-ae-final.rtf"), column_labels = c( label = "System Organ Class / Preferred Term", placebo = "Placebo", active_low = "Active Low", active_high = "Active High", total = "Total" ), spanning_headers = data.frame( from = "placebo", to = "total", label = "Treatment Group" ), column_widths = "3.8|1.1|1.3|1.3|1.1", max_chars_per_line = 132, rows_per_page = 24, output_types = c("RTF", "TXT", "HTML") ) ``` ## Output checklist Before treating generated files as final, check that: - the source table type is intentional (`gtsummary` object or plain data frame); - all requested report formats were written; - title, footnote, and footer metadata are present; - label indentation is preserved after wrapping, especially for TXT outputs with manual label widths; - spanning headers cover the intended display columns; - any chosen manual widths fit the page setup and TXT character budget; - manual page breaks occur at the intended row boundaries, if `rows_per_page` is used; - saved RDS data matches the rows and columns sent to `reporter`.