5Patient Report with officer, ggplot2 and flextable
This chapter demonstrates a complete end-to-end workflow for creating patient profile reports using officer, flextable, and ggplot2. We’ll work with clinical trial data (CDISC ADaM datasets) to create individualized patient reports that combine tables and graphics.
5.1 Overview of the Workflow
We’ll build patient reports that include:
Subject-level demographics table, a basic patient information
Vital signs visualization, time series plots showing metabolic measurements
Vital signs summary table, a tabular summary of measurements by visit
Each patient will get their own Word document with a consistent structure and formatting.
5.2 Enriching the Template with Custom Styles
Before generating reports, we’ll create custom paragraph styles that will be used throughout our documents. These styles ensure consistency and make it easy to apply formatting.
We’ll create three specialized styles: - “Table Caption”: For table titles (centered, italic) - “Image Caption”: For figure titles (centered, italic) - “Graphic”: For centering graphics
Why create styles programmatically?
While you could create these styles manually in Word, defining them programmatically ensures: - Reproducibility: Every report uses identical styling - Version control: Style definitions are tracked in your R code - Flexibility: Easy to adjust styles for all reports by changing one place in your code - Distribution: No need to maintain and distribute multiple template versions
Throughout our report generation process, we’ll use these styles to: - Apply "Table Caption" to titles above tables (e.g., “Table 1: Demographics”) - Apply "Image Caption" to titles above figures (e.g., “Figure 1: Vital Signs Over Time”) - Apply "Graphic" as the paragraph style when inserting plots with body_add_gg()
This creates visual consistency: all table captions look the same, all figure captions look the same, and all graphics are properly centered.
5.2.1 Testing the New Styles
Before using the styles in production, let’s verify they were created correctly and see how they render:
Using print(doc, preview = TRUE) opens the document in Word immediately, allowing you to verify formatting before generating hundreds of patient reports. This is invaluable during development.
5.3 Setting Global Defaults
To ensure consistency across all tables and plots in our reports, we’ll set global defaults for flextable and ggplot2:
Code
path_to_template<-here("template", "template-02.docx")# Set flextable defaultsset_flextable_defaults( table.layout ="autofit", # Let Word optimize column widths font.family ="Arial", # Match our organization's standard font.size =11, # Standard body text size digits =2# Two decimal places for numbers)# Set ggplot2 themetheme_set(theme_minimal( base_family ="Arial"# Match font across tables and plots))
Why set these defaults?
flextable defaults: Every table we create will automatically use Arial 11pt with 2 decimal places. We don’t need to repeat colformat_double(digits = 2) every time.
ggplot2 theme: All plots will have a clean minimal theme with Arial font, matching our tables and maintaining visual consistency.
5.4 Preparing the Data
Clinical trial data is typically organized in CDISC ADaM datasets. We’ll work with: - adsl: Subject-Level Analysis Dataset (demographics, dates) - advs_metabolic: Analysis Dataset for Vital Signs (metabolic measurements)
5.4.1 Structuring Data for Individual Reports
We need to nest the data so we have one row per patient, with their complete data stored in nested data frames:
After nesting, LIST_TBLS has one row per patient with two list-columns:
SL: A data frame with subject-level information
METABOLIC: A data frame with all metabolic vital signs measurements
This structure makes it easy to iterate over patients and extract their complete data for each report.
5.5 Generating Individual Patient Reports
Now we’ll loop through each patient and generate their individual report. Each report will have:
A table of contents
Subject-level information table
Vital signs plots
Vital signs summary table
Code
sub_path<-here("output", "patient-profile", "simple")# Create output directorydir.create(sub_path, showWarnings =FALSE, recursive =TRUE)# Loop through each patientfor(iinseq_len(nrow(LIST_TBLS))){# Get patient ID and create filenameUSUBJID_STR<-LIST_TBLS[["USUBJID"]][i]# result file pathfile_basename<-paste0(USUBJID_STR, ".docx")fileout<-file.path(sub_path, file_basename)# Start with the templatedoc<-read_docx(path =path_to_template)# Add table of contentsdoc<-body_add_toc(doc)doc<-body_add_break(doc)## Section 1: Subject-Level Analysis ----# Extract subject-level data for this patientSL_TBL<-LIST_TBLS[["SL"]][[i]]# Create a formatted table with proper labelstab1<-as_flextable(SL_TBL, show_coltype =FALSE)|>labelizor( labels =pharmaverseadam::adsl|>labelled::get_variable_labels()|>unlist())|>padding(padding =6)# Add section heading and tabledoc<-body_add_par(doc, value ="Subject Level Analysis", style ="heading 1")doc<-body_add_flextable(doc, tab1, align ="center")## Section 2: Vital Signs Analysis ----# Extract metabolic data for this patientMETABOLIC_TBL<-LIST_TBLS[["METABOLIC"]][[i]]|>mutate(ADY =as.Date(ADY))# Create time series plotgg<-ggplot(METABOLIC_TBL, aes(ADY, AVAL))+geom_path()+scale_x_date(date_breaks ="4 weeks", date_labels ="%W")+facet_wrap(~PARAMCD, scales ="free_y")+labs( x ="Study Week", y ="Measurement Value", title =NULL)# Add section heading, subsection, and plotdoc<-body_add_par(doc, value ="Vital Signs Analysis for Metabolic", style ="heading 1")doc<-body_add_par(doc, value ="Graphic", style ="heading 2")doc<-body_add_gg(doc, gg, style ="Graphic")# Create summary table (wide format)ft<-METABOLIC_TBL|>pivot_wider( id_cols =c(AVISIT), names_from =PARAMCD, values_from =AVAL)|>flextable()|>theme_vanilla()|>colformat_double(digits =2)|>labelizor(labels =labels)# Add subsection and tabledoc<-body_add_par(doc, value ="Table", style ="heading 2")doc<-body_add_flextable(doc, ft, align ="center")# Save the patient's reportprint(doc, target =fileout)}
Transforms long data (one row per measurement) to wide format (one row per visit)
Each parameter becomes a column
Makes it easy to compare parameters at each visit
5.6 Reports with captions
The previous example demonstrated basic report generation. Now we’ll enhance it with automatic numbering for tables and figures, along with dedicated table of figures (ToF) and table of tables (ToT).
5.6.1 Why Use Automatic Numbering?
In pharmaceutical reports:
Tables and figures must be numbered sequentially
Captions need consistent formatting
Cross-references should update automatically
Regulatory requirements often mandate specific caption styles
The run_autonum() function provides automatic sequential numbering that updates when content is reordered.
5.6.2 Understanding autonumbered runs
Use fpar() with run_autonum() to create a paragraph-caption:
fpar(run_autonum(seq_id ="table", pre_label ="Table "),"Description of the table", fp_p =fp_par_lite( word_style ="Table Caption", keep_with_next =TRUE))
Key features:
seq_id: Creates a unique numbering sequence (e.g., “table” for tables, “fig” for figures)
pre_label: Text before the number (e.g., “Table” or “Figure”)
keep_with_next = TRUE: Ensures captions stay with their content (critical for pagination)
5.6.3 Generating Reports with Automatic Numbering
Let’s create enhanced patient reports with automatic numbering:
Code
sub_path<-here("output", "patient-profile", "with-captions")# Create output directorydir.create(sub_path, showWarnings =FALSE, recursive =TRUE)# Loop through each patientfor(iinseq_len(nrow(LIST_TBLS))){# Get patient ID and create filenameUSUBJID_STR<-LIST_TBLS[["USUBJID"]][i]file_basename<-paste0(USUBJID_STR, ".docx")fileout<-file.path(sub_path, file_basename)# Start with the templatedoc<-read_docx(path =path_to_template)# Add table of contents for headingsdoc<-body_add_toc(doc)doc<-body_add_break(doc)# Add table of tablesdoc<-body_add_par(doc, "List of Tables", style ="heading 1")doc<-body_add_toc(doc, style ="Table Caption")doc<-body_add_break(doc)# Add table of figuresdoc<-body_add_par(doc, "List of Figures", style ="heading 1")doc<-body_add_toc(doc, style ="Image Caption")doc<-body_add_break(doc)## Section 1: Subject-Level Analysis ----# Extract subject-level data for this patientSL_TBL<-LIST_TBLS[["SL"]][[i]]# Create a formatted table with proper labelstab1<-as_flextable(SL_TBL, show_coltype =FALSE)|>labelizor( labels =pharmaverseadam::adsl|>labelled::get_variable_labels()|>unlist())|>padding(padding =6)# Add section headingdoc<-body_add_par(doc, value ="Subject Level Analysis", style ="heading 1")# Add table caption with automatic numberingdoc<-body_add_fpar(doc,fpar(run_autonum(seq_id ="table", pre_label ="Table "),"Subject Demographics and Baseline Characteristics", fp_p =fp_par_lite( word_style ="Table Caption", keep_with_next =TRUE)))# Add the tabledoc<-body_add_flextable(doc, tab1, align ="center")## Section 2: Vital Signs Analysis ----# Extract metabolic data for this patientMETABOLIC_TBL<-LIST_TBLS[["METABOLIC"]][[i]]|>mutate(ADY =as.Date(ADY))# Create time series plotgg<-ggplot(METABOLIC_TBL, aes(ADY, AVAL))+geom_path()+scale_x_date(date_breaks ="4 weeks", date_labels ="%W")+facet_wrap(~PARAMCD, scales ="free_y")+labs( x ="Study Week", y ="Measurement Value", title =NULL)# Add section headingdoc<-body_add_par(doc, value ="Vital Signs Analysis for Metabolic", style ="heading 1")# Add subsectiondoc<-body_add_par(doc, value ="Longitudinal Trends", style ="heading 2")# Add the plotdoc<-body_add_gg(doc, gg, style ="Graphic")doc<-body_add_fpar(doc,fpar(run_autonum(seq_id ="fig", pre_label ="Figure "),"Metabolic Vital Signs Over Time by Parameter", fp_p =fp_par_lite( word_style ="Image Caption", keep_with_next =FALSE)))# Create summary table (wide format)ft<-METABOLIC_TBL|>pivot_wider( id_cols =AVISIT, names_from =PARAMCD, values_from =AVAL)|>flextable()|>theme_vanilla()|>colformat_double(digits =2)|>labelizor(labels =labels)# Add subsectiondoc<-body_add_par(doc, value ="Summary by Visit", style ="heading 2")# Add table caption with automatic numberingdoc<-body_add_fpar(doc,fpar(run_autonum(seq_id ="table", pre_label ="Table "),"Metabolic Vital Signs Summary by Study Visit", fp_p =fp_par_lite( word_style ="Table Caption", keep_with_next =TRUE)))# Add the tabledoc<-body_add_flextable(doc, ft, align ="center")# Save the patient's reportprint(doc, target =fileout)}
After generating the documents, users need to update the fields in Word:
Open the generated document
Press Ctrl+A (Windows) or Cmd+A (Mac) to select all
Press F9 to update all fields
The TOC, ToT, and ToF will populate with the correct page numbers
Automating Field Updates
While Word requires manual field updates by default, you can automate this using VBA macros or the ‘doconv’ package if you need to process many documents programmatically.