Skip to contents

A swimmer plot gives every patient a lane: how long they were on treatment or on study, when they responded, when they progressed and who is still going. It is the standard picture of response in single arm and early phase trials, where the course of each patient matters as much as the summary. ggswimmer() draws one with each lane’s record under the pointer.

A first swimmer plot

The survival package ships myeloid, a simulated data set based on a trial in acute myeloid leukemia, with the times of complete response, stem cell transplant, relapse and death. The first 30 patients make a readable plot. The events go in a data frame of their own, one row per event, with the patient’s id, the time and what happened:

aml <- subset(survival::myeloid, id <= 30)
aml$arm <- paste("Arm", aml$trt)
month <- 30.44
events <- rbind(
  data.frame(id = aml$id, time = aml$crtime / month, event = "Complete response"),
  data.frame(id = aml$id, time = aml$txtime / month, event = "Transplant"),
  data.frame(id = aml$id, time = aml$rltime / month, event = "Relapse"),
  data.frame(id = aml$id, time = ifelse(aml$death == 1, aml$futime / month, NA),
             event = "Death")
)
events <- events[!is.na(events$time), ]

ggswimmer(aml, id, futime / month, events = events, group = arm,
          ongoing = death == 0, hover = c("sex", "flt3"),
          ongoing_label = "Alive at last follow-up",
          xlab = "Months since randomization",
          title = "Acute myeloid leukemia, 30 patients")

Hover over a lane to read the patient’s arm, the columns named in hover, the first time of each event and whether they are still being followed; the other lanes fade. Click it for the patient’s events in order, beside their full record. The buttons under the plot reorder the lanes, longest first, by arm, by best response or in the order of the data, and the lanes slide into place.

Events

end and ongoing take expressions of the columns of data, as above. Events are drawn by their wording, whatever the case:

wording mark
complete, CR star
partial, PR, response, remission triangle
progression, PD, relapse, recurrence diamond
death, died cross
anything else a circle or square in its own color

Waterfall and trajectories

In a solid tumor trial the lanes are only half the story: how far each tumor shrank matters too. waterfall gives each patient’s best percent change from baseline, drawn beside their lane, and trajectories their change at each assessment, drawn under the lanes on the same time axis. The three panels share one row, or one line, per patient: hovering over any of them lights the patient in all three. The order “Best change” sorts the lanes as a waterfall, and the response and progression thresholds, by default the 30 percent decrease and 20 percent increase of RECIST 1.1 for target lesions, are marked on both change panels with a count past each.

set.seed(4)
n <- 16
pts <- data.frame(patient = sprintf("P%02d", 1:n), arm = rep(c("Drug", "Control"), each = n / 2),
                  months = round(pmax(2, rexp(n, 1 / 10)), 1))
slope <- ifelse(pts$arm == "Drug", -9, -2) + rnorm(n, 0, 5)
traj <- do.call(rbind, lapply(1:n, function(i) {
  t <- seq(2, pts$months[i], by = 2)
  if (!length(t)) return(NULL)
  data.frame(patient = pts$patient[i], time = t,
             change = round(pmax(-100, cumsum(slope[i] + rnorm(length(t), 0, 6)))))
}))
pts$best <- as.numeric(tapply(traj$change, traj$patient, min)[pts$patient])

ggswimmer(pts, patient, months, group = arm, waterfall = best,
          trajectories = traj, sort = "change", xlab = "Months since first dose")

These data are simulated. The measured change is not the whole of RECIST: new lesions and other progression count too. response in the result holds the counts past each threshold.

Options

argument effect
start where each bar starts; zero by default
group the column that colors the bars, such as the arm
ongoing, ongoing_label who gets an arrow, and what it means
sort the order of a static copy: "duration", "group", "response", "change" or "data"
palette colors for the groups
hover columns for each lane’s hover card
waterfall, trajectories best change beside the lanes, and change over time under them
thresholds, change_label the response and progression thresholds, and the axis label

On a dark page the plot takes a dark palette of its own, and graph_save(s, "swimmer.png", theme = "dark") writes a dark static copy.