Exporting Clinical Tables with derrick

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

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.

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
Characteristic
Treatment Group
p-value
Drug A
N = 98
1
Drug B
N = 102
1
Age 46 (37, 60) 48 (39, 56)
    Unknown 7 4
Grade


    I 35 (36%) 33 (32%)
    II 32 (33%) 36 (35%)
    III 31 (32%) 33 (32%)
1 Median (Q1, Q3); n (%)

For gtsummary inputs, derrick reads displayed columns, column labels, indentation rules, p-values, and spanning headers from the object.

preview_cols <- intersect(
  c("label", "stat_1", "stat_2", "p.value"),
  names(demog_tbl$table_body)
)

utils::head(demog_tbl$table_body[preview_cols])
#> # A tibble: 6 × 4
#>   label   stat_1      stat_2      p.value
#>   <chr>   <chr>       <chr>       <lgl>  
#> 1 Age     46 (37, 60) 48 (39, 56) NA     
#> 2 Unknown 7           4           NA     
#> 3 Grade   <NA>        <NA>        NA     
#> 4 I       35 (36%)    33 (32%)    NA     
#> 5 II      32 (33%)    36 (35%)    NA     
#> 6 III     31 (32%)    33 (32%)    NA
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.

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)
#>   format         file bytes
#> 1    TXT  t-demog.txt   764
#> 2   HTML t-demog.html  3864

TXT preview

TXT output is useful for checking fixed-width alignment, indentation, page titles, footnotes, and line wrapping.

Table 14.1.1 Summary of Demographics
Safety Population

______________________________________________
                   Treatment Group
               _______________________
                 Drug A      Drug B
                 N = 98      N = 102
Characteristic    N (%)       N (%)    p-value
______________________________________________

Age            46 (37, 60) 48 (39, 56)
    Unknown         7           4
Grade
    I           35 (36%)    33 (32%)
    II          32 (33%)    36 (35%)
    III         31 (32%)    33 (32%)
______________________________________________
Percentages are based on subjects in each tr...

                                                                          programs/t-demog.R 10JUL2026 22:46

HTML preview

HTML output is useful for a quick browser review of proportional-width table layout and header styling.

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.

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
Treatment Group
Drug A Drug B Total
Grade


    I 35 (51%) 33 (49%) 68 (100%)
    II 32 (47%) 36 (53%) 68 (100%)
    III 31 (48%) 33 (52%) 64 (100%)
Total 98 (49%) 102 (51%) 200 (100%)
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)
#>   format               file bytes
#> 1    TXT  t-grade-cross.txt   629
#> 2   HTML t-grade-cross.html  3520

TXT preview

Table 14.1.2 Grade by Treatment
Safety Population

_____________________________________
               Treatment Group
        _____________________________
         Drug A   Drug B     Total
         N (%)     N (%)     N (%)
_____________________________________

Grade
    I   35 (51%) 33 (49%)  68 (100%)
    II  32 (47%) 36 (53%)  68 (100%)
    III 31 (48%) 33 (52%)  64 (100%)
Total   98 (49%) 102 (51%) 200 (100%)
_____________________________________
Percentages are row percentages.

                                                                    programs/t-grade-cross.R 10JUL2026 22:46

HTML preview

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.

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
#>                                         label   placebo active_low active_high
#> 1                      Serious adverse events                                 
#> 2            Subjects with at least one event 2 (10.0%)   1 (5.0%)   3 (15.0%)
#> 3   Events leading to study drug interruption  1 (5.0%)          0   2 (10.0%)
#> 4           Treatment-emergent adverse events                                 
#> 5                                    Headache 6 (30.0%)  5 (25.0%)   7 (35.0%)
#> 6                                      Nausea 4 (20.0%)  3 (15.0%)   5 (25.0%)
#> 7          Alanine aminotransferase increased  1 (5.0%)  2 (10.0%)   3 (15.0%)
#>        total
#> 1           
#> 2  6 (10.0%)
#> 3   3 (5.0%)
#> 4           
#> 5 18 (30.0%)
#> 6 12 (20.0%)
#> 7  6 (10.0%)
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)
#>   format               file bytes
#> 1    TXT  t-ae-overview.txt  1293
#> 2   HTML t-ae-overview.html  4491

TXT preview

Table 14.3.1 Overview of Adverse Events
Safety Population

____________________________________________________________________________________
                                                         Treatment Group
                                            ________________________________________
                                                       Active    Active
System Organ Class / Preferred Term          Placebo     Low      High      Total
____________________________________________________________________________________

Serious adverse events
  Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
  Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
Treatment-emergent adverse events
  Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
  Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
  Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
____________________________________________________________________________________
Events are counted once per subject within each row.

                                                                    programs/t-ae-overview.R 10JUL2026 22:46

HTML preview

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.

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)
#> [1] "t-ae-auto.txt"
Table 14.3.1 Overview of Adverse Events
Automatic column widths

____________________________________________________________________________________
                                                         Treatment Group
                                            ________________________________________
                                                       Active    Active
System Organ Class / Preferred Term          Placebo     Low      High      Total
____________________________________________________________________________________

Serious adverse events
  Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
  Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
Treatment-emergent adverse events
  Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
  Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
  Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
____________________________________________________________________________________
This version lets reporter estimate the width of each column.

                                                                    programs/t-ae-overview.R 10JUL2026 22:46

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.

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)
#> [1] "t-ae-narrow.txt"
Table 14.3.1 Overview of Adverse Events
Narrow table width

____________________________________________________________________________________
                                                         Treatment Group
                                            ________________________________________
                                                       Active    Active
System Organ Class / Preferred Term          Placebo     Low      High      Total
____________________________________________________________________________________

Serious adverse events
  Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
  Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
Treatment-emergent adverse events
  Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
  Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
  Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
____________________________________________________________________________________
This version caps the total table width at 5.5 inches.

                                                                    programs/t-ae-overview.R 10JUL2026 22:46

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.

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)
#> [1] "t-ae-manual.txt"
Table 14.3.1 Overview of Adverse Events
Manual column widths

_______________________________________________________________________________________________________
                                                                   Treatment Group
                                              _________________________________________________________
System Organ Class / Preferred Term             Placebo      Active Low      Active High      Total
_______________________________________________________________________________________________________

Serious adverse events
  Subjects with at least one event             2 (10.0%)      1 (5.0%)        3 (15.0%)     6 (10.0%)
  Events leading to study drug interruption     1 (5.0%)          0           2 (10.0%)      3 (5.0%)
Treatment-emergent adverse events
  Headache                                     6 (30.0%)      5 (25.0%)       7 (35.0%)     18 (30.0%)
  Nausea                                       4 (20.0%)      3 (15.0%)       5 (25.0%)     12 (20.0%)
  Alanine aminotransferase increased            1 (5.0%)      2 (10.0%)       3 (15.0%)     6 (10.0%)
_______________________________________________________________________________________________________
This version gives more room to the label column.

                                                                    programs/t-ae-overview.R 10JUL2026 22:46

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.

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)
#> [1] 18
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)
#>   format            file bytes
#> 1    TXT  t-ae-paged.txt  3710
#> 2   HTML t-ae-paged.html 11715

TXT preview

TXT output repeats the title, table header, footnote, and footer after each manual page break.

Table 14.3.2 Adverse Events by Body System
Manual pagination

____________________________________________________________________________________
                                                         Treatment Group
                                            ________________________________________
                                                       Active    Active
System Organ Class / Preferred Term          Placebo     Low      High      Total
____________________________________________________________________________________

Serious adverse events
  Subjects with at least one event          2 (10.0%) 1 (5.0%)  3 (15.0%) 6 (10.0%)
  Events leading to study drug interruption 1 (5.0%)      0     2 (10.0%)  3 (5.0%)
Treatment-emergent adverse events
  Headache                                  6 (30.0%) 5 (25.0%) 7 (35.0%) 18 (30.0%)
  Nausea                                    4 (20.0%) 3 (15.0%) 5 (25.0%) 12 (20.0%)
  Alanine aminotransferase increased        1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
____________________________________________________________________________________
This version starts a new report page after every seven data rows.

                                                                       programs/t-ae-paged.R 10JUL2026 22:46
Table 14.3.2 Adverse Events by Body System
Manual pagination

________________________________________________________________________________________
                                                             Treatment Group
                                                ________________________________________
                                                           Active    Active
System Organ Class / Preferred Term              Placebo     Low      High      Total
________________________________________________________________________________________

Skin and subcutaneous tissue disorders
  Rash                                          2 (10.0%) 3 (15.0%) 4 (20.0%) 9 (15.0%)
  Pruritus                                      1 (5.0%)  1 (5.0%)  2 (10.0%)  4 (6.7%)
Respiratory, thoracic and mediastinal disorders
  Cough                                         3 (15.0%) 2 (10.0%) 5 (25.0%) 10 (16.7%)
  Oropharyngeal pain                            1 (5.0%)  2 (10.0%) 2 (10.0%)  5 (8.3%)
Gastrointestinal disorders
________________________________________________________________________________________
This version starts a new report page after every seven data rows.

                                                                       programs/t-ae-paged.R 10JUL2026 22:46
Table 14.3.2 Adverse Events by Body System
Manual pagination

_________________________________________________________________________________
                                                      Treatment Group
                                         ________________________________________
                                                    Active    Active
System Organ Class / Preferred Term       Placebo     Low      High      Total
_________________________________________________________________________________

  Diarrhea                               2 (10.0%) 4 (20.0%) 5 (25.0%) 11 (18.3%)
  Abdominal pain                         1 (5.0%)  2 (10.0%) 3 (15.0%) 6 (10.0%)
Investigations
  Blood creatine phosphokinase increased 1 (5.0%)  2 (10.0%) 4 (20.0%) 7 (11.7%)
_________________________________________________________________________________
This version starts a new report page after every seven data rows.

                                                                       programs/t-ae-paged.R 10JUL2026 22:46

HTML preview

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.
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.