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.
What Is ggplot2?
ggplot2 is an R package for data visualization based on the grammar of graphics.
Instead of thinking:
ggplot2 encourages a different approach:
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.
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.
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()
group =
USUBJID is still needed.
Clinical Example: Patient-Level Trajectories
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:
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.
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
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:
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
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.
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:
For a 95% confidence interval under a normal approximation:
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.
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:
For a mean difference, the null value is generally:
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:
This makes log-scaled forest plots particularly useful for ratio measures.
Example Forest Plot
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:
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:
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.
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.
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:
Percentage change is:
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.
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.
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
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.
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
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.
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 |
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.
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:
and, under a simple normal approximation:
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:
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
- Connecting patients together. Failure to specify the correct grouping variable can create false trajectories.
- Using the wrong treatment variable. The treatment displayed must correspond to the intended analysis.
- Incorrect visit ordering. Character sorting can place Week 12 before Week 4.
- Using inconsistent denominators. Percentages may appear correct while being based on the wrong population.
- Truncating the y-axis without investigation. Important clinical observations can disappear.
- Hiding missing observations. Artificially connecting observations can imply measurements that do not exist.
- Overusing color. The figure becomes difficult to interpret in grayscale or for color-impaired readers.
- Relying on default formatting. Default graphics may not meet clinical-reporting requirements.
- Embedding statistical decisions in plotting code. The figure should implement the analysis rather than silently change it.
- 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:
and:
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.
Clinical Trial Visualization Workflow
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:
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:
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
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:
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
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.