How to vary "position_dodge" for one point and not others in a series in ggplot2

Viewed 302

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)

2 Answers

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)))

enter image description here

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

enter image description here

?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()

enter image description here

Related