Search code examples
rggplot2facet-wrapfacet-gridgeom-point

Control jitter of geom_point with consistent placement in ggplot


I'm making a plot with facet_grid() and use both geom_point() and geom_errorbar() to make something resembling a coefficient plot, but where I have two groups I'm plotting on top of each other.

In order to not make them overlap, I've specified position = position_jitter(seed=100) within both geom_point() and geom_errorbar(), so that the points and bars stay together.

However, because jittering is inherently random (as far as I'm aware), across different facets I'm ending up with the points/bars for each group sometimes being on one side and then sometimes being on the other. See below for a visual example of what I mean.

The distance between them also seems to be random; sometimes the points/bars are right next to each other, sometimes a bit far apart.

So my question is: Is there a way to be able to specify the magnitude and direction of the jittering, by group? (Or perhaps a totally different way to do this without jitter)?

EDIT: ADDED CODE/DATA HERE IN CASE THERE'S A NON-JITTER WAY TO DO THIS

library(ggplot2)
library(dplyr)
library(patchwork)

dex$group <- factor(dex$outcome_type2, c("TPW", "Money", "Support"))

plot_fun <- function(.data) {
  remove_facet <- if (unique(.data$group) %in% c("TPW", "Money")) {
    theme(strip.text.y = element_blank())
  }

  .data$outcome_type <- forcats::fct_relevel(
    .data$outcome_type,
    "TPW T", "TPW I", "TPW E", "Money1", "Money2", "Support1", "Support2")

  .data %>%
    tidyr::complete(outcome_type, wave = unique(dex$wave), attend) %>%
    ggplot(aes(x = outcome_type, y = Mean, color = attend,
               ymin = LowCI, ymax = UpperCI)) +
    geom_errorbar(linewidth = 1,
                  width = 0,
                  position = position_jitter(width = 0.25, seed = 100)) +
    geom_point(size = 1.9,
               shape = 16,
               position = position_jitter(width = 0.25, seed = 100)) +
    facet_grid(wave ~ outcome_type,
              scales = "free") +
    scale_fill_manual(values = c("Attend" = "red",
                                 "Not_Attend" = "blue"),
                      aesthetics = c("fill", "color"), guide = guide_legend(element_blank)) +
    theme_classic(base_size = 13) +
    remove_facet
}

dex_split <- split(dex, dex$group)

lapply(dex_split, plot_fun) %>%
  wrap_plots() +
  plot_layout(widths = c(3, 2, 2),
              guides = "collect") &
  labs(x = NULL, y = NULL, fill = NULL) &
  theme(axis.text.x = element_blank(),
        strip.text.x = element_text(size = 11),
        legend.position = "bottom",
        legend.title = element_blank(),
        plot.title = element_text(size = 15,
                                  face = "bold"),
        plot.subtitle = element_text(size = 13),
        panel.grid.major = element_blank())

Data:

dex <- structure(list(v1 = c("tpw_e_w1", "tpw_e_w1", "tpw_e_w2", "tpw_e_w2", 
"tpw_e_w3", "tpw_e_w3", "tpw_i_w1", "tpw_i_w1", "tpw_i_w2", "tpw_i_w2", 
"tpw_i_w3", "tpw_i_w3", "tpw_t_w1", "tpw_t_w1", "tpw_t_w2", "tpw_t_w2", 
"tpw_t_w3", "tpw_t_w3", "money1_w1", "money1_w1", "money1_w3", 
"money1_w3", "money2_w2", "money2_w2", "money2_w3", "money2_w3", 
"support1_score_w1", "support1_score_w1", "support1_score_w3", 
"support1_score_w3", "support2_score_w1", "support2_score_w1", 
"support2_score_w3", "support2_score_w3"), attend = c("Not_Attend", 
"Attend", "Not_Attend", "Attend", "Not_Attend", "Attend", "Not_Attend", 
"Attend", "Not_Attend", "Attend", "Not_Attend", "Attend", "Not_Attend", 
"Attend", "Not_Attend", "Attend", "Not_Attend", "Attend", "Not_Attend", 
"Attend", "Not_Attend", "Attend", "Not_Attend", "Attend", "Not_Attend", 
"Attend", "Not_Attend", "Attend", "Not_Attend", "Attend", "Not_Attend", 
"Attend", "Not_Attend", "Attend"), Mean = c(-0.01, 0.32, -0.01, 
0.35, -0.03, 0.65, 0, 0.12, 0, 0.26, -0.03, 0.65, -0.01, 0.23, 
-0.01, 0.32, -0.03, 0.69, 0.01, 0.45, 0, 0.44, 0, 0.56, 0.1, 
0.73, 0.01, -0.23, 0, -0.12, 0.01, -0.16, -0.01, -0.22), LowCI = c(-0.05, 
0.07, -0.05, 0.05, -0.08, 0.34, -0.04, -0.12, -0.05, -0.05, -0.07, 
0.35, -0.05, -0.02, -0.05, 0.02, -0.08, 0.38, -0.04, 0.17, -0.05, 
0.17, -0.05, 0.25, 0.05, 0.39, -0.03, -0.45, -0.05, -0.39, -0.03, 
-0.32, -0.06, -0.5), UpperCI = c(0.03, 0.56, 0.04, 0.65, 0.01, 
0.97, 0.04, 0.37, 0.04, 0.57, 0.02, 0.95, 0.03, 0.47, 0.04, 0.62, 
0.01, 0.99, 0.05, 0.73, 0.04, 0.71, 0.04, 0.87, 0.15, 1.07, 0.05, 
-0.01, 0.05, 0.16, 0.06, 0, 0.05, 0.06), outcome_type = c("TPW E", 
"TPW E", "TPW E", "TPW E", "TPW E", "TPW E", "TPW I", "TPW I", 
"TPW I", "TPW I", "TPW I", "TPW I", "TPW T", "TPW T", "TPW T", 
"TPW T", "TPW T", "TPW T", "Money1", "Money1", "Money1", "Money1", 
"Money2", "Money2", "Money2", "Money2", "Support1", "Support1", 
"Support1", "Support1", "Support2", "Support2", "Support2", "Support2"
), wave = c("Wave 1", "Wave 1", "Wave 2", "Wave 2", "Wave 3", 
"Wave 3", "Wave 1", "Wave 1", "Wave 2", "Wave 2", "Wave 3", "Wave 3", 
"Wave 1", "Wave 1", "Wave 2", "Wave 2", "Wave 3", "Wave 3", "Wave 1", 
"Wave 1", "Wave 3", "Wave 3", "Wave 2", "Wave 2", "Wave 3", "Wave 3", 
"Wave 1", "Wave 1", "Wave 3", "Wave 3", "Wave 1", "Wave 1", "Wave 3", 
"Wave 3"), outcome_type2 = c("TPW", "TPW", "TPW", "TPW", "TPW", 
"TPW", "TPW", "TPW", "TPW", "TPW", "TPW", "TPW", "TPW", "TPW", 
"TPW", "TPW", "TPW", "TPW", "Money", "Money", "Money", "Money", 
"Money", "Money", "Money", "Money", "Support", "Support", "Support", 
"Support", "Support", "Support", "Support", "Support")), row.names = c(NA, 
-34L), spec = structure(list(cols = list(v1 = structure(list(), class = c("collector_character", 
"collector")), attend = structure(list(), class = c("collector_character", 
"collector")), Mean = structure(list(), class = c("collector_double", 
"collector")), LowCI = structure(list(), class = c("collector_double", 
"collector")), UpperCI = structure(list(), class = c("collector_double", 
"collector")), outcome_type = structure(list(), class = c("collector_character", 
"collector")), wave = structure(list(), class = c("collector_character", 
"collector")), outcome_type2 = structure(list(), class = c("collector_character", 
"collector"))), default = structure(list(), class = c("collector_guess", 
"collector")), delim = ","), class = "col_spec"), class = c("spec_tbl_df", 
"tbl_df", "tbl", "data.frame"))

Solution

  • Your example is far from minimal and when I try your code I do not even see points.

    Having said that, I would use position_dodge instead like in this minimal example:

    library(ggplot2)
    library(dplyr)
    library(tibble)
    
    library(ggplot2)
    library(dplyr)
    library(tibble)
    
    set.seed(123)
    my_data <- tibble(
       x = rep(1:2, each = 200L),
       y = rnorm(400),
       grp = gl(2, 100, 400, paste("Group", 1:2))
    )
    
    ggplot(my_data, 
           aes(x, y, color = grp)) + 
       geom_point(position = position_dodge(0.1), alpha = .1) +
       stat_summary(fun.data = "mean_se", fun.args = qt(0.975, 99L), 
                    width = .05, geom = "errorbar", 
                    position = position_dodge(0.1)) +
       scale_x_continuous(breaks = seq(1, 2, 1), minor_breaks = .5)
    

    Scatterplot where each group is dodged to the side for better legibility

    Alternatively, if you do not like the straight appearance of your points, you cna use a combination of position_jitterdodge (for the points) and position_dodge (for the errorbar):

    ggplot(my_data, 
           aes(x, y, color = grp)) + 
       geom_point(position = position_jitterdodge(0.1, dodge.width = .1), alpha = .1) +
       stat_summary(fun.data = "mean_se", fun.args = qt(0.975, 99L), 
                    width = .05, geom = "errorbar", 
                    position = position_dodge(0.1)) +
       scale_x_continuous(breaks = seq(1, 2, 1), minor_breaks = .5)
    

    Scatterplot where each group is dodged jittered to the side for better legibility