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