Problem: My dataset has a shared baseline (timepoint 1) with 2 repeated measures. The followup points are repeated with 2 different conditions (ie a cross-over). When plotting, the results, the error bars and data points overlap.
library(tidyverse)
set.seed(11)
df_test <-
tibble(group = factor(rep(c("A", "B"), each = 3)),
timepoint = factor(rep(1:3, 2)),
y = c(0, rnorm(2, mean = 0, sd = .2), 0, rnorm(2, mean = .7, sd = 0.35))) %>%
mutate(ymax = y + .2,
ymin = y - .2)
# plot_nododge <-
df_test %>%
ggplot(aes(timepoint, y,
group = group,
shape = group,
fill = group)) +
geom_linerange(linetype = 1,
aes(ymin = ymin,
ymax = ymax)) +
geom_point() +
geom_line()

Solution with position_dodge(): This solution fixes the overlap, but the "Baseline" point is actually a single measure. I would like for this to be a single point to avoid confusion, but still dodge the followup points.
Is there a simple solution to this that I'm missing?
I would like to use a custom dodge for each point (eg scale_position_dodge_identity), but position is not accepted as an aesthetic.
# plot1 <-
df_test %>%
ggplot(aes(timepoint, y,
group = group,
shape = group,
fill = group)) +
geom_linerange(linetype = 1,
position = position_dodge(.2),
aes(ymin = ymin,
ymax = ymax)) +
geom_point(position = position_dodge(.2)) +
geom_line(position = position_dodge(.2))

Solution and expansion* Maybe something better will be developed in the future but manually adjusting the position seems best for now. Since the point shape and color should also reflect the different "Baseline" measure and the x-axis should really have nice labels, I've edited to show some more finishing touches.
df_test %>%
mutate(shape_var = case_when(timepoint == 1 ~ 22,
group == "A" ~ 21,
group == "B" ~ 24),
fill_var = case_when(timepoint == 1 ~ "black",
group == "A" ~ "red",
group == "B" ~ "blue"),
timepoint2 = as.numeric(as.character(timepoint)),
timepoint2 = timepoint2 + 0.05*(timepoint2 > 1 & group == "B"),
timepoint2 = timepoint2 - 0.05*(timepoint2 > 1 & group == "A")) %>%
ggplot(aes(timepoint2, y,
group = group,
shape = shape_var,
fill = fill_var)) +
geom_linerange(aes(ymin = ymin,
ymax = ymax)) +
geom_line() +
geom_point(size = 2) +
scale_x_continuous(breaks = 1:3, # limit to selected time points
labels = c("Baseline", "time 1", "time 2"), # label like a discrete scale
limits = c(.75, 3.25)) + # give it some room
scale_shape_identity(guide = "legend",
name = "Treatment",
breaks = c(21, 24), # Defines legend order too; only label the treatment groups
labels = c("A", "B" )) + # this part is tricky - could easily reverse them
scale_fill_identity(guide = "legend",
name = "Treatment",
breaks = c("red", "blue"),# Defines legend order too; only label the treatment groups
labels = c("A", "B" )) + # this part is tricky - could easily reverse them
labs(x=NULL)

Created on 2021-09-19 by the reprex package (v2.0.1)
After some experimentation, I have found a pretty good way to do this. Seems like you can use position_dodge2(width = c(...)) to specify individual dodge widths.
For your example, specify width = c(0.000001, 0.2, 0.2):
df_test %>%
ggplot(aes(timepoint, y, group = group, shape = group, fill = group)) +
geom_linerange(linetype = 1, position = position_dodge2(width = c(0.000001, 0.2, 0.2)), aes(ymin = ymin, ymax = ymax)) +
geom_point(position = position_dodge2(width = c(0.000001, 0.2, 0.2))) +
geom_line(position = position_dodge2(width = c(0.000001, 0.2, 0.2)))

Initially, I tried setting width = c(0, 0.2, 0.2), but that didn't behave as expected:

?position_dodge doesn't explicity explain using a string of values as the dodge widths, so I don't know why it doesn't work when you use zero as a width. A value of 0.000001 is 'close enough' to zero that you can't tell by looking at the figure, so hopefully this will suffice.
One solution could be to just change the position of the original points directly in the data set for the purpose of the plot:
df_test %>%
mutate(timepoint = as.numeric(as.character(timepoint))) %>%
mutate(timepoint = timepoint + 0.1*(timepoint > 1 & group == "B")) %>%
ggplot(aes(timepoint, y,
group = group,
shape = group,
fill = group)) +
geom_linerange(linetype = 1,
aes(ymin = ymin,
ymax = ymax)) +
scale_x_continuous(breaks = unique(as.numeric(as.character(df_test$timepoint))))+
geom_point() +
geom_line()

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With