Patchwork aligning nested patches wtih free()?

I'm running into some issues trying to put together several panels of patchworks. I can't seem to figure out how to align axes of nested patches with other, or with other ggplots. I think I might just not quite understand nesting patchworks or how free() works, or maybe this is an underlying issue?

Here's a reprex with some of my attempts and fix it.

# make arbitrary paired data

```{r}
penguin_data <- palmerpenguins::penguins %>% 
  filter(!is.na(bill_length_mm)) %>% 
  mutate(status = "original") %>% 
  rowid_to_column("ID")

penguin_alt <- penguin_data %>% 
  mutate(bill_length_mm    = jitter(bill_length_mm, amount = 10),
         bill_depth_mm     = jitter(bill_depth_mm, amount = 5),
         flipper_length_mm = jitter(flipper_length_mm, amount = 50),
         body_mass_g       = jitter(body_mass_g, amount = 200),
         status = "altered") %>% 
  full_join(penguin_data) %>% 
  mutate(status = fct_relevel(as_factor(status), "original", "altered")) %>% 
  group_by(ID)
penguin_alt
```

# plot individual data plots
```{r, fig.width=2.35, fig.height=5}
plot_A1 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Torgersen") %>% 
  ggplot() +
  geom_line( aes(x = status, y = bill_length_mm, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = bill_length_mm, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Torgersen Island") +
  scale_y_continuous(breaks = seq(0,100,10), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,85)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_A1  

plot_A2 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Biscoe") %>% 
  mutate(bill_length_mm = case_when(status == "original" ~ bill_length_mm, status == "altered" ~ bill_length_mm*1.5)) %>% #add variance
  ggplot() +
  geom_line( aes(x = status, y = bill_length_mm, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = bill_length_mm, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Biscoe Island") +
  scale_y_continuous(breaks = seq(0,100,10), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,85)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_A2
```

```{r, fig.width=2.35, fig.height=5}
plot_B1 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Torgersen") %>% 
  ggplot() +
  geom_line( aes(x = status, y = bill_depth_mm, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = bill_depth_mm, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Torgersen Island") +
  scale_y_continuous(breaks = seq(0,30,5), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,30)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_B1  

plot_B2 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Biscoe") %>% 
  mutate(bill_depth_mm = case_when(status == "original" ~ bill_depth_mm, status == "altered" ~ bill_depth_mm*0.75)) %>% #add variance
  ggplot() +
  geom_line( aes(x = status, y = bill_depth_mm, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = bill_depth_mm, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Biscoe Island") +
  scale_y_continuous(breaks = seq(0,40,5), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,30)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_B2
```

```{r, fig.width=2.35, fig.height=5}
plot_C1 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Torgersen") %>% 
  mutate(flipper_length_mm = case_when(status == "original" ~ flipper_length_mm, 
                                       status == "altered" ~ flipper_length_mm*1.5)) %>% #add variance
  ggplot() +
  geom_line( aes(x = status, y = flipper_length_mm, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = flipper_length_mm, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Torgersen Island") +
  scale_y_continuous(breaks = seq(0,400,50), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,400)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_C1  

plot_C2 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Biscoe") %>% 
  ggplot() +
  geom_line( aes(x = status, y = flipper_length_mm, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = flipper_length_mm, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Biscoe Island") +
  scale_y_continuous(breaks = seq(0,400,50), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,400)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_C2
```

```{r, fig.width=2.35, fig.height=5}
plot_D1 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Torgersen") %>% 
  mutate(body_mass_g = case_when(status == "original" ~ body_mass_g, status == "altered" ~ body_mass_g*1.2)) %>% #add variance
  ggplot() +
  geom_line( aes(x = status, y = body_mass_g, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = body_mass_g, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Torgersen Island") +
  scale_y_continuous(breaks = seq(0,6000,1000), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,6000)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_D1  

plot_D2 <- penguin_alt %>% 
  filter(species == "Adelie" & island == "Biscoe") %>% 
  ggplot() +
  geom_line( aes(x = status, y = body_mass_g, group = ID), linewidth = 1, alpha = 0.5) +
  geom_point(aes(x = status, y = body_mass_g, group = ID), size = 3.5, alpha = 0.9, show.legend = FALSE, shape = 21) +
  labs(x = "Biscoe Island") +
  scale_y_continuous(breaks = seq(0,6000,1000), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,6000)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_D2
```


# combine individual plots
NOTE: I have to do this instead of facets because adding p values to faceted plots doesn't work when you have more than 2 variables you're trying to facet with (penguins and island, and year). `gg4hx` used to work, but broke a while ago, so I have to add p values to individual plots and then patchwork them together into pairs.

```{r, fig.width=3.75, fig.height=5}
plotA <- plot_A1 + plot_A2 + plot_layout(axes = "collect_y", axis_titles = "collect_y")
plotB <- plot_B1 + plot_B2 + plot_layout(axes = "collect_y", axis_titles = "collect_y")
plotC <- plot_C1 + plot_C2 + plot_layout(axes = "collect_y", axis_titles = "collect_y")
plotD <- plot_D1 + plot_D2 + plot_layout(axes = "collect_y", axis_titles = "collect_y")
```


# and now make a panel
I know I could do this in adobe or publisher, but I specifically would like the plots to align by the X/Y axes

```{r, fig.width=7.5, fig.height=10}
(plotA | plotB) / (plotC | plotD)
```

But, the right two plots do not align horizontally. Here it's because plotD's y axis has more digits, but I also have had this issue when the X axis labels ("original"/"altered") are too long and overlap for one specific plot. 

I tried asking for equal widths, but that doesn't change anything.

```{r, fig.width=7.5, fig.height=10}
(plotA | plotB) / (plotC | plotD) +
  plot_layout(widths = c(1,1))
```


I tried to use `free()` to let the y axis labels adjust, and I just can't seem to get it to work. Various combinations of connecting the `free()` plot with `|` or `+` do move them around, but nothing actually lines it up. Same with adding `wrap_plots()` to the other plots. I think I'm just missing something here but I don't know what else I can try. 

```{r, fig.width=7.5, fig.height=10}
(wrap_plots(plotA) | free(plotB, type = "label", side = "l")) / (plotC | wrap_plots(plotD)) +
  plot_layout(widths = c(1,1))
```

My example above is using a whole bunch of nested patches, but the issue persists when trying to align a patch plot to a ggplot:

```{r, fig.width=3, fig.height=5}
plot_E <- penguin_alt %>% 
  filter(species == "Adelie") %>% 
  ggplot() +
  geom_bar(aes(x = island)) +
  labs(x = "penguins") +
  scale_y_continuous(breaks = seq(0,140,20), guide = "prism_minor", expand = c(0,0)) +
  coord_cartesian(ylim = c(0,120)) +
  theme_prism(base_size = 14, base_fontface = "plain")
# plot_E
```

```{r, fig.width=7.5, fig.height=10}
(plotA | free(plotB, type = "label", side = "l")) / (plotC | plot_E)
```
#> Error in parse(text = input): attempt to use zero-length variable name

Created on 2024-11-14 with reprex v2.1.1

Hi @Megan_Huber

Thank you for your question. Would you mind explaining a little more about your issue, please?

Following your script I get the following output which looks good to me.

Looking closer I can see there is not perfect alignment in the y-axis in plots B and D - is this what you're referring to?

Yes, that is what I'm referring to. I'm not sure if the reprex translates this, but the plots are meant to have portrait-style dimensions (so all plots together would be 7.5in wide and 10in tall) and the misalignment is more obvious with those dimensions.

I know it seems nitpicky, but the actual plots I'm trying to align for my manuscript are a lot of lines/graphs squished together and it's just adds to the visual noise when none of the 12 plots are lined up.

I might be missing something here because I don't use patchwork much, but have you tried cowplot instead? The plot_grid function lets you align axes.

Thank you for the clarification, @Megan_Huber .

I can think of two ways of approaching this. Neither are perfect but perhaps you'd like to experiment to see what works for you?

Change how scales are displayed

The first is to reduce the number of digits shown in the y-axis of plot D using the {scales} package so that large numbers like 6,000 are represented by '6 k', and are more likely to align with the scales in plot B.

scale_y_continuous(breaks = seq(0,6000,1000), guide = "prism_minor", expand = c(0,0), labels = scales::label_number(suffix = ' k', scale = 1e-3)) +

Change how the patchwork is assembled

Patchwork attempts to align axes in charts by default. Once charts are combined together into 'patches', as you do in assembling plots A:D, then I think patchwork loses the ability to detect the axes and isn't able to align them as you'd like.

In this approach I assemble the eight plots together in one step with vertical stacking instead of horizontal stacking, so that patchwork can detect axes and align appropriately.

However, this is a little hacky because I first had to manually remove the y-axes in plots A2, B2, C2 and D2 to simulate the result of combining plots A1 and A2 into a patch.

# plots A2, B2, C2 and D2 need to be adjusted to remove the y-axis components, e.g.
plot_A2 <- plot_A2 +
  theme(
    axis.line.y = element_blank(), 
    axis.ticks.y = element_blank(), 
    axis.text.y = element_blank(),
    axis.title.y = element_blank()
  )

# the 8 plots are then assembled in patchwork with vertical groups, so that patchwork 
# can detect axes and align axes in plot B1 with those of plot D1:
(plot_A1 / plot_C1) | (plot_A2 / plot_C2) | (plot_B1 / plot_D1) | (plot_B2 / plot_D2)

I hope these suggestions are helpful.

1 Like