Visualization of clinical data

Laure Cougnaud, Michela Pasetto

April 11, 2025

This vignette focuses on the visualizations available in the clinDataReview package.

We will use example data sets from the clinUtils package.

If you have doubts on the data format, please check first the vignette on data preprocessing available at: here.

If everything is clear on that side, let’s get started!

Please note that the patient profiles and interactive visualizations are only displayed in the vignette if Pandoc is available.

library(clinDataReview)
library(plotly)
library(clinUtils)

data(dataADaMCDISCP01)
labelVars <- attr(dataADaMCDISCP01, "labelVars")

varsLB <- c(
    "PARAM", "PARAMCD", "USUBJID", "TRTP", 
    "ADY", "VISITNUM", "VISIT", "LBSTRESN"
)
dataLB <- dataADaMCDISCP01$ADLBC[, varsLB]

varsAE <- c("USUBJID", "AESOC", "AEDECOD", "ASTDY", "AENDY", "AESEV")
dataAE <- dataADaMCDISCP01$ADAE[, varsAE]

varsDM <- c("RFSTDTC", "USUBJID")
dataDM <- dataADaMCDISCP01$ADSL[, varsDM]

1 Patient profiles

The interactive visualizations of the clinical data package include functionalities to link a plot to patient-specific report, e.g. patient profiles created with the patientProfilesVis package.

Such patient profiles can be created via a config file, with a dedicated template report available in the clinDataReview package.

A simple patient profile report for each subject in the example dataset is created below.

Please note that the patient profiles are created and included in the interactive visualizations only during an interactive session (via interactive()) .

# please change to the working directory
# (a temporary directory is used for the vignette)
dir <- tempdir()

# create a directory to store the patient profiles:
patientProfilesDir <- file.path(dir, "patientProfiles")
dir.create(patientProfilesDir)

# get examples of parameters for the report
configDir <- system.file("skeleton", "config", package = "clinDataReview")
params <- getParamsFromConfig(
    configDir = configDir, 
    configFile = "config-patientProfiles.yml"
)
# create patient profile with only one panel for the demo
params$patientProfilesParams <- params$patientProfilesParams[1]
# use dataset from the clinUtils package
params$pathDataFolder <- system.file("extdata", "cdiscpilot01", "SDTM", package = "clinUtils")
# store patient profile in this folder:
params$patientProfilePath <- patientProfilesDir

# create patient profiles
pathTemplatePkg <- clinDataReview::getPathTemplate(params$template)
pathTemplateUser <- file.path(dir, basename(pathTemplatePkg))
tmp <- file.copy(from = pathTemplatePkg, to = pathTemplateUser)
report <- rmarkdown::render(
  input = pathTemplateUser, 
  envir = new.env()
)

# clean-up
unlink(pathTemplateUser);unlink(report)

Please refer to the vignette about reporting for more details on how to set up a config file and use template reports available in the package.

You can directly skip to reporting vignette, which is available here or run in your console the command below.

vignette("clinDataReview-reporting", "clinDataReview")

2 Data visualization

All the visualizations available in the package are interactive.

2.1 Visualization of individual profiles

Visualization of individual profiles is available via the function scatterplotClinData.

2.1.1 Explore the visualization data

To facilitate the exploration of the data, the underlying data behind each visualization can be included as a table as well below the plot by setting the parameter table to TRUE.

Please note that this functionality is not demonstrated in this document to ensure a lightweight vignette in the package.

2.1.3 Spaghetti plot of time profile

labParam <- "ALT"
dataPlot <- subset(dataLB, PARAMCD == labParam)

visitLab <- with(dataPlot, tapply(ADY, VISIT, median))
names(visitLab) <- sub("-", "\n", names(visitLab))

# link to patient profiles
if(interactive())
    dataPlot$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataPlot$USUBJID), ".pdf"
    )

scatterplotClinData(
    data = dataPlot, 
    xVar = "ADY",
    yVar = "LBSTRESN",
    aesPointVar = list(color = "TRTP", fill = "TRTP"),
    aesLineVar = list(group = "USUBJID", color = "TRTP"),
    linePars = list(size = 0.5, alpha = 0.7),
    hoverVars = c("USUBJID", "VISIT", "ADY", "LBSTRESN", "TRTP"),
    labelVars = labelVars,
    xPars = list(breaks = visitLab, labels = names(visitLab)),
    title = paste("Actual value of", 
        getLabelParamcd(
            paramcd = labParam, data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
        )
    ),
    # include link to patient profiles:
    pathVar = if(interactive()) "patientProfilePath",
    subtitle = paste(
        "Profile plot by subject",
        "Points are positioned at relative day",
        "Visits are positioned based on median relative day across subjects.",
        sep = "\n"
    ),
    verbose = TRUE,
    table = FALSE
)
SCREENING 1WEEK 12WEEK 16WEEK 2WEEK 20WEEK 24WEEK 26WEEK 4WEEK 6WEEK 8203040
Planned Treatment(Placebo,1)(Xanomeline High Dose,1)(Xanomeline Low Dose,1)PlaceboXanomeline High DoseXanomeline Low DoseActual value of Alanine Aminotransferase (U/L)Analysis Relative DayNumeric Result/Finding in Standard UnitsProfile plot by subjectPoints are positioned at relative dayVisits are positioned based on median relative day across subjects.

2.1.4 Scatterplot

# format data long -> wide format (one column per lab param)
dataPlot <- subset(dataLB, PARAMCD %in% c("ALT", "ALB"))
dataPlot <- stats::aggregate(
    LBSTRESN ~ USUBJID + VISIT + VISITNUM + PARAMCD, 
    data = dataPlot,
    FUN = mean
)
dataPlotWide <- stats::reshape(
    data = dataPlot,
    timevar = "PARAMCD", idvar = c("USUBJID", "VISIT", "VISITNUM"),
    direction = "wide"
)
colnames(dataPlotWide) <- sub("^LBSTRESN.", "", colnames(dataPlotWide))

# link to patient profiles
if(interactive())
    dataPlotWide$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataPlotWide$USUBJID), ".pdf"
    )

# scatterplot per visit
scatterplotClinData(
    data = dataPlotWide, 
    xVar = "ALT", yVar = "ALB",
    xLab = getLabelParamcd(
        paramcd = "ALT", data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
    ),
    yLab = getLabelParamcd(
        paramcd = "ALB", data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
    ),
    aesPointVar = list(color = "USUBJID", fill = "USUBJID"),
    facetPars = list(facets = ~ VISIT),
    labelVars = labelVars,
    pathVar = if(interactive()) "patientProfilePath",
    table = FALSE
)
32.535.037.540.042.532.535.037.540.042.532.535.037.540.042.532.535.037.540.042.520304032.535.037.540.042.5203040
Unique Subject Identifier01-701-114801-701-119201-701-121101-704-144501-710-108301-718-137101-718-1427Albumin (g/L) vs Alanine Aminotransferase (U/L)Alanine Aminotransferase (U/L)Albumin (g/L)SCREENING 1WEEK 12WEEK 16WEEK 2WEEK 20WEEK 24WEEK 26WEEK 4WEEK 6WEEK 8

2.1.5 eDish plot

dataALT <- subset(dataLB, PARAMCD == "ALT")
dataBILI <- subset(dataLB, PARAMCD == "BILI")

byVar <- c("USUBJID", "VISIT")

dataPlot <- merge(
    x = dataALT, y = dataBILI[, c(byVar, "LBSTRESN")], 
    by = c("USUBJID", "VISIT"), 
    suffixes = c(".ALT", ".BILI"),
    all = TRUE
)
labelVars[paste0("LBSTRESN.", c("ALT", "BILI"))] <-
    paste(
        "Actual value of", 
        getLabelParamcd(
            paramcd = c("ALT", "BILI"), data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
        )
    )

# link to patient profiles
if(interactive())
    dataPlot$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataPlot$USUBJID), ".pdf"
    )

# scatterplot per visit
scatterplotClinData(
    data = dataPlot, 
    xVar = "LBSTRESN.ALT", yVar = "LBSTRESN.BILI",
    xLab = getLabelParamcd(
        paramcd = "ALT", data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
    ),
    yLab = getLabelParamcd(
        paramcd = "BILI", data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
    ),
    aesPointVar = list(color = "VISIT", fill = "VISIT"),
    xTrans = "log10", yTrans = "log10",
    hoverVars = "USUBJID",
    labelVars = labelVars,
    table = FALSE, 
    pathVar = if(interactive()) "patientProfilePath"
)
20305051020
Visit NameSCREENING 1WEEK 12WEEK 16WEEK 2WEEK 20WEEK 24WEEK 26WEEK 4WEEK 6WEEK 8Bilirubin (umol/L) vs Alanine Aminotransferase (U/L)Alanine Aminotransferase (U/L)Bilirubin (umol/L)

2.1.6 Visualization of time-intervals

Time-intervals are displayed with the timeProfileIntervalPlot function:

# link to patient profiles
if(interactive())
    dataAE$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataAE$USUBJID), ".pdf"
    )
timeProfileIntervalPlot(
    data = dataAE,
    paramVar = "USUBJID",
    # time-variables
    timeStartVar = "ASTDY",
    timeEndVar = "ASTDY",
    colorVar = "AESEV",
    hoverVars = c("USUBJID", "AEDECOD", "ASTDY", "AENDY", "AESEV"),
    labelVars = labelVars,
    table = FALSE, 
    pathVar = if(interactive()) "patientProfilePath",
    tableVars = c("USUBJID", "AEDECOD", "ASTDY", "AENDY", "AESEV")
)
−800−600−400−200020001-701-114801-701-119201-701-121101-704-144501-710-108301-718-137101-718-1427
Severity/IntensityMILDMODERATESEVEREAnalysis Start Relative Day and Analysis Start Relative Day

By default, empty intervals are represented if the start/end time variables are missing. Missing start/end time can be imputed, or different symbols can be used to represent such cases:

# create variable to indicate status of start/end date
dataAE$AESTFLG <- ifelse(is.na(dataAE$ASTDY), "Missing start", "Complete")
dataAE$AEENFLG <- ifelse(is.na(dataAE$AENDY), "Missing end", "Complete")
shapePalette <- c(
    `Missing start` = "triangle-left", 
    `Complete` = "square-open", 
    `Missing end` = "triangle-right"
)

# 'simple'-imputation:
# if start is missing, 'Missing' symbol displayed at end interval
dataAE$AESTDYIMP <- with(dataAE, ifelse(is.na(ASTDY), AENDY, ASTDY))
# if end is missing, 'Missing' symbol displayed at start interval
dataAE$AEENDYIMP <- with(dataAE, ifelse(is.na(AENDY), ASTDY, AENDY))

timeProfileIntervalPlot(
    data = dataAE,
    paramVar = "USUBJID", 
    # time-variables
    timeStartVar = "AESTDYIMP", timeStartLab = "Start day",
    timeEndVar = "AEENDYIMP", timeEndLab = "End day",
    # shape variables
    timeStartShapeVar = "AESTFLG",
    timeStartShapeLab = "Status of start date",
    timeEndShapeVar = "AEENFLG",
    timeEndShapeLab = "Status of end date",
    shapePalette = shapePalette,
    hoverVars = c("USUBJID", "AEDECOD", "AESEV", "ASTDY", "AESTFLG", "AENDY", "AEENFLG"),
    labelVars = labelVars,
    table = FALSE, 
    tableVars = c("USUBJID", "AEDECOD", "AESEV", "ASTDY", "AESTFLG", "AENDY", "AEENFLG"),
    pathVar = if(interactive()) "patientProfilePath"
)
−800−600−400−200020001-701-114801-701-119201-701-121101-704-144501-710-108301-718-137101-718-1427
Start day and End day

2.2 Visualization of summary statistics

Summary statistics can also be visualized with the package, via different types of visualizations: sunburst, treemap and barplot.

These functions take as input a table of summary statistics, especially counts. Such table can e.g. computed with the inTextSummaryTable R package (see corresponding package vignette for more information).

2.2.2 Categorical variables

2.2.2.1 Compute count statistics

In this example, counts of adverse events are extracted for each Primary System Organ Class and Dictionary-Derived Term.

Besides the counts of the number of subjects, the paths to the patient profile report for each subgroup are extracted and combined.

# total counts: Safety Analysis Set (patients with start date for the first treatment)
dataTotal <- subset(dataDM, RFSTDTC != "")

## patient profiles report

if(interactive()){

    # add path in data
    
    dataAE$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataAE$USUBJID), ".pdf"
    )

    # add link in data (for attached table)
    dataAE$patientProfileLink <- with(dataAE,
        paste0(
            '<a href="', patientProfilePath, 
            '" target="_blank">', USUBJID, '</a>'
        )
    )

    # Specify extra summarizations besides the standard stats
    # When the data is summarized,
    # the patient profile path are summarized
    # as well across patients
    # (the paths should be collapsed with: ', ')
    statsExtraPP <- list(
        statPatientProfilePath = function(data) 
          toString(sort(unique(data$patientProfilePath))),
        statPatientProfileLink = function(data)
          toString(sort(unique(data$patientProfileLink)))
    )
    
}

# get counts (records, subjects, % subjects) + stats with subjects profiles path
statsPP <- c(
    inTextSummaryTable::getStats(type = "count"),
    if(interactive())
        list(
            patientProfilePath = quote(statPatientProfilePath),
            patientProfileLink = quote(statPatientProfileLink)
        )
)

dataAE$AESEV <- factor(
    dataAE$AESEV,
    levels = c("MILD", "MODERATE", "SEVERE")
)
dataAE$AESEVN <- as.numeric(dataAE$AESEV)

# compute adverse event table
tableAE <- inTextSummaryTable::computeSummaryStatisticsTable(
    
    data = dataAE,
    rowVar = c("AESOC", "AEDECOD"),
    dataTotal = dataTotal,
    labelVars = labelVars,
    
    # The total across the variable used for the nodes
    # should be specified
    rowVarTotalInclude = c("AESOC", "AEDECOD"),
    
    rowOrder = "total",
    
    # statistics of interest
    # include columns with patients
    stats = statsPP, 
    # add extra 'statistic': concatenate subject IDs
    statsExtra = if(interactive())  statsExtraPP

)
knitr::kable(head(tableAE),
    caption = paste("Extract of the Adverse Event summary table",
        "used for the sunburst and barplot visualization"
    )
)
Extract of the Adverse Event summary table used for the sunburst and barplot visualization
AESOC AEDECOD isTotal statN statm statPercTotalN statPercN n % m
CARDIAC DISORDERS MYOCARDIAL INFARCTION FALSE 1 1 7 14.28571 1 14.3 1
GASTROINTESTINAL DISORDERS DYSPEPSIA FALSE 1 1 7 14.28571 1 14.3 1
GASTROINTESTINAL DISORDERS NAUSEA FALSE 2 7 7 28.57143 2 28.6 7
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS APPLICATION SITE DERMATITIS FALSE 1 2 7 14.28571 1 14.3 2
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS APPLICATION SITE ERYTHEMA FALSE 3 3 7 42.85714 3 42.9 3
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS APPLICATION SITE IRRITATION FALSE 2 4 7 28.57143 2 28.6 4

2.2.2.2 Sunburst

The sunburstClinData function visualizes the counts of hierarchical data in nested circles.

The different groups are visualized from the biggest class (root node) in the center of the visualization to the smallest sub-groups (leaves) on the outside of the circles.

The size of the different segments is relative the respective counts.

dataSunburst <- tableAE

dataSunburst$m <- as.numeric(dataSunburst$m)

sunburstClinData(
    data = dataSunburst,
    vars = c("AESOC", "AEDECOD"),
    valueVar = "m", 
    valueLab = "Number of adverse events",
    pathVar = if(interactive()) "patientProfileLink",
    pathLab = clinUtils::getLabelVar(var = "USUBJID", labelVars = labelVars),
    table = FALSE,
    labelVars = labelVars
)
Overall: 58GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS: 18GASTROINTESTINAL DISORDERS: 8MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS: 5INFECTIONS AND INFESTATIONS: 4NERVOUS SYSTEM DISORDERS: 4PSYCHIATRIC DISORDERS: 4RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS: 4SKIN AND SUBCUTANEOUS TISSUE DISORDERS: 3INJURY, POISONING AND PROCEDURAL COMPLICATIONS: 2METABOLISM AND NUTRITION DISORDERS: 2RENAL AND URINARY DISORDERS: 2CARDIAC DISORDERS: 1INVESTIGATIONS: 1APPLICATION SITE PRURITUS: 5APPLICATION SITE IRRITATION: 4APPLICATION SITE ERYTHEMA: 3APPLICATION SITE DERMATITIS: 2SECRETION DISCHARGE: 2FATIGUE: 1SUDDEN DEATH: 1NAUSEA: 7DYSPEPSIA: 1MUSCULAR WEAKNESS: 2BACK PAIN: 1FLANK PAIN: 1SHOULDER PAIN: 1LOWER RESPIRATORY TRACT INFECTION: 2PNEUMONIA: 2AMNESIA: 2LETHARGY: 1PARTIAL SEIZURES WITH SECONDARY GENERALISATION: 1COMPLETED SUICIDE: 1CONFUSIONAL STATE: 1DEPRESSED MOOD: 1HALLUCINATION, VISUAL: 1COUGH: 2DYSPNOEA: 1EPISTAXIS: 1ERYTHEMA: 2ACTINIC KERATOSIS: 1JOINT DISLOCATION: 1SKIN LACERATION: 1DECREASED APPETITE: 2CALCULUS URETHRAL: 1INCONTINENCE: 1MYOCARDIAL INFARCTION: 1NASAL MUCOSA BIOPSY: 1
Number of adverse events by Primary System Organ Class and Dictionary-Derived Term

2.2.2.3 Treemap

A treemap visualizes the counts of the hierarchical data in nested rectangles. The area of each rectangle is proportional to the counts of the respective group.

Note, that a treemap can also be colored accordingly to a meaningful variable. For instance, if we show adverse events, we might color the plot by severity. This can be achieved with the colorVar parameter.

dataTreemap <- tableAE

dataTreemap$m <- as.numeric(dataTreemap$m)

treemapClinData(
    data = dataTreemap,
    vars = c("AESOC", "AEDECOD"),
    valueVar = "m",
    valueLab = "Number of adverse events",
    pathVar = if(interactive()) "patientProfileLink",
    pathLab = clinUtils::getLabelVar(var = "USUBJID", labelVars = labelVars),
    table = FALSE,
    labelVars = labelVars
)
Overall: 58GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS: 18GASTROINTESTINAL DISORDERS: 8MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS: 5INFECTIONS AND INFESTATIONS: 4NERVOUS SYSTEM DISORDERS: 4PSYCHIATRIC DISORDERS: 4RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS: 4SKIN AND SUBCUTANEOUS TISSUE DISORDERS: 3INJURY, POISONING AND PROCEDURAL COMPLICATIONS: 2METABOLISM AND NUTRITION DISORDERS: 2RENAL AND URINARY DISORDERS: 2CARDIAC DISORDERS: 1INVESTIGATIONS: 1APPLICATION SITE PRURITUS: 5APPLICATION SITE IRRITATION: 4APPLICATION SITE ERYTHEMA: 3APPLICATION SITE DERMATITIS: 2SECRETION DISCHARGE: 2FATIGUE: 1SUDDEN DEATH: 1NAUSEA: 7DYSPEPSIA: 1MUSCULAR WEAKNESS: 2BACK PAIN: 1FLANK PAIN: 1SHOULDER PAIN: 1LOWER RESPIRATORY TRACT INFECTION: 2PNEUMONIA: 2AMNESIA: 2LETHARGY: 1PARTIAL SEIZURES WITH SECONDARY GENERALISATION: 1COMPLETED SUICIDE: 1CONFUSIONAL STATE: 1DEPRESSED MOOD: 1HALLUCINATION, VISUAL: 1COUGH: 2DYSPNOEA: 1EPISTAXIS: 1ERYTHEMA: 2ACTINIC KERATOSIS: 1JOINT DISLOCATION: 1SKIN LACERATION: 1DECREASED APPETITE: 2CALCULUS URETHRAL: 1INCONTINENCE: 1MYOCARDIAL INFARCTION: 1NASAL MUCOSA BIOPSY: 1
Number of adverse events by Primary System Organ Class and Dictionary-Derived Term

2.2.2.4 Barplot

A barplot visualizes the counts for one single variable in a specific order.

dataPlot <- subset(tableAE, AEDECOD != "Total")

dataPlot$n <- as.numeric(dataPlot$n)

# create plot
barplotClinData(
    data = dataPlot,
    xVar = "AEDECOD", 
    yVar = "n",
    yLab = "Number of patients with adverse events",
    textVar = "n",
    labelVars = labelVars,
    pathVar = if(interactive()) "patientProfileLink",
    pathLab = clinUtils::getLabelVar(var = "USUBJID", labelVars = labelVars),
    table = FALSE
)
4322111111111111111111111111111111APPLICATION SITE PRURITUSAPPLICATION SITE ERYTHEMAAPPLICATION SITE IRRITATIONNAUSEAACTINIC KERATOSISAMNESIAAPPLICATION SITE DERMATITISBACK PAINCALCULUS URETHRALCOMPLETED SUICIDECONFUSIONAL STATECOUGHDECREASED APPETITEDEPRESSED MOODDYSPEPSIADYSPNOEAEPISTAXISERYTHEMAFATIGUEFLANK PAINHALLUCINATION, VISUALINCONTINENCEJOINT DISLOCATIONLETHARGYLOWER RESPIRATORY TRACT INFECTIONMUSCULAR WEAKNESSMYOCARDIAL INFARCTIONNASAL MUCOSA BIOPSYPARTIAL SEIZURES WITH SECONDARY GENERALISATIONPNEUMONIASECRETION DISCHARGESHOULDER PAINSKIN LACERATIONSUDDEN DEATH01234
Number of patients with adverse events vs Dictionary-Derived TermDictionary-Derived TermNumber of patients with adverse events

2.2.3 Continuous variable

dataVSDIABP <- subset(dataADaMCDISCP01$ADVS, 
    PARAMCD == "DIABP" & ANL01FL == "Y" &
    AVISIT %in% c("Baseline", "Week 2", "Week 4", "Week 6", "Week 8")
)

# add link to patient profiles report

# add path in data
if(interactive()){
    
    dataVSDIABP$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataVSDIABP$USUBJID), ".pdf"
    )

    # add link in data (for attached table)
    dataVSDIABP$patientProfileLink <- with(dataVSDIABP,
        paste0(
            '<a href="', patientProfilePath, 
            '" target="_blank">', USUBJID, '</a>'
        )
    )

}

2.2.3.1 Compute summary statistics

# Specify extra summarizations besides the standard stats
# When the data is summarized,
# the patient profile path are summarized
# as well across patients
# (the paths should be collapsed with: ', ')
if(interactive())
    statsExtraPP <- list(
        statPatientProfilePath = function(data) 
          toString(sort(unique(data$patientProfilePath))),
        statPatientProfileLink = function(data)
          toString(sort(unique(data$patientProfileLink)))
    )
    
# get default counts + stats with subjects profiles path
statsPP <- c(
    inTextSummaryTable::getStats(x = dataVSDIABP$AVAL, type = "summary"),
    if(interactive())
        list(
            patientProfilePath = quote(statPatientProfilePath),
            patientProfileLink = quote(statPatientProfileLink)
        )
)

# compute summary table of actual value
summaryTableCont <- inTextSummaryTable::computeSummaryStatisticsTable(
    
    data = dataVSDIABP,
    rowVar = c("AVISIT", "ATPT"),
    var = "AVAL",
    
    labelVars = labelVars,
    
    # statistics of interest
    # for DT output, include columns with patients
    stats = statsPP, 
    # add extra 'statistic': concatenate subject IDs
    statsExtra = if(interactive())  statsExtraPP

)
knitr::kable(head(summaryTableCont, 1))
AVISIT ATPT isTotal statN statm statMean statSD statSE statMedian statMin statMax statPercTotalN statPercN n Mean SD SE Median Min Max % m
Baseline AFTER LYING DOWN FOR 5 MINUTES FALSE 7 7 70.71429 9.214378 3.482708 70 56 84 7 100 7 70.7 9.2 3.48 70.0 56 84 100 7

2.2.3.2 Plot error bars/confidence intervals

dataPlot <- subset(summaryTableCont, !isTotal)

errorbarClinData(
    data = dataPlot,
    xVar = "AVISIT",
    colorVar = "ATPT",
    
    # use non-rounded statistics for the plot
    yVar = "statMean", 
    yErrorVar = "statSE", 
    
    # display rounded stats in the hover + n: number of subjects
    hoverVars = c("AVISIT", "ATPT", "n", "Mean", "SE"),
    
    yLab = "Mean", yErrorLab = "Standard Error",
    title = "Diastolic Blood Pressure summary profile by actual visit and and analysis timepoint",
    
    # include lines connecting the error bars
    mode = "markers+lines", 
    
    table = FALSE, labelVars = labelVars,
    pathVar = if(interactive()) "patientProfileLink",
    pathLab = clinUtils::getLabelVar(var = "USUBJID", labelVars = labelVars)

)
BaselineWeek 2Week 4Week 6Week 865707580
Analysis TimepointAFTER LYING DOWN FOR 5 MINUTESAFTER STANDING FOR 1 MINUTEAFTER STANDING FOR 3 MINUTESDiastolic Blood Pressure summary profile by actual visit and and analysis timepointAnalysis VisitMean and Standard Error

2.2.3.3 Boxplot

A boxplot visualizes the distribution of a continuous variable of interest versus specific categorical variables.

This visualization doesn’t rely on pre-computed statistics, so the continuous variable of interest is directly passed to the functionality.

dataPlot <- subset(dataADaMCDISCP01$ADVS, 
    PARAMCD == "DIABP" & ANL01FL == "Y" &
    AVISIT %in% c("Baseline", "Week 2", "Week 4", "Week 6", "Week 8")
)

# link to patient profiles
if(interactive())
    dataPlot$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataPlot$USUBJID), ".pdf"
    )

boxplotClinData(
    data = dataPlot,
    xVar = "AVISIT", 
    yVar = "AVAL",
    colorVar = "TRTA",
    facetVar = "ATPT",
    title = "Diastolic Blood Pressure distribution by actual visit and analysis timepoint",
    yLab = "Actual value of the Diastolic Blood Pressure parameter (mmHg)",
    labelVars = labelVars,
    pathVar = if(interactive()) "patientProfilePath",
    pathLab = clinUtils::getLabelVar(var = "USUBJID", labelVars = labelVars),
    table = FALSE
)
556065707580855060708090100BaselineWeek 2Week 4Week 6Week 85060708090100
Actual TreatmentPlaceboXanomeline High DoseXanomeline Low DoseDiastolic Blood Pressure distribution by actual visit and analysis timepointAFTER LYING DOWN FOR 5 MINUTESAFTER STANDING FOR 1 MINUTEAFTER STANDING FOR 3 MINUTESActual value of the Diastolic Blood Pressure parameter (mmHg)Analysis Visit

2.3 Multiple visualizations in a loop

To include multiple clinical data visualizations (with or without attached table) in a loop (in the same Rmarkdown chunk), the list of visualizations should be passed to the knitPrintListObjects function of the clinUtils package.

# consider only restricted set of lab parameters
dataPlot <- subset(dataLB, PARAMCD %in% c("SODIUM", "K"))

# link to patient profiles
if(interactive())
    dataPlot$patientProfilePath <- paste0(
        "patientProfiles/subjectProfile-", 
        sub("/", "-", dataPlot$USUBJID), ".pdf"
    )

# 1) create plot+table for each laboratory parameter:
library(plyr) # for ddply
plotsLab <- dlply(dataPlot, "PARAMCD", function(dataLBParam){
      
      paramcd <- unique(dataLBParam$PARAMCD)
      
      scatterplotClinData(
          data = dataLBParam, 
          xVar = "ADY",
          yVar = "LBSTRESN",
          aesPointVar = list(color = "TRTP", fill = "TRTP"),
          aesLineVar = list(group = "USUBJID", color = "TRTP"),
          labelVars = labelVars,
          title = paste("Actual value of", 
              getLabelParamcd(
                  paramcd = paramcd, data = dataLBParam, paramcdVar = "PARAMCD", paramVar = "PARAM"
              )
          ),
          # include link to patient profiles:
          pathVar = if(interactive())   "patientProfilePath",
          table = FALSE, 
          # important: each plot should have an unique ID!
          # for unique relationship of interactivity between plot <-> table
          id = paste("labProfileLoop", paramcd, sep = "-")
      )
      
    })

# include this output in the report:
listLabels <- getLabelParamcd(
    paramcd = names(plotsLab), data = dataLB, paramcdVar = "PARAMCD", paramVar = "PARAM"
)
clinUtils::knitPrintListObjects(
    xList = plotsLab, 
    titles = listLabels, titleLevel = 4
)

2.3.0.1 Potassium (mmol/L)

0501001503.63.94.24.5
Planned Treatment(Placebo,1)(Xanomeline High Dose,1)(Xanomeline Low Dose,1)PlaceboXanomeline High DoseXanomeline Low DoseActual value of Potassium (mmol/L)Analysis Relative DayNumeric Result/Finding in Standard Units

2.3.0.2 Sodium (mmol/L)

050100150132.5135.0137.5140.0142.5
Planned Treatment(Placebo,1)(Xanomeline High Dose,1)(Xanomeline Low Dose,1)PlaceboXanomeline High DoseXanomeline Low DoseActual value of Sodium (mmol/L)Analysis Relative DayNumeric Result/Finding in Standard Units

2.4 Watermark

A watermark can be included in any of the visualization of the package, from a specified file, via the watermark parameter.

In this example, an exploratory watermark is added to the adverse events barplot.

# create a file with a 'EXPLORATORY' watermark
file <- tempfile(pattern = "watermark", fileext = ".png")
getWatermark(file = file)

# create plot
barplotClinData(
  data = subset(tableAE, AEDECOD != "Total"),
  xVar = "AEDECOD", 
  yVar = "n",
  yLab = "Number of patients with adverse events",
  textVar = "n",
  labelVars = labelVars,
  # include the watermark
  watermark = file,
  table = FALSE
)
4322111111111111111111111111111111APPLICATION SITE PRURITUSAPPLICATION SITE ERYTHEMAAPPLICATION SITE IRRITATIONNAUSEAACTINIC KERATOSISAMNESIAAPPLICATION SITE DERMATITISBACK PAINCALCULUS URETHRALCOMPLETED SUICIDECONFUSIONAL STATECOUGHDECREASED APPETITEDEPRESSED MOODDYSPEPSIADYSPNOEAEPISTAXISERYTHEMAFATIGUEFLANK PAINHALLUCINATION, VISUALINCONTINENCEJOINT DISLOCATIONLETHARGYLOWER RESPIRATORY TRACT INFECTIONMUSCULAR WEAKNESSMYOCARDIAL INFARCTIONNASAL MUCOSA BIOPSYPARTIAL SEIZURES WITH SECONDARY GENERALISATIONPNEUMONIASECRETION DISCHARGESHOULDER PAINSKIN LACERATIONSUDDEN DEATH1234
Number of patients with adverse events vs Dictionary-Derived TermDictionary-Derived TermNumber of patients with adverse events

3 Palettes

3.1 Set palette for the entire session

Palette for the colors and shapes associated with specific variables can be set for all clinical data visualizations at once by setting the clinDataReview.colors and clinDataReview.shapes options at the start of the R session.

Please see the clinUtils package for the default colors and shapes.

# display default palettes
colorsDefault <- getOption("clinDataReview.colors")
str(colorsDefault)
## function (n, alpha = 1, begin = 0, end = 1, direction = 1, option = "D")
shapesDefault <- getOption("clinDataReview.shapes")
str(shapesDefault)
##  int [1:24] 21 22 23 24 25 0 1 2 3 4 ...
timeProfileIntervalPlot(
    data = dataAE,
    paramVar = "USUBJID",
    # time-variables
    timeStartVar = "ASTDY",
    timeEndVar = "AENDY",
    colorVar = "AESEV",
    timeStartShapeVar = "AESTFLG",
    timeEndShapeVar = "AEENFLG",
    labelVars = labelVars
)
−800−600−400−200020001-701-114801-701-119201-701-121101-704-144501-710-108301-718-137101-718-1427
Severity/IntensityMILDMODERATESEVEREAnalysis Start Relative Day and Analysis End Relative Day

The palettes can be set for all visualizations, e.g. at the start of the R session, with:

# change palettes for the entire R session
options(clinDataReview.colors = c("gold", "pink", "cyan"))
options(clinDataReview.shapes = clinShapes)

In case the palette contains less elements than available in the data, these are replicated.

timeProfileIntervalPlot(
    data = dataAE,
    paramVar = "USUBJID",
    # time-variables
    timeStartVar = "ASTDY",
    timeEndVar = "AENDY",
    colorVar = "AESEV",
    timeStartShapeVar = "AESTFLG",
    timeEndShapeVar = "AEENFLG",
    labelVars = labelVars
)
−800−600−400−200020001-701-114801-701-119201-701-121101-704-144501-710-108301-718-137101-718-1427
Severity/IntensityMILDMODERATESEVEREAnalysis Start Relative Day and Analysis End Relative Day

Palettes are reset to the default patient profiles palettes at the start of a new R session, or by setting:

# change palettes for the entire R session
options(clinDataReview.colors = colorsDefault)
options(clinDataReview.shapes = shapesDefault)

4 Appendix

4.1 Session info

R version 4.5.0 RC (2025-04-04 r88126) Platform: x86_64-pc-linux-gnu Running under: Ubuntu 24.04.1 LTS

Matrix products: default BLAS: /home/lcougnaud/Documents/R/R-4.5.0/lib/libRblas.so LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 LAPACK version 3.12.0

locale: [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
[3] LC_TIME=en_US.UTF-8 LC_COLLATE=C
[5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
[7] LC_PAPER=en_US.UTF-8 LC_NAME=C
[9] LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C

time zone: Europe/Brussels tzcode source: system (glibc)

attached base packages: [1] stats graphics grDevices utils datasets methods base

other attached packages: [1] plyr_1.8.9 plotly_4.10.4 ggplot2_3.5.2
[4] clinUtils_0.2.0 clinDataReview_1.6.2 knitr_1.50

loaded via a namespace (and not attached): [1] gtable_0.3.6 xfun_0.52 bslib_0.9.0
[4] htmlwidgets_1.6.4 ggrepel_0.9.6 vctrs_0.6.5
[7] tools_4.5.0 crosstalk_1.2.1 generics_0.1.3
[10] parallel_4.5.0 tibble_3.2.1 pkgconfig_2.0.3
[13] data.table_1.17.0 uuid_1.2-1 lifecycle_1.0.4
[16] flextable_0.9.7 compiler_4.5.0 farver_2.1.2
[19] stringr_1.5.1 textshaping_1.0.0 munsell_0.5.1
[22] httpuv_1.6.15 fontquiver_0.2.1 fontLiberation_0.1.0
[25] htmltools_0.5.8.1 sass_0.4.9 yaml_2.3.10
[28] lazyeval_0.2.2 pillar_1.10.2 later_1.4.2
[31] jquerylib_0.1.4 tidyr_1.3.1 openssl_2.3.2
[34] DT_0.33 cachem_1.1.0 jsonvalidate_1.5.0
[37] mime_0.13 fontBitstreamVera_0.1.1 zip_2.3.2
[40] tidyselect_1.2.1 digest_0.6.37 stringi_1.8.7
[43] reshape2_1.4.4 dplyr_1.1.4 purrr_1.0.4
[46] bookdown_0.42 labeling_0.4.3 forcats_1.0.0
[49] cowplot_1.1.3 fastmap_1.2.0 grid_4.5.0
[52] colorspace_2.1-1 cli_3.6.4 magrittr_2.0.3
[55] base64enc_0.1-3 withr_3.0.2 gdtools_0.4.2
[58] scales_1.3.0 promises_1.3.2 officer_0.6.8
[61] rmarkdown_2.29 httr_1.4.7 inTextSummaryTable_3.3.3 [64] ragg_1.4.0 askpass_1.2.1 hms_1.1.3
[67] shiny_1.10.0 evaluate_1.0.3 haven_2.5.4
[70] viridisLite_0.4.2 rlang_1.1.6 Rcpp_1.0.14
[73] xtable_1.8-4 glue_1.8.0 xml2_1.3.8
[76] jsonlite_2.0.0 R6_2.6.1 systemfonts_1.2.2