thomasp85 / patchwork

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

color and size guides partially failing to collect #329

Closed twest820 closed 11 months ago

twest820 commented 1 year ago

Looks like I've hit another failure mode among this common class of issues. I've got three plots, patchwork collects legends for the first two but really, really, really wants to make separate legends for the third. Even though all three plots have identical legends for point size and scale_color_manual() is successfully applied in all three cases. no collection

Repex:

data = tibble(x = seq(0, 10), y1 = seq(0, 10), z1 = seq(1, 11), y2 = seq(2, 12), z2 = seq(3, 13), w = seq(4, 14), size = seq(5, 15))
ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL) +
plot_annotation(theme = theme(plot.margin = margin())) +
plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"))

Various alternatives I've tried, none of which work:

ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL) +
  theme(legend.position = "none") +
plot_annotation(theme = theme(plot.margin = margin())) +
plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"), drop = FALSE)

ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL) +
plot_layout(guides = "collect") &
  scale_color_discrete(breaks = c("y", "z", "w"))

ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL) +
plot_annotation(theme = theme(plot.margin = margin())) +
plot_layout(guides = "collect") &
  guides(color = guide_legend(order = 1), size = guide_legend(order = 2)) &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"))

p1 = ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL)
p2 = ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL)
p3 = ggplot(data) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL)
(p1 + p2 + p3) + plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"))

p1 = ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  geom_blank(aes(color = "w")) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL)
p2 = ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  geom_blank(aes(color = "w")) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL)
p3 = ggplot(data) +
  geom_blank(aes(color = "x")) +
  geom_blank(aes(color = "y")) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL)
(p1 + p2 + p3) + plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"))

p1 = ggplot(data) +
  geom_point(aes(x = x, y = y1, color = factor("y", levels = c("y", "z", "w")), size = size)) +
  geom_point(aes(x = x, y = z1, color = factor("z", levels = c("y", "z", "w")), size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL)
p2 = ggplot(data) +
  geom_point(aes(x = x, y = y2, color = factor("y", levels = c("y", "z", "w")), size = size)) +
  geom_point(aes(x = x, y = z2, color = factor("z", levels = c("y", "z", "w")), size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL)
p3 = ggplot(data) +
  geom_point(aes(x = x, y = w, color = factor("w", levels = c("y", "z", "w")), size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL)
(p1 + p2 + p3) + plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"))

data = tibble(x = seq(0, 10), y1 = seq(0, 10), z1 = seq(1, 11), y2 = seq(2, 12), z2 = seq(3, 13), w = seq(4, 14), yColor = factor("y", levels = c("y", "z", "w")), zColor = factor("z", levels = c("y", "z", "w")), wColor = factor("w", levels = c("y", "z", "w")), size = seq(5, 15))
ggplot(data) +
  geom_point(aes(x = x, y = y1, color = yColor, size = size)) +
  geom_point(aes(x = x, y = z1, color = zColor, size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = y2, color = yColor, size = size)) +
  geom_point(aes(x = x, y = z2, color = zColor, size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = w, color = wColor, size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL) +
plot_annotation(theme = theme(plot.margin = margin())) +
plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), values = c("red", "green", "blue"))

What does succeed is force disabling the uncollected legends and then forcing limits on the collected color legend:

ggplot(data) +
  geom_point(aes(x = x, y = y1, color = "y", size = size)) +
  geom_point(aes(x = x, y = z1, color = "z", size = size)) +
  labs(x = "x", y = "y1 and z1", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = y2, color = "y", size = size)) +
  geom_point(aes(x = x, y = z2, color = "z", size = size)) +
  labs(x = "x", y = "y2 and z2", color = NULL, size = NULL) +
ggplot(data) +
  geom_point(aes(x = x, y = w, color = "w", size = size)) +
  labs(x = "x", y = "w", color = NULL, size = NULL) +
  theme(legend.position = "none") +
plot_annotation(theme = theme(plot.margin = margin())) +
plot_layout(guides = "collect") &
  scale_color_manual(breaks = c("y", "z", "w"), limits = c("y", "z", "w"), values = c("red", "green", "blue"))

The code footprint for this workaround is fairly compact but, IMO, this seems a large hammer to use for what should be a trivial collect—the size legends are the same and color factor levels are either disjoint (repex) or forced identical (in some of the attempted workarounds), so there's no merge conflicts. The break in ggplot's scale_color_manual(drop = FALSE) in the first alternative suggests color factors aren't flowing.

It's also curious the color and size legends don't group together when ordering is explicitly specified. Even if all three color legends don't collect what I'd expect to have happen is two uncollected color legends next to each other—which is incorrect but might render just fine for certain choices of legend spacing when legend titles happen to be absent—followed by a single size legend. It appear patchwork might only be collecting when all legends from a plot are identical rather than looking to color collect legends independently from size legends.

For completeness, in the actual application code I have behind this there's also weird things happening with alpha and override.aes = list(alpha) isn't driving collecting when it should either. I suspect those are artifacts of incomplete color and size collection, though.

teunbrand commented 11 months ago

The reason that your 3rd plots guides can't be combined with the first two plots is that the first two display two points on top of one another per key, whereas the third plot has just a single point per key. These are therefore not identical legends.

thomasp85 commented 11 months ago

Yes, I concede the guide collecting can lead to hard to debug "failures" because seemingly identical legends have different underlying data representation. I'm unlikely to change how it works as it would most likely lead to other and worse regressions if the code tried to guess if a legend where "identical enough" despite differing data representation.

If the automatic collection fails you can always turn of one of the legends in the code for the specific plot