thomasp85 / patchwork

The Composer of ggplots
https://patchwork.data-imaginist.com
Other
2.47k stars 162 forks source link

Legends duplicated when collected #170

Closed robjhyndman closed 4 years ago

robjhyndman commented 4 years ago

This was working in R3.6.3, but not in R4.0.

library(ggplot2)
library(patchwork)
p1 <- ggplot(mtcars) + geom_point(aes(mpg, disp, col=gear))
p2 <- ggplot(mtcars) + geom_boxplot(aes(gear, disp, group = gear, col=gear))
(p1 | p2 ) +
  plot_layout(guides = "collect") & theme(legend.position = 'bottom')

Created on 2020-04-27 by the reprex package (v0.3.0)

bling1000 commented 4 years ago

The same happened to me ! I updated my R version to 4.0.0 recently.

mikecuoco commented 4 years ago

Same happened to me after I updated to R 4.0.0

teng-gao commented 4 years ago

+1 My R is 3.6.1

hexuebao commented 4 years ago

Same happened to me after I updated to R 4.0.0

MMJansen commented 4 years ago

Anyone know if a solution is to be expected soon? If I can help in anyway, please let me know.

llrs commented 4 years ago

No idea, but it would help if someone could find the source of the problem. At least then it would be easier to fix it (via PR or Thomas himself).

thomasp85 commented 4 years ago

I will look into this within a week or two

mcanouil commented 4 years ago

So I took a look into that issue and the behaviour of all.equal is not the same in R 4.0.0 on grobs (https://github.com/thomasp85/patchwork/blob/master/R/guides.R#L31). Although, I don't know (yet) how to fix this.

library(patchwork)
library(ggplot2)
library(gtable)

p <- ggplot(iris, aes(Sepal.Length, Petal.Length, colour = Species)) +
  geom_point()

guides <- list(
  gtable_filter(ggplotGrob(p), "guide-box"), 
  gtable_filter(ggplotGrob(p), "guide-box")
)

unnamed <- lapply(guides, patchwork:::unname_grob)

i <- 2
j <- 1
res <- all.equal(unnamed[[i]], unnamed[[j]], check.names = FALSE, check.attributes = FALSE)
head(res)
#> [1] "Component \"grobs\": Component 1: Component 1: Component 1: Component 1: Component 2: Component 3: Component 3: Component 1: Component 2: Component 1: Component 2: Component 2: Component 1: Component 2: Component 9: 1 string mismatch"                                        
#> [2] "Component \"grobs\": Component 1: Component 1: Component 1: Component 1: Component 2: Component 3: Component 3: Component 1: Component 2: Component 2: Component 2: Component 2: Component 1: Component 2: Component 9: 1 string mismatch"                                        
#> [3] "Component \"grobs\": Component 1: Component 1: Component 1: Component 1: Component 2: Component 3: Component 3: Component 1: Component 2: Component 3: 1 string mismatch"                                                                                                         
#> [4] "Component \"grobs\": Component 1: Component 1: Component 1: Component 1: Component 2: Component 3: Component 3: Component 1: Component 2: Component 5: Component 1: Component 11: Component 3: Component 2: Component 2: Component 1: Component 2: Component 9: 1 string mismatch"
#> [5] "Component \"grobs\": Component 1: Component 1: Component 1: Component 1: Component 2: Component 3: Component 3: Component 1: Component 2: Component 5: Component 1: Component 11: Component 4: Component 2: Component 2: Component 1: Component 2: Component 9: 1 string mismatch"
#> [6] "Component \"grobs\": Component 1: Component 1: Component 1: Component 1: Component 2: Component 3: Component 3: Component 1: Component 2: Component 5: Component 1: Component 17: 1 string mismatch"
thomasp85 commented 4 years ago

thanks - yeah, I suspected some change in all.equal() was the root cause

mcanouil commented 4 years ago

For the ones more expert in grid than me, the challenge to solve the issue is to return TRUE with the following code (in R 4.0.0):

library("grid")
x <- grob()
y <- grob()
all.equal(x, y)
#> [1] "Component \"name\": 1 string mismatch"

EDIT:

library("grid")
x <- patchwork:::unname_grob(grob())
y <- patchwork:::unname_grob(grob())
all.equal(x, y)
#> [1] TRUE

Maybe unname_grob needs to be called recursively on the guides.

EDIT2: Following my previous reprex, I think the issue comes from the new unit class used in grid.

# R 4.0.0
x <- unnamed[[1]]$grobs[[1]]$grobs[[1]]$grobs[[2]]$children[[1]]$widths[[2]]
y <- unnamed[[2]]$grobs[[1]]$grobs[[1]]$grobs[[2]]$children[[1]]$widths[[2]]
all.equal(x, y, check.names = FALSE, check.attributes = FALSE)
#> [1] "Component 1: Component 2: Component 1: Component 2: Component 9: 1 string mismatch" 
# this is the location of the component '$name         : chr "GRID.text.509"',  in the sub element of classes "text", "grob" and "gDesc"
class(x)
#> [1] "unit"    "unit_v2"
class(y)
#> [1] "unit"    "unit_v2"
# R 3.6.3
x <- unnamed[[1]]$grobs[[1]]$grobs[[1]]$grobs[[2]]$children[[1]]$widths[[2]]
y <- unnamed[[2]]$grobs[[1]]$grobs[[1]]$grobs[[2]]$children[[1]]$widths[[2]]
all.equal(x, y, check.names = FALSE, check.attributes = FALSE)
#> [1] TRUE
class(x)
#> [1] "unit.arithmetic" "unit"
class(y)
#> [1] "unit.arithmetic" "unit"
RobinM92 commented 4 years ago

Is there a workaround for now that we can use?

mcanouil commented 4 years ago

Yes, to manually remove the duplicated legends.

library(patchwork)
library(ggplot2)
p <- ggplot(iris, aes(Sepal.Length, Petal.Length, colour = Species)) +
  geom_point()

p + p + plot_layout(guides = "collect")

(p + theme(legend.position = "none")) + p + plot_layout(guides = "collect")

RobinM92 commented 4 years ago

Thanks, logical! To combine this with the legend positions new problems arise of course. E.g.

library(patchwork)
library(ggplot2)
p <- ggplot(iris, aes(Sepal.Length, Petal.Length, colour = Species)) +
  geom_point()

(p + theme(legend.position = "none")) + p + plot_layout(guides = "collect") & theme(legend.position = "bottom")

image

vs

library(patchwork)
library(ggplot2)
p <- ggplot(iris, aes(Sepal.Length, Petal.Length, colour = Species)) +
  geom_point()

(p + theme(legend.position = "none")) + p + plot_layout(guides = "collect") & theme(legend.position = "bottom")

image

Both are not ideal, but if you define the position for the first legend and then remove the later ones it works as we want to:

library(patchwork)
library(ggplot2)
p <- ggplot(iris, aes(Sepal.Length, Petal.Length, colour = Species)) +
  geom_point()

(p + plot_layout(guides = "collect") & theme(legend.position = "bottom")) + (p + theme(legend.position = "none")) 

image

mcanouil commented 4 years ago

Use guides to remove the legend then

library(patchwork)
library(ggplot2)

p <- ggplot(iris, aes(Sepal.Length, Petal.Length, colour = Species)) +
  geom_point()

p_theme <- p + theme(legend.position = "none")
p_guide <- p + guides(colour = "none")
p_theme + p + 
  plot_layout(guides = "collect") & theme(legend.position = "bottom")

p_guide + p + 
  plot_layout(guides = "collect") & theme(legend.position = "bottom")