Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

r

ggplot2

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)

like image 234
Matt L. Avatar asked Dec 21 '25 10:12

Matt L.


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.

like image 149
hugh-allan Avatar answered Dec 23 '25 00:12

hugh-allan


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

like image 26
Daniel Avatar answered Dec 23 '25 00:12

Daniel



Donate For Us

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