Tutorials › Biostatistics › ggplot2 for Clinical Trial Visualizations

Clinical Trial Programming

ggplot2 for Clinical Trial Visualizations

A practical guide to building reproducible clinical-trial graphics in R with ggplot2, including longitudinal laboratory plots, adverse-event displays, boxplots, spaghetti plots, treatment comparisons, forest plots, survival figures, clinical reporting conventions, and programming quality control.

Intermediate 22 min read

What You'll Learn

  • How the grammar of graphics applies to clinical-trial figures
  • How to build common clinical visualizations with ggplot2
  • How to create longitudinal patient-level plots
  • How to visualize adverse events and treatment-emergent events
  • How to create publication and CSR-ready graphics
  • How to validate and standardize ggplot2 output in clinical programming

Introduction

Clinical trials generate large quantities of data that are difficult to understand from tables alone.

A clinical study may contain thousands of laboratory measurements, repeated vital signs, longitudinal efficacy assessments, treatment-emergent adverse events, patient exposure intervals, time-to-event endpoints, and subgroup analyses.

Tables are essential for regulatory reporting, but visualizations often make patterns much easier to recognize.

A longitudinal laboratory table can show that values changed over time. A plot can show whether the change was gradual, abrupt, transient, or driven by a small number of patients.

This is where ggplot2 becomes particularly useful.

Key idea: ggplot2 is not simply a collection of chart types. It provides a structured system for constructing graphics from data, mappings, geometric objects, statistical transformations, scales, coordinate systems, and themes. That structure is particularly valuable in clinical programming because the same logic can be applied repeatedly across studies, endpoints, treatment groups, and reporting outputs.

What Is ggplot2?

ggplot2 is an R package for data visualization based on the grammar of graphics.

Instead of thinking:

$$ \text{Choose a chart type} \rightarrow \text{fill in the data} $$

ggplot2 encourages a different approach:

$$ \text{Data} + \text{Aesthetic mappings} + \text{Geometries} + \text{Scales} + \text{Theme} \rightarrow \text{Graphic} $$

This distinction matters because clinical-trial graphics frequently need small modifications to standard chart types.

For example, a simple line chart may need:

  • One line per patient
  • Separate treatment panels
  • A reference line for a clinically meaningful threshold
  • Observed-value points
  • Confidence intervals
  • Specific axis limits
  • A controlled legend
  • A reporting-specific theme

ggplot2 provides a systematic way to add these components.

The Core ggplot2 Structure

A typical ggplot2 expression looks like this:

ggplot(
  data = analysis_data,
  aes(
    x = AVISIT,
    y = AVAL
  )
) +
  geom_line() +
  geom_point() +
  labs(
    x = "Visit",
    y = "Change from Baseline"
  ) +
  theme_classic()

The components can be thought of as layers.

Component Purpose
ggplot() Initializes the graphic and identifies the data
aes() Defines mappings between variables and visual properties
geom_* Determines how observations are drawn
scale_* Controls axes, colors, shapes, and other scales
facet_* Creates separate panels
labs() Defines titles, labels, subtitles, and legends
theme() Controls visual presentation

Aesthetic Mappings

The aes() function is one of the most important parts of ggplot2.

An aesthetic mapping tells ggplot2 how a variable should be represented visually.

Common mappings include:

Aesthetic Visual Representation Clinical Example
x Horizontal position Visit, study day, age
y Vertical position Laboratory value, change from baseline
color Line or point color Treatment group
fill Area or bar fill Treatment group, severity
shape Point shape Assessment type
linetype Line style Observed versus reference
group Observation grouping Patient identifier

Clinical Data Usually Require Explicit Grouping

One of the most important ggplot2 concepts for clinical programmers is grouping.

Suppose a longitudinal dataset contains:

USUBJID   AVISIT   AVAL
001       Week 4    85
001       Week 8    72
001       Week 12   68
002       Week 4    91
002       Week 8    88
002       Week 12   82

If the objective is to draw one line per patient, the patient identifier should normally define the group.

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    group = USUBJID
  )
) +
  geom_line()

Without appropriate grouping, ggplot2 may connect observations that belong to different patients.

Clinical programming rule: Whenever a plot contains repeated observations from individual patients, explicitly consider whether group = USUBJID is required. A visually plausible plot can still be completely wrong if observations from different subjects are connected.

First Clinical Example: Longitudinal Laboratory Values

Longitudinal laboratory plots are among the most useful clinical-trial applications of ggplot2.

Consider a study evaluating a liver laboratory parameter over time.

Figure 1. Mean Change From Baseline in a Laboratory Parameter
Simulated treatment-group means with 95% confidence intervals across scheduled study visits.
Treatment A
Treatment B

Values are simulated for instructional purposes. Error bars represent illustrative 95% confidence intervals.

Building the Longitudinal Plot in R

The corresponding ggplot2 implementation could be:

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    group = TRT01P,
    color = TRT01P
  )
) +
  geom_line(
    linewidth = 0.8
  ) +
  geom_point(
    size = 2
  ) +
  geom_errorbar(
    aes(
      ymin = LOWER,
      ymax = UPPER
    ),
    width = 0.12
  ) +
  labs(
    x = "Study Visit",
    y = "Mean Change from Baseline",
    color = "Treatment"
  ) +
  theme_classic()

This structure separates the underlying data from the graphical representation.

Mean Versus Individual Patient Values

A critical decision in clinical visualization is whether to show:

  • Individual patient observations
  • Summary statistics
  • Both

A mean trajectory can be useful for describing the overall treatment-group pattern.

However, a mean can conceal substantial patient-level heterogeneity.

For example, a mean change of −5 units could arise because nearly every patient experienced a small decrease or because a few patients experienced very large decreases while the remaining patients did not change.

Those scenarios are clinically different.

Spaghetti Plots

A spaghetti plot displays individual patient trajectories over time.

This is especially useful for:

  • Laboratory parameters
  • Vital signs
  • Biomarkers
  • Patient-reported outcomes
  • Tumor measurements
  • Pharmacodynamic endpoints

The basic implementation is:

ggplot(
  data,
  aes(
    x = ADY,
    y = AVAL,
    group = USUBJID
  )
) +
  geom_line(
    alpha = 0.35
  ) +
  geom_point(
    alpha = 0.45,
    size = 1
  ) +
  labs(
    x = "Days from First Dose",
    y = "Observed Value"
  ) +
  theme_classic()

Transparency is often useful because hundreds of overlapping patient trajectories can otherwise become visually opaque.

Adding a Treatment Group

Treatment can be used as a color aesthetic while patient identifier remains the grouping variable.

ggplot(
  data,
  aes(
    x = ADY,
    y = AVAL,
    group = USUBJID,
    color = TRT01P
  )
) +
  geom_line(
    alpha = 0.35
  ) +
  labs(
    x = "Days from First Dose",
    y = "Observed Value",
    color = "Treatment"
  ) +
  theme_classic()
Important: Coloring by treatment does not automatically define the patient trajectory. When each patient has repeated measurements, group = USUBJID is still needed.

Clinical Example: Patient-Level Trajectories

Figure 2. Individual Patient Laboratory Trajectories
Simulated patient-level measurements showing different longitudinal patterns.

The trajectories illustrate stable measurements, gradual increases, transient changes, and sustained changes. Hover over a trajectory to identify the simulated patient.

When Spaghetti Plots Become Too Dense

A major limitation of individual-patient graphics is overplotting.

If a trial contains 1,000 patients and each patient has ten assessments, the result can contain thousands of plotted observations.

Possible solutions include:

  • Lowering line opacity
  • Removing points
  • Faceting by treatment group
  • Using a representative subset for exploratory visualization
  • Displaying quantile bands
  • Using median and percentile trajectories
  • Providing an interactive patient-selection display

For a formal clinical-reporting figure, any restriction of the displayed population should be explicitly defined.

Boxplots for Clinical Data

Boxplots are useful when the primary question concerns the distribution of a continuous endpoint across groups.

A standard boxplot displays:

  • Median
  • First quartile
  • Third quartile
  • Potential outliers
  • Overall distribution

The interquartile range is:

$$ IQR = Q_3-Q_1 $$

A basic treatment-group boxplot is:

ggplot(
  data,
  aes(
    x = TRT01P,
    y = AVAL
  )
) +
  geom_boxplot(
    width = 0.65
  ) +
  labs(
    x = "Treatment",
    y = "Endpoint"
  ) +
  theme_classic()

Boxplot With Individual Observations

For clinical datasets, it can be useful to show individual observations on top of a boxplot.

ggplot(
  data,
  aes(
    x = TRT01P,
    y = AVAL
  )
) +
  geom_boxplot(
    outlier.shape = NA
  ) +
  geom_jitter(
    width = 0.12,
    alpha = 0.45,
    size = 1.5
  ) +
  theme_classic()

This prevents the boxplot from hiding the actual patient-level distribution.

Figure 3. Distribution of a Continuous Clinical Endpoint
Simulated treatment-group distributions with individual observations.

Bar Charts for Adverse Events

Bar charts are frequently used to summarize adverse-event incidence.

For example, suppose the analysis dataset contains:

AEDECOD                 TRT01P       PCT
Headache               Treatment A  24.5
Headache               Treatment B  18.2
Nausea                  Treatment A  21.1
Nausea                  Treatment B  16.7
Fatigue                 Treatment A  19.8
Fatigue                 Treatment B  20.4

A grouped bar chart can be created using:

ggplot(
  data,
  aes(
    x = AEDECOD,
    y = PCT,
    fill = TRT01P
  )
) +
  geom_col(
    position = "dodge"
  ) +
  labs(
    x = "Adverse Event",
    y = "Patients with Event (%)",
    fill = "Treatment"
  ) +
  theme_classic()

Clinical Example: Adverse Event Incidence

Figure 4. Selected Treatment-Emergent Adverse Events
Simulated percentage of patients experiencing selected adverse events.

The values are illustrative and should not be interpreted as actual safety findings.

Percentages Require a Correct Denominator

An adverse-event figure is only as reliable as the denominator used to calculate its percentages.

For a treatment group:

$$ \text{AE Percentage} = \frac{\text{Number of patients with the event}} {\text{Number of patients in the relevant analysis population}} \times100 $$

The denominator may differ depending on the table or figure.

For example:

  • Safety population
  • Intent-to-treat population
  • Modified intent-to-treat population
  • Patients with evaluable assessments
Clinical reporting principle: Never assume that a percentage's denominator can be inferred from the chart alone. The denominator and analysis population should be defined in the analysis specification and, where necessary, communicated in the figure footnote.

Horizontal Bar Charts

Long adverse-event names often make vertical bar charts difficult to read.

A horizontal bar chart is frequently better.

ggplot(
  data,
  aes(
    x = reorder(AEDECOD, PCT),
    y = PCT
  )
) +
  geom_col() +
  coord_flip() +
  labs(
    x = "Adverse Event",
    y = "Patients with Event (%)"
  ) +
  theme_classic()

The reorder() function can arrange categories by their frequency.

Faceting Clinical Figures

Faceting creates separate panels according to a categorical variable.

For example:

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    group = USUBJID
  )
) +
  geom_line(
    alpha = 0.3
  ) +
  facet_wrap(
    ~ TRT01P
  ) +
  labs(
    x = "Visit",
    y = "Observed Value"
  ) +
  theme_classic()

Faceting is particularly useful for:

  • Treatment groups
  • Disease subgroups
  • Biomarker categories
  • Sex
  • Age categories
  • Geographic regions

Keep Scales Comparable When Comparing Panels

Suppose two treatment panels have different y-axis ranges.

A visual difference between the panels may appear larger or smaller simply because the scales differ.

For direct comparison, consider:

facet_wrap(
  ~ TRT01P,
  scales = "fixed"
)

Free scales can be appropriate when the distributions are fundamentally different, but the choice should be deliberate.

Best practice: When treatment panels are intended to support direct visual comparison, consistent axis limits are generally easier for readers to interpret.

Reference Lines

Clinical figures often require reference values.

For example, a laboratory plot might include an upper reference limit.

geom_hline(
  yintercept = 40,
  linetype = "dashed"
)

A reference line might represent:

  • Upper limit of normal
  • Lower limit of normal
  • Target threshold
  • Clinical response threshold
  • Baseline
  • Protocol-defined cutoff

Reference lines should always be labeled or explained clearly.

Vertical Reference Lines

Vertical lines are useful for identifying important time points.

geom_vline(
  xintercept = 84,
  linetype = "dashed"
)

Examples include:

  • End of treatment
  • Primary analysis cutoff
  • Week 12 milestone
  • Dose escalation point
  • Protocol amendment

Annotating a Clinical Figure

Annotations can help explain important events.

annotate(
  "text",
  x = 84,
  y = 45,
  label = "Primary analysis",
  hjust = 0
)

However, annotations should be used sparingly.

Too many labels can turn a figure into a second table.

Confidence Intervals

Clinical-trial figures frequently display estimates with confidence intervals.

For an estimated treatment effect \(\hat{\theta}\), a simple approximate two-sided confidence interval can be expressed as:

$$ \hat{\theta} \pm z_{1-\alpha/2} SE(\hat{\theta}) $$

For a 95% confidence interval under a normal approximation:

$$ \hat{\theta} \pm 1.96SE(\hat{\theta}) $$

The actual inferential method used in a clinical trial may be more complex.

ggplot2 does not determine the statistical method. It visualizes the estimates and intervals supplied to it.

Important separation: Statistical analysis produces the estimate and confidence interval. ggplot2 produces the visualization. The graphics code should not silently substitute a different statistical method from the one specified in the SAP.

Forest Plots

Forest plots are among the most important clinical-trial visualizations.

They can display:

  • Treatment effects
  • Hazard ratios
  • Risk ratios
  • Odds ratios
  • Mean differences
  • Subgroup treatment effects

A typical forest plot contains:

  • Subgroup labels
  • Point estimates
  • Confidence intervals
  • A vertical null-effect reference line

For a hazard ratio, the null value is:

$$ HR=1 $$

For a mean difference, the null value is generally:

$$ MD=0 $$

Forest Plot in ggplot2

ggplot(
  forest_data,
  aes(
    x = estimate,
    y = subgroup
  )
) +
  geom_vline(
    xintercept = 1,
    linetype = "dashed"
  ) +
  geom_errorbarh(
    aes(
      xmin = lower,
      xmax = upper
    ),
    height = 0
  ) +
  geom_point(
    size = 2.5
  ) +
  scale_x_log10() +
  labs(
    x = "Hazard Ratio",
    y = NULL
  ) +
  theme_classic()

Depending on the version of ggplot2 and supporting packages used in the programming environment, horizontal intervals may instead be constructed with geom_errorbar() after transforming the data or using a coordinate orientation.

Why Log Scales Are Common in Forest Plots

Ratios such as hazard ratios are naturally interpreted multiplicatively.

A hazard ratio of 0.5 and 2.0 are approximately reciprocal effects.

A logarithmic axis places reciprocal values at comparable visual distances from the null value of 1.

Thus:

$$ \log(HR)=0 \quad\text{when}\quad HR=1 $$

This makes log-scaled forest plots particularly useful for ratio measures.

Example Forest Plot

Figure 5. Simulated Treatment Effect by Subgroup
Hazard-ratio estimates with illustrative 95% confidence intervals. The vertical line at 1 represents the null effect.
Hazard ratio estimate

The subgroup estimates are simulated and are intended to demonstrate forest plot construction rather than represent an actual treatment comparison.

Survival Curves

Kaplan-Meier plots are another major clinical-trial visualization.

A survival curve estimates the probability of remaining event-free over time.

The Kaplan-Meier estimator can be written conceptually as:

$$ \hat{S}(t) = \prod_{t_i\leq t} \left( 1-\frac{d_i}{n_i} \right) $$

where:

  • \(d_i\) is the number of events at time \(t_i\)
  • \(n_i\) is the number of subjects at risk immediately before \(t_i\)

The statistical estimation is generally performed by a survival-analysis package. ggplot2 can then be used to style or extend the resulting data.

Do Not Confuse Plotting With Survival Analysis

A clinical programmer should distinguish:

1
Analysis: derive event times, censoring indicators, survival estimates, confidence intervals, and other required statistics.
2
Preparation: organize the derived estimates in a plotting dataset.
3
Visualization: use ggplot2 to construct the final graphic.

This separation improves traceability.

Using ggplot2 With Analysis Datasets

In a CDISC-oriented environment, plotting data may be derived from analysis datasets such as ADaM structures.

For example, a longitudinal efficacy plot might use variables such as:

Variable Example Meaning
USUBJID Unique subject identifier
TRT01P Planned treatment
AVISIT Analysis visit
AVISITN Numeric analysis visit
ADY Analysis day
AVAL Analysis value
CHG Change from baseline
PCHG Percentage change from baseline

The exact variables and derivations depend on the study and analysis specification.

Why Numeric Visit Variables Are Useful

Clinical datasets often contain labels such as:

Baseline
Week 4
Week 8
Week 12
Week 24

These are useful for display but are not always ideal for numerical ordering.

A numeric visit variable such as:

AVISITN
0
4
8
12
24

can make plotting and sorting more reliable.

The display label can then be controlled separately.

Discrete Versus Continuous Time

Clinical figures can treat time as either discrete or continuous.

A discrete visit plot might use:

aes(
  x = AVISIT,
  y = AVAL
)

A continuous time plot might use:

aes(
  x = ADY,
  y = AVAL
)

These answer slightly different questions.

Approach Useful For
Nominal visit Scheduled clinical-trial assessment summaries
Actual day Individual longitudinal timing
Months from treatment Long-term follow-up visualization
Study day Patient-level exposure or laboratory trajectories

Ordering Factors Correctly

One common clinical-programming problem is incorrect categorical ordering.

Suppose R treats visits alphabetically:

Baseline
Week 12
Week 4
Week 8

The order is incorrect.

A factor can solve this:

data$AVISIT <-
  factor(
    data$AVISIT,
    levels = c(
      "Baseline",
      "Week 4",
      "Week 8",
      "Week 12"
    )
  )

This is particularly important for:

  • Study visits
  • Severity categories
  • Response categories
  • Age groups
  • Disease stages
  • Clinical significance categories

Controlling Scales

Scales determine how data values are translated into visual positions and labels.

For example:

scale_y_continuous(
  breaks = seq(0, 100, 20),
  limits = c(0, 100)
)

Clinical figures should use scales that make the relevant clinical pattern visible without distorting the data.

Never Use the Axis to Hide Data

Suppose most laboratory values are between 0 and 100, but one patient has a value of 600.

Automatically setting:

scale_y_continuous(
  limits = c(0, 100)
)

would remove the observation from the displayed range.

This can materially change the interpretation.

Better approach: Investigate extreme values first. Determine whether they are valid, whether they are clinically meaningful, and whether the figure should show the full range, use a secondary display, or apply another explicitly documented strategy.

Color in Clinical Graphics

Color can encode treatment, response category, severity, or another clinical dimension.

For example:

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    color = TRT01P
  )
) +
  geom_line(
    aes(group = TRT01P)
  )

However, color should not be the only mechanism for communicating a critical distinction.

Important information should remain understandable in:

  • Grayscale printing
  • Low-quality reproduction
  • PDF documents
  • Projected presentations
  • Color-vision deficiencies

Colorblind-Friendly Design

Clinical figures should use distinguishable colors and should avoid relying entirely on subtle color differences.

Where practical, additional encoding can include:

  • Different line types
  • Different point shapes
  • Direct labels
  • Faceting

Clinical Reporting Themes

A reporting environment should generally use a consistent visual theme.

For example:

clinical_theme <- function(){

  theme_classic(
    base_size = 11
  ) +
    theme(
      plot.title = element_text(
        face = "bold"
      ),
      axis.title = element_text(
        face = "plain"
      ),
      legend.position = "bottom"
    )

}

The advantage is that every figure can use:

+
  clinical_theme()

rather than redefining formatting independently for every figure.

Why Custom Themes Matter in Clinical Programming

Suppose a clinical study contains 75 figures.

If every programmer manually specifies:

  • Font size
  • Legend position
  • Axis formatting
  • Plot margins
  • Title formatting
  • Line widths

then small inconsistencies are almost guaranteed.

A shared theme reduces this variation.

Programming principle: Standardize recurring formatting in reusable functions rather than copying formatting instructions across dozens of figure programs.

Using Reusable Plot Functions

Clinical programming frequently benefits from parameterized functions.

make_longitudinal_plot <- function(
  data,
  x_var,
  y_var,
  group_var
){

  ggplot(
    data,
    aes(
      x = {{ x_var }},
      y = {{ y_var }},
      group = {{ group_var }}
    )
  ) +
    geom_line(
      alpha = 0.5
    ) +
    geom_point(
      size = 1.5
    ) +
    theme_classic()

}

The function can then be reused for different endpoints.

Longitudinal Change From Baseline

Change from baseline is frequently calculated as:

$$ CHG_t = AVAL_t-BASE $$

Percentage change is:

$$ PCHG_t = \frac{AVAL_t-BASE}{BASE} \times100 $$

A plot can then display:

ggplot(
  data,
  aes(
    x = AVISITN,
    y = PCHG,
    group = USUBJID
  )
) +
  geom_line(
    alpha = 0.3
  ) +
  geom_hline(
    yintercept = 0
  ) +
  labs(
    x = "Study Visit",
    y = "Percent Change from Baseline"
  ) +
  theme_classic()

Handling Missing Values

Clinical datasets frequently contain missing assessments.

ggplot2 generally does not draw missing y-values, but the resulting line behavior should be reviewed carefully.

For longitudinal data, missing observations may produce gaps rather than continuous observed trajectories.

That distinction is important.

Do not impute merely for plotting. A visualization should generally reflect the analysis dataset and its prespecified missing-data conventions. Creating artificial values solely to make a line continuous can imply measurements that were never observed.

Unscheduled Assessments

Clinical trials frequently contain unscheduled measurements.

For example:

Day 28
Day 56
Day 61
Day 84

If the purpose is to show actual patient trajectories, the actual analysis day may be preferable.

If the purpose is to summarize scheduled visits, nominal visit mapping may be more appropriate.

The important requirement is consistency with the analysis specification.

Reference Ranges for Laboratory Data

Laboratory figures can display reference limits.

geom_hline(
  yintercept = ULN,
  linetype = "dashed"
) +
geom_hline(
  yintercept = LLN,
  linetype = "dashed"
)

If reference limits vary by:

  • Sex
  • Age
  • Laboratory
  • Collection method
  • Unit

the plotting dataset may require patient-specific or assessment-specific reference limits.

Do not assume a single universal laboratory limit. Clinical laboratory reference ranges can depend on the laboratory, analyte, population, unit, and other study-specific factors.

Unit Conversion

Clinical datasets can contain measurements in different units.

Before plotting, verify that:

  • Units are consistent
  • Conversion factors are correct
  • Reference ranges use the same units
  • Axis labels state the displayed unit

For example:

labs(
  y = "ALT (U/L)"
)

is much better than:

labs(
  y = "ALT"
)

when the unit matters clinically.

Dates and Study Days

Dates should be handled carefully.

For example:

ggplot(
  data,
  aes(
    x = ADT,
    y = AVAL
  )
) +
  geom_point()

can display actual calendar dates.

But clinical reporting may instead require study-relative time:

ggplot(
  data,
  aes(
    x = ADY,
    y = AVAL
  )
) +
  geom_point()

Study day often makes treatment timing easier to interpret.

Logarithmic Axes

Some clinical endpoints span several orders of magnitude.

A logarithmic scale can be appropriate for:

  • Drug concentrations
  • Biomarker measurements
  • Viral load
  • Pharmacokinetic parameters

For example:

scale_y_log10()

However, log transformation changes the visual interpretation and should be used only when scientifically appropriate.

Faceted Biomarker Plots

Suppose a study measures several biomarkers.

The data may be structured as:

USUBJID   AVISIT   PARAM       AVAL
001       Week 4   Biomarker A  12
001       Week 4   Biomarker B  84
001       Week 8   Biomarker A  17
001       Week 8   Biomarker B  77

A faceted graphic can separate parameters:

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    color = TRT01P,
    group = TRT01P
  )
) +
  geom_line() +
  facet_wrap(
    ~ PARAM,
    scales = "free_y"
  ) +
  theme_classic()

Whether free y-scales are appropriate depends on whether cross-panel magnitude comparisons are important.

Combining Summary and Individual Data

One powerful clinical visualization is to display individual observations together with a summary statistic.

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL
  )
) +
  geom_jitter(
    width = 0.08,
    alpha = 0.25
  ) +
  stat_summary(
    fun = median,
    geom = "point",
    size = 3
  ) +
  stat_summary(
    fun = median,
    geom = "line",
    aes(group = 1),
    linewidth = 0.9
  ) +
  theme_classic()

This can reveal both the central tendency and the underlying distribution.

Clinical Figure 6: Summary Plus Patient Data

Figure 6. Individual Observations With Median Trajectory
Simulated longitudinal data showing individual observations and the median value at each visit.

This type of display can be useful during exploratory analysis because it shows both patient-level variation and the group-level summary.

Direct Labels Versus Legends

Legends are useful when several groups are displayed.

However, direct labeling can sometimes be clearer.

For example, the final point of a treatment trajectory can be labeled directly.

geom_text(
  data = final_points,
  aes(
    label = TRT01P
  ),
  hjust = -0.1
)

Direct labeling can reduce the amount of eye movement required to connect a line with its treatment group.

For dense clinical figures, however, a well-designed legend may be preferable.

Legends in Clinical Reports

A clinical figure legend should answer:

  • What is being plotted?
  • What population is included?
  • What do colors or symbols represent?
  • What does the error bar represent?
  • What is the time scale?
  • What are the units?
  • What analysis population was used?

The graphical legend and the figure footnote serve different purposes.

Titles, Subtitles, and Footnotes

A useful ggplot2 structure is:

labs(
  title = "Mean Change From Baseline",
  subtitle = "Safety population",
  x = "Study Visit",
  y = "Change from Baseline",
  caption = "Values are mean ± 95% CI."
)

In a clinical report, however, the final presentation may place the detailed statistical description outside the plot itself.

Publication-Quality Output

The same ggplot2 figure can be exported to multiple formats.

ggsave(
  filename = "figure1.pdf",
  plot = p,
  width = 7,
  height = 5
)

For raster output:

ggsave(
  filename = "figure1.png",
  plot = p,
  width = 7,
  height = 5,
  dpi = 300
)

The appropriate output format depends on the submission or reporting requirements.

Why Vector Graphics Are Often Valuable

Vector formats such as PDF preserve graphical elements as scalable objects.

This can be useful for:

  • Clinical study reports
  • Regulatory documents
  • Manuscripts
  • High-resolution printing

Raster formats can still be appropriate for web pages, presentations, and systems that require bitmap images.

Reproducibility

One of the greatest advantages of ggplot2 in clinical programming is reproducibility.

A figure generated manually in a spreadsheet may require several manual steps.

A figure generated from code can be regenerated after:

  • A data correction
  • A database update
  • A new cutoff date
  • A revised analysis population
  • A programming correction

The code therefore becomes part of the analysis traceability.

Parameterizing Figure Programs

Rather than hard-coding study-specific values throughout a program, use parameters.

y_label <- "Change from Baseline"
x_label <- "Study Visit"
analysis_population <- "Safety Population"

p <- ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    color = TRT01P
  )
) +
  geom_line() +
  labs(
    x = x_label,
    y = y_label
  ) +
  theme_classic()

This makes updates easier and reduces accidental inconsistencies.

Common Clinical ggplot2 Geometries

Geometry Common Clinical Application
geom_point() Individual observations
geom_line() Longitudinal trajectories
geom_col() Adverse-event frequencies
geom_bar() Counts or summarized frequencies
geom_boxplot() Distribution of continuous endpoints
geom_errorbar() Confidence intervals
geom_ribbon() Confidence or percentile bands
geom_hline() Clinical thresholds
geom_vline() Time or event references
geom_text() Annotations and labels

Statistical Transformations

Some ggplot2 layers calculate summaries.

For example:

stat_summary(
  fun = mean,
  geom = "point"
)

However, in a clinical-trial reporting environment, it is often preferable to derive the required analysis values explicitly before plotting.

Traceability principle: For formal clinical reporting, explicit analysis datasets and derived summary statistics can make the source of each plotted value easier to inspect and validate than relying heavily on implicit statistical transformations inside the plotting layer.

Precompute What Matters

For example, rather than asking ggplot2 to calculate a confidence interval inside the graphic, a clinical programmer may prepare:

AVISIT   TRT01P   AVAL   LOWER   UPPER
Week 4   A          -4.2   -6.1   -2.3
Week 4   B          -2.1   -4.0   -0.2
Week 8   A          -8.4  -11.2   -5.6
Week 8   B          -4.8   -7.5   -2.1

Then ggplot2 becomes a transparent visualization layer.

Separating Analysis From Presentation

1
Raw clinical data: source observations and events.
2
Analysis dataset: derived variables and analysis flags.
3
Figure dataset: estimates, intervals, labels, and plotting variables.
4
ggplot2: graphical construction and formatting.
5
Output: validated PDF, PNG, or other required format.

Common Mistake: Using Raw Data Directly

A common beginner workflow is:

ggplot(raw_data) + ...

This can be appropriate for exploration, but formal clinical reporting usually requires more thought.

The programmer should know:

  • Which population is included
  • Which records are analysis records
  • Which baseline is used
  • How duplicates are handled
  • How unscheduled visits are treated
  • How missing values are represented
  • Which treatment variable is appropriate

Treatment Assignment Matters

Clinical datasets may contain several treatment variables.

Examples can include:

  • Planned treatment
  • Actual treatment
  • Treatment at randomization
  • Treatment at analysis

The correct choice depends on the analysis and figure specification.

Never select a treatment variable simply because its name looks convenient. The treatment variable used for a figure should match the prespecified analysis population and estimand.

Baseline Visualization

Baseline characteristics are often summarized in tables, but graphics can also be useful during exploratory analysis.

For example, a continuous baseline measure can be displayed with:

ggplot(
  baseline_data,
  aes(
    x = TRT01P,
    y = AGE
  )
) +
  geom_boxplot() +
  geom_jitter(
    width = 0.1,
    alpha = 0.35
  ) +
  labs(
    x = "Treatment",
    y = "Age (years)"
  ) +
  theme_classic()

For formal CSR presentation, whether such a figure is appropriate depends on the reporting objective.

Clinical Trial Forest Plots: A More Complete Example

A subgroup forest plot often requires several pieces of information:

Variable Example
Subgroup Age <65 years
Estimate 0.72
Lower CI 0.54
Upper CI 0.96
N 312
Events 148

The plotting dataset may therefore contain both graphical and textual information.

A more elaborate forest plot can use:

ggplot(
  forest_data,
  aes(
    y = subgroup,
    x = estimate
  )
) +
  geom_vline(
    xintercept = 1,
    linetype = "dashed"
  ) +
  geom_errorbar(
    aes(
      xmin = lower,
      xmax = upper
    ),
    orientation = "y",
    height = 0
  ) +
  geom_point(
    size = 2.5
  ) +
  scale_x_log10() +
  labs(
    x = "Hazard Ratio",
    y = NULL
  ) +
  theme_classic()

The exact implementation should be tested against the installed ggplot2 version and the chosen coordinate orientation.

Combining Multiple Plot Layers

The power of ggplot2 comes from composing layers.

For example:

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    color = TRT01P
  )
) +
  geom_line(
    linewidth = 0.8
  ) +
  geom_point(
    size = 2
  ) +
  geom_errorbar(
    aes(
      ymin = LOWER,
      ymax = UPPER
    ),
    width = 0.12
  ) +
  geom_hline(
    yintercept = 0,
    linetype = "dashed"
  ) +
  labs(
    x = "Study Visit",
    y = "Mean Change from Baseline",
    color = "Treatment"
  ) +
  theme_classic()

Each layer has a specific role.

Global Versus Layer-Specific Aesthetics

This distinction is important.

A global mapping:

ggplot(
  data,
  aes(
    x = AVISIT,
    y = AVAL,
    color = TRT01P
  )
)

applies to subsequent layers unless overridden.

A layer-specific mapping:

geom_errorbar(
  aes(
    ymin = LOWER,
    ymax = UPPER
  )
)

adds information only where required.

This makes complex clinical graphics easier to construct.

Clinical Figure Specifications

Before programming a formal figure, create a figure specification.

Specification Example
Population Safety population
Analysis variable Change from baseline
Time variable Analysis visit
Grouping Treatment
Summary statistic Mean
Interval 95% confidence interval
Units U/L
Reference value 0
Missing data Not displayed
Output PDF

Why Figure Specifications Matter

A figure program should not be the first place where analytical decisions are made.

The specification should establish what the figure means.

The program should implement that specification.

Think of the figure program as an implementation of the analysis specification, not as the place where the analysis specification is invented.

Quality Control of ggplot2 Figures

Clinical figures require more than checking whether the R program runs without errors.

A technically valid graphic can still contain incorrect data.

At minimum, review:

  • Analysis population
  • Treatment assignment
  • Variable derivations
  • Units
  • Visit ordering
  • Baseline definitions
  • Summary statistics
  • Confidence intervals
  • Reference lines
  • Axis limits
  • Legend labels
  • Figure titles
  • Footnotes

Patient-Level Validation

For longitudinal figures, select several patients and manually compare the underlying data against the plotted values.

For example:

subset(
  data,
  USUBJID == "ABC-001"
)

ggsave(
  "validated_patient_plot.pdf",
  plot = p
)

Verify that the plotted trajectory follows the expected observations.

Aggregate Validation

For summary plots, independently verify:

  • Number of patients
  • Number of observations
  • Means or medians
  • Standard deviations
  • Standard errors
  • Confidence intervals
  • Percentages

For example:

$$ SE = \frac{SD}{\sqrt{N}} $$

and, under a simple normal approximation:

$$ 95\%\,CI = \bar{x} \pm 1.96SE $$

The exact analysis method may instead use a t-distribution or a model-based standard error.

Cross-Checking Against Tables

A powerful QC strategy is to compare figure-derived values with corresponding tables.

For example, if a figure shows:

$$ 24.5\% $$

for an adverse event, the corresponding safety table should be checked.

Differences may reveal:

  • Different populations
  • Different denominators
  • Different analysis flags
  • Different event-selection rules
  • Programming errors

Common ggplot2 Mistakes in Clinical Programming

  1. Connecting patients together. Failure to specify the correct grouping variable can create false trajectories.
  2. Using the wrong treatment variable. The treatment displayed must correspond to the intended analysis.
  3. Incorrect visit ordering. Character sorting can place Week 12 before Week 4.
  4. Using inconsistent denominators. Percentages may appear correct while being based on the wrong population.
  5. Truncating the y-axis without investigation. Important clinical observations can disappear.
  6. Hiding missing observations. Artificially connecting observations can imply measurements that do not exist.
  7. Overusing color. The figure becomes difficult to interpret in grayscale or for color-impaired readers.
  8. Relying on default formatting. Default graphics may not meet clinical-reporting requirements.
  9. Embedding statistical decisions in plotting code. The figure should implement the analysis rather than silently change it.
  10. Failing to validate output. A successfully rendered PDF is not evidence that the figure is correct.

Common Mistake: Using Counts When Percentages Are Required

Consider an adverse-event chart showing:

Treatment A: 35
Treatment B: 30

The counts appear similar.

But suppose the treatment groups contain:

Treatment A: 100 patients
Treatment B: 50 patients

The corresponding percentages are:

$$ 35/100=35\% $$

and:

$$ 30/50=60\% $$

The clinical interpretation is completely different.

Common Mistake: Using Means for Skewed Data

Some clinical variables are strongly skewed.

Examples can include:

  • Hospital length of stay
  • Time to event
  • Drug concentrations
  • Certain biomarker measurements

A mean may not adequately represent the distribution.

Median and percentile summaries may be more informative depending on the endpoint and analysis plan.

Common Mistake: Misinterpreting Error Bars

An error bar does not have a universal meaning.

It could represent:

  • Standard deviation
  • Standard error
  • 95% confidence interval
  • Interquartile range
  • Prediction interval

The figure must identify what the error bar represents.

Common Mistake: Confusing Confidence Intervals With Variability

A confidence interval quantifies uncertainty around an estimated parameter.

It is not the same thing as displaying individual-patient variability.

For example, a narrow 95% confidence interval around a mean can coexist with large individual-level variation if the sample size is large.

Remember: A confidence interval answers an inferential question about an estimate. A distribution plot answers a descriptive question about observed data. They should not be interpreted interchangeably.

Clinical Trial Visualization Workflow

1
Define the clinical question the figure must answer.
2
Identify the analysis population.
3
Identify the analysis dataset and variables.
4
Define the statistical summary or endpoint derivation.
5
Create a plotting dataset.
6
Construct the ggplot2 layers.
7
Apply standardized clinical-reporting formatting.
8
Export the figure in the required format.
9
Perform visual and numerical QC.
10
Archive the program, input dataset, output, and validation evidence.

Choosing the Right Clinical Visualization

Clinical Question Useful Visualization
How does an endpoint change over time? Line plot
How do individual patients change? Spaghetti plot
How are continuous values distributed? Boxplot, violin plot, histogram
How frequent are adverse events? Bar chart
How do treatment effects vary by subgroup? Forest plot
How does time-to-event differ? Kaplan-Meier plot
How do categorical response rates compare? Bar chart or dot plot
How do tumor measurements change longitudinally? Spider plot
How deep are individual tumor responses? Waterfall plot
How long do patients remain on treatment? Swimmer plot

ggplot2 and Oncology Visualizations

ggplot2 is especially useful in oncology because many oncology graphics are built from longitudinal patient-level data.

Examples include:

  • Spider plots
  • Waterfall plots
  • Swimmer plots
  • Time-to-response plots
  • Biomarker trajectories
  • Tumor burden summaries
  • Response-category distributions

For example, a basic spider plot can be constructed with:

ggplot(
  data,
  aes(
    x = ADY,
    y = PCHG,
    group = USUBJID
  )
) +
  geom_line(
    alpha = 0.45
  ) +
  geom_hline(
    yintercept = 0
  ) +
  theme_classic()

The important point is that ggplot2 provides the graphical framework; the clinical interpretation comes from the analysis specification and disease context.

ggplot2 and CDISC Traceability

A well-designed figure program should make it possible to trace a plotted value backward through the analysis workflow.

A simplified chain is:

$$ \text{Source Data} \rightarrow \text{SDTM} \rightarrow \text{ADaM} \rightarrow \text{Figure Dataset} \rightarrow \text{ggplot2} \rightarrow \text{Figure} $$

The exact pathway varies by study and analysis.

The key principle is that the figure should be reproducible from defined analysis inputs.

Traceability Example

Suppose a forest plot contains a hazard ratio of:

$$ HR=0.72 $$

A reviewer should be able to determine:

  • Which analysis population produced the estimate
  • Which endpoint was analyzed
  • Which statistical model was used
  • Which confidence interval was used
  • Which subgroup definition was used
  • Which dataset supplied the estimate
  • Which program generated the figure

The figure is the final presentation layer, not the source of the statistical truth.

Reproducible Figure Naming

A standardized naming convention can help organize outputs.

FIG_14_2_1
FIG_14_2_2
FIG_14_2_3

The exact naming convention should match the organization's reporting standards.

Version Control

Clinical graphics benefit from version-controlled code.

A code repository can preserve:

  • Figure programs
  • Reusable theme functions
  • Utility functions
  • Test data
  • Documentation
  • Change history

This becomes particularly valuable when a figure must be regenerated after database lock updates or programming corrections.

Making ggplot2 More Efficient

Large clinical datasets can contain millions of records.

Before plotting, reduce the data to only what is needed.

plot_data <- analysis_data |>
  dplyr::filter(
    SAFFL == "Y"
  ) |>
  dplyr::select(
    USUBJID,
    TRT01P,
    AVISIT,
    AVISITN,
    AVAL
  )

This can improve both clarity and performance.

Do Not Plot Unnecessary Variables

A common programming habit is to pass an entire analysis dataset into every plotting function.

A better approach is to construct a focused plotting dataset.

plot_data <- adlb |>
  dplyr::filter(
    PARAMCD == "ALT",
    SAFFL == "Y"
  ) |>
  dplyr::select(
    USUBJID,
    TRT01P,
    AVISIT,
    AVISITN,
    AVAL
  )

This makes the plotting logic easier to review.

Interactive Versus Static Figures

ggplot2 primarily produces static graphics.

Static figures are generally appropriate for:

  • Clinical study reports
  • Regulatory submissions
  • Manuscripts
  • Archived analysis outputs

Interactive graphics may be more useful for exploratory analysis.

For example, an interactive system can allow a user to:

  • Select a patient
  • Filter treatment
  • Inspect an observation
  • Zoom into a time interval
  • Display additional metadata

However, the final regulated reporting output may still need to be a static, reproducible figure.

Exploratory Versus Confirmatory Figures

This distinction should be explicit.

Exploratory Figure Confirmatory / Reporting Figure
Flexible Specification-driven
May investigate unusual observations Should follow prespecified rules
May use interactive features Usually static and reproducible
Can rapidly evolve Requires stronger QC
May display additional variables Should contain defined analysis population

A Practical Figure Template

A reusable clinical figure function might start with:

clinical_plot <- function(
  data,
  x,
  y,
  color = NULL,
  title = NULL,
  x_label = NULL,
  y_label = NULL
){

  p <- ggplot(
    data,
    aes(
      x = {{ x }},
      y = {{ y }}
    )
  )

  if(!is.null(color)){
    p <- p +
      aes(color = {{ color }})
  }

  p +
    labs(
      title = title,
      x = x_label,
      y = y_label
    ) +
    theme_classic()

}

In production programming, functions can be made considerably more robust, including explicit validation of required variables and controlled handling of optional aesthetics.

Validation of Figure Inputs

Reusable functions should ideally fail clearly when required data are missing.

For example:

required_vars <- c(
  "USUBJID",
  "TRT01P",
  "AVISIT",
  "AVAL"
)

missing_vars <- setdiff(
  required_vars,
  names(data)
)

if(length(missing_vars) > 0){

  stop(
    paste(
      "Missing required variables:",
      paste(missing_vars, collapse = ", ")
    )
  )

}

This is safer than allowing a partially specified figure to render silently.

Testing a Figure Program

Before using a figure program on production data, test it against a small known dataset.

For example:

test_data <- data.frame(
  USUBJID = c(
    "001","001","001",
    "002","002","002"
  ),
  AVISITN = c(
    0,4,8,
    0,4,8
  ),
  AVAL = c(
    100,90,80,
    100,105,110
  )
)

The expected trajectories are obvious:

  • Patient 001 decreases
  • Patient 002 increases

If the resulting plot does not show those patterns, the grouping or ordering logic should be investigated.

Clinical Figure QC Checklist

1
Confirm the analysis population.
2
Confirm treatment assignment.
3
Confirm baseline and derived variables.
4
Confirm visit ordering.
5
Confirm units.
6
Confirm denominators.
7
Confirm summary statistics and confidence intervals.
8
Confirm reference lines and axis limits.
9
Review labels, legends, titles, and footnotes.
10
Compare the figure with independent numerical output.

Clinical Reporting Checklist

  • Is the figure answering a clearly defined clinical question?
  • Is the analysis population correct?
  • Are treatment groups correct?
  • Are all units explicitly identified?
  • Are visit labels correctly ordered?
  • Are missing observations handled appropriately?
  • Are confidence intervals correctly calculated upstream?
  • Are reference lines clinically justified?
  • Are extreme values visible or appropriately explained?
  • Can every plotted value be traced to analysis data?
  • Does the output meet the reporting specification?
  • Does the figure remain interpretable when printed in grayscale?

What Makes a Good Clinical Figure?

A good clinical figure is not necessarily the most visually elaborate figure.

It should instead make the intended clinical pattern easy to understand while preserving statistical and data integrity.

A strong figure generally has:

  • A clearly defined population
  • A clinically meaningful endpoint
  • Correct denominators
  • Appropriate scales
  • Consistent treatment encoding
  • Minimal unnecessary decoration
  • Clear labels
  • Reproducible code
  • Traceable input data

ggplot2 as a Clinical Programming Skill

Learning ggplot2 is more valuable than memorizing individual plotting functions.

A clinical programmer should become comfortable asking:

  • What variable belongs on the x-axis?
  • What belongs on the y-axis?
  • What defines an observation?
  • What defines a patient trajectory?
  • What variable should control treatment color?
  • Should the data be summarized first?
  • Should the x-axis be continuous or categorical?
  • Should panels share a scale?
  • What clinical reference values should be displayed?
  • How will the output be validated?

Once these questions are answered, the ggplot2 syntax is usually much easier to construct.

A Complete Clinical Plot Example

The following example combines several principles discussed throughout this tutorial.

library(ggplot2)

p <- ggplot(
  plot_data,
  aes(
    x = AVISITN,
    y = AVAL,
    color = TRT01P,
    group = TRT01P
  )
) +

  geom_hline(
    yintercept = 0,
    linetype = "dashed",
    linewidth = 0.5
  ) +

  geom_line(
    linewidth = 0.8
  ) +

  geom_point(
    size = 2
  ) +

  geom_errorbar(
    aes(
      ymin = LOWER,
      ymax = UPPER
    ),
    width = 0.15
  ) +

  scale_x_continuous(
    breaks = c(0, 4, 8, 12, 16, 24)
  ) +

  labs(
    title = "Mean Change From Baseline",
    x = "Weeks From First Dose",
    y = "Mean Change From Baseline",
    color = "Treatment"
  ) +

  theme_classic(
    base_size = 11
  ) +

  theme(
    legend.position = "bottom",
    plot.title = element_text(
      face = "bold"
    )
  )

p

This pattern can serve as a starting point for many longitudinal clinical figures.

How to Think About ggplot2 in a Clinical Trial

A useful mental model is:

$$ \boxed{ \text{Clinical Question} \rightarrow \text{Analysis Dataset} \rightarrow \text{Statistical Summary} \rightarrow \text{Visual Mapping} \rightarrow \text{Validated Figure} } $$

ggplot2 primarily operates in the fourth stage.

It does not determine what the clinical question is, which patients belong in the analysis, or which statistical method should be used.

Those decisions must come from the protocol, SAP, analysis specifications, and clinical/statistical requirements.

ggplot2 and the Statistical Programmer

For clinical trial statisticians and programmers, ggplot2 is most powerful when combined with sound data-management and statistical-analysis practices.

The goal is not simply to produce attractive charts.

The goal is to create graphics that are:

  • Accurate
  • Reproducible
  • Traceable
  • Clinically interpretable
  • Consistent
  • Auditable
  • Appropriate for the intended audience
Bottom line: ggplot2 provides a flexible grammar for constructing clinical-trial graphics from analysis data. Its greatest value in clinical programming comes from the combination of explicit data mappings, reusable plotting layers, standardized themes, reproducible code, and rigorous validation. Whether the goal is a longitudinal laboratory plot, adverse-event chart, boxplot, spider plot, waterfall plot, swimmer plot, forest plot, or survival display, the same core principles apply: define the analysis population, prepare the correct plotting dataset, map variables deliberately, use clinically meaningful scales and annotations, and validate the final figure against independent numerical evidence.

Key Takeaways

Concept Key Lesson
Grammar of graphics Build figures from data, mappings, geometries, scales, and themes.
Grouping Use patient identifiers when repeated observations must form individual trajectories.
Clinical summaries Precompute analysis statistics when traceability is important.
Scales Choose axis ranges and transformations deliberately.
Color Use treatment or other clinically meaningful variables consistently.
Faceting Separate treatment groups or subgroups when appropriate.
Missing data Do not create artificial observations solely to improve appearance.
QC Validate both the numbers and the visual presentation.
Traceability Maintain a clear path from analysis data to final figure.
Reproducibility Use code and reusable functions rather than manual chart editing.

References

Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer, Cham.

Wickham, H., Averick, M., Bryan, J., et al. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686.

Wilke, C.O. (2020). Fundamentals of Data Visualization. O'Reilly Media.

R Core Team. R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing.

The R Foundation. R Documentation.

Wickham, H., François, R., Henry, L., Müller, K., and Vaughan, D. dplyr: A Grammar of Data Manipulation. R package documentation.

Therneau, T.M. A Package for Survival Analysis in R. R package documentation for survival analysis and time-to-event methods.