thomasp85/patchwork

Change the y-axis alignment of two faceted plots

LisaNicvert opened this issue · 8 comments

Hello,
I am using the patchwork package (fantastic package, thank you!) to combine 2 ggplots which are faceted using facet_grid. Patchwork works perfectly, and aligns both x- and y-axes.
However, when one of the plots is only faceted by columns, and the other by columns but also by rows, in some cases it can make sense to align only the x-axes. Indeed, aligning both plots y-axes causes a space to be added between the y-axis and the plot with only columns as facets (when switch = y) (red square on the image). Below is a reproducible example:

# load useful libraries
library(ggplot2)
library(patchwork)

# load data
data(iris)

# Add dummy factor variable
iris$dummy <- rep(c("A", "B", "C"), 50)

# First plot (facets = cols only)
g1 <- ggplot(iris) + 
  geom_density(aes(x = Petal.Width)) +
  facet_grid(cols = vars(Species))

# Second plot (facets = cols and rows)
g2 <- ggplot(iris) +
  geom_point(aes(x = Petal.Width, y = Petal.Length)) +
  facet_grid(cols = vars(Species),
             rows = vars(dummy),
             switch = "y")

# Patchwork
g1 / g2 + plot_layout(widths = c(1, 3))
# I would like the y axis on the top plot to be directly
# next to the plotting area

plot_annotated

Is there a way to control the placement of the y-axes (in that case, move the y-axis of the top plot closer to the plot area, as indicated by the red arrow)?

Thanks!

I don't think this is currently possible but IIRC ggplot2 allows to control the placement of the faceting 'boxes' and legends independently. So I'd expect than by moving the A/B/C boxes in the lower to the right but leaving the y-axis on the left for both plots, patchwork would keep aligning both y-axes underneath each other but at the position indicated by your red arrow. Additionally, you might be able to drop the x axis of the upper plot as well as the species boxes from the lower plot completely, as they contain redundant information, to get an even cleaner plot.


edit (I): I just realized that you were explicitly asking about having the boxes on the left (switch = "y"). So feel free to ignore my suggestion.


edit (II): Alternatively, I thought of adding theme(strip.placement = "outside") to g2 since that would move the y-axis of the lower plot to where you want it in the upper plot. However, while this works on its own, it results in a subscript out of bounds error when combining the two plots using patchwork.


edit (III): Just for completeness, in case it is helpful for anybody, here is the cleanest version of the plot I could come up with without using the broken strip.placement option:

# load useful libraries
library(ggplot2)
library(patchwork)

# load data
data(iris)

# Add dummy factor variable
iris$dummy <- rep(c("A", "B", "C"), 50)

# First plot (facets = cols only)
g1 <- ggplot(iris) + 
  geom_density(aes(x = Petal.Width)) +
  facet_grid(cols = vars(Species))

# Second plot (facets = cols and rows)
g2 <- ggplot(iris) +
  geom_point(aes(x = Petal.Width, y = Petal.Length)) +
  facet_grid(cols = vars(Species),
             rows = vars(dummy),
#
# CHANGES START HERE!
#
             switch = "x")

# Patchwork
(g1 + theme(axis.text.x = element_blank(),
            axis.ticks.x = element_blank(),
            axis.title.x = element_blank(),
            strip.text.x = element_blank())) /
  (g2 + theme(strip.text.x = element_text(angle = 0))) +
  plot_layout(heights = c(1, 3))

plot


edit (IV): Finally, the best version I could come up with with the faceting boxes on top and left instead of bottom and right (adding a descriptive 'dummy' dummy (no pun intended) value for the density plot to cover up the gap instead of moving the y axis):

# load useful libraries
library(ggplot2)
library(patchwork)

# load data
data(iris)

# Add dummy factor variable
iris$dummy <- rep(c("A", "B", "C"), 50)

# First plot (facets = cols only)
g1 <- ggplot(iris) + 
  geom_density(aes(x = Petal.Width)) +
#
# CHANGES START HERE!
#
  facet_grid("pooled" ~ Species,
             switch = "y")

# Second plot (facets = cols and rows)
g2 <- ggplot(iris) +
  geom_point(aes(x = Petal.Width, y = Petal.Length)) +
  facet_grid(cols = vars(Species),
             rows = vars(dummy),
             switch = "both")

(g1 + theme(axis.text.x = element_blank(),
            axis.ticks.x = element_blank(),
            axis.title.x = element_blank())) /
  (g2 + theme(strip.text.y = element_text(angle = 0),
              strip.text.x = element_blank())) +
  plot_layout(heights = c(1, 3))

plot

Hi @mschilli87, thank you for your answer! I appreciate your comments regarding discarding redundant x-axis and x-strips, so I will keep those suggestions.

Adding a "dummy" value as a faceting variable for the top plot can do the trick. However, if anyone has a solution that doesn't involve adding this dummy faceting variable, it would be better for my use-case (and I guess more generic).

Unfortunately, this solution doesn't work if the y-strips widths are different, which is something I would like for the particular problem at hand. See for example what happens if the strip text is horizontal:

# load useful libraries
library(ggplot2)
library(patchwork)

# load data
data(iris)

# Add dummy factor variable
iris$dummy <- rep(c("A", "B", "C"), 50)

# First plot (facets = cols only)
g1 <- ggplot(iris) + 
  geom_density(aes(x = Petal.Width)) +
  facet_grid("pooled" ~ Species, # Add dummy facet
             switch = "y") +
  # theme personnalization to remove redundant x-axis text
  theme(axis.text.x = element_blank(),
        axis.ticks.x = element_blank(),
        axis.title.x = element_blank())

# Second plot (facets = cols and rows)
g2 <- ggplot(iris) +
  geom_point(aes(x = Petal.Width, y = Petal.Length)) +
  facet_grid(cols = vars(Species),
             rows = vars(dummy),
             switch = "y") +
  # theme personnalization to remove redundant column strips
  theme(strip.text.y = element_blank())

# Patchwork
(g1 / g2) + plot_layout(heights = c(1, 3)) &
  theme(strip.text.y.left = element_text(angle = 0)) # This causes a problem
  
# The proposed solution works well unless the strip text on y-axis is written horizontally 
# (then strip width are different)

plot2

Does anyone have an idea to constrain both strips to the same width?

@LisaNicvert: I understand your issue. IMHO the 'best' solution would be setting strip.placement = "outside" which should work but clearly doesn't. I am not sure if this is a known bug or a new one and how complicated fixing it would be. Would that (i.e. swapping the y-axis and the strips be an acceptable solution for you or do you really want non-aligned y-axes as a (new?) feature of patchwork?


PS: I really like your idea of determining the width/height of the strips by the corresponding maximum across plots. Are you aware of a way to control those in ggplot2 so that adding this feature to patchwork would require work here only? Otherwise this would probably require upstreaming a new feature to ggplot2 first which is likely more work.

Hi @mschilli87! Following your remark, I tried setting strip.placement = "outside" for the first plot and it worked (i.e. resulted on the y-axis being not aligned as required). It introduces a small space between the x strips and the plot area in the top plot, and the y-axis titles are no more aligned, but the solution is acceptable to me. Below are the corresponding code and output:

# load useful libraries
library(ggplot2)
library(patchwork)

# load data
data(iris)

# Add dummy factor variable
iris$dummy <- rep(c("A", "B", "C"), 50)

# First plot (facets = cols only)
g1 <- ggplot(iris) + 
  geom_density(aes(x = Petal.Width)) +
  facet_grid(cols = vars(Species)) +
  # theme personnalization to remove redundant x-axis text
  theme(axis.text.x = element_blank(),
        axis.ticks.x = element_blank(),
        axis.title.x = element_blank(),
        # Fix
        strip.placement = "outside") 

# Second plot (facets = cols and rows)
g2 <- ggplot(iris) +
  geom_point(aes(x = Petal.Width, y = Petal.Length)) +
  facet_grid(cols = vars(Species),
             rows = vars(dummy),
             switch = "y") +
  # theme personnalization to remove redundant column strips
  # and write row strips labels horizontally
  theme(strip.text.y.left = element_text(angle = 0),
        strip.text.x = element_blank())

# Patchwork
(g1 / g2) + plot_layout(heights = c(1, 3))

solution

I don't know why it would not work for you. I am using patchwork 1.1.1 and ggplot2 3.3.6. Below is the output of my sessionInfo in case it is enlightening:

> sessionInfo()
R version 4.1.3 (2022-03-10)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.04.5 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0

locale:
 [1] LC_CTYPE=fr_FR.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=fr_FR.UTF-8        LC_COLLATE=fr_FR.UTF-8    
 [5] LC_MONETARY=fr_FR.UTF-8    LC_MESSAGES=fr_FR.UTF-8   
 [7] LC_PAPER=fr_FR.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=fr_FR.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods  
[7] base     

other attached packages:
[1] patchwork_1.1.1 ggplot2_3.3.6  

loaded via a namespace (and not attached):
 [1] magrittr_2.0.2   tidyselect_1.1.2 munsell_0.5.0   
 [4] colorspace_2.0-3 R6_2.5.1         rlang_1.0.1     
 [7] fansi_1.0.2      dplyr_1.0.8      tools_4.1.3     
[10] grid_4.1.3       gtable_0.3.0     utf8_1.2.2      
[13] cli_3.4.0        DBI_1.1.1        withr_2.4.3     
[16] ellipsis_0.3.2   digest_0.6.29    assertthat_0.2.1
[19] tibble_3.1.6     lifecycle_1.0.1  crayon_1.5.0    
[22] farver_2.1.0     purrr_0.3.4      vctrs_0.3.8     
[25] glue_1.6.2       labeling_0.4.2   compiler_4.1.3  
[28] pillar_1.7.0     generics_0.1.2   scales_1.1.1    
[31] pkgconfig_2.0.3

Anyway, this works for me and I guess the issue can be closed if you manage to reproduce this result or if you find a valuable reason why this would not work in your install.


PS: regardless, I also think it could be useful to be able to set the strips heights or widths manually in ggplot2, but I am not aware of any built-in way to do this. The fixes I saw involved converting the ggplot object to a grob and setting widths and heights of the strips directly there (see here for instance), but it is not "native" ggplot2 code.

@LisaNicvert: I didn't even think of setting the strip placement for the first plot. I tried setting it on the second one, so its y-axis would be aligned with the upper one while placing the strip to the left of it. This is where I am getting the error when combining both plots with patchwork (v. 1.1.2 with ggplot2 v. 3.4.0 under R v. 4.2.1). Anyway, I am happy you found a workaround that suffices for you.

Great! I'm closing the issue then. Thank you for your help @mschilli87, I hadn't thought of changing strip.placement :)

@thomasp85: Could you maybe comment on the subscript out of bounds error I got? Is this a known bug? Should I open a separate (new) issue for it?