Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

guide vs. show.legend with new_scale() #59

Open
dschlaep opened this issue Nov 28, 2023 · 5 comments
Open

guide vs. show.legend with new_scale() #59

dschlaep opened this issue Nov 28, 2023 · 5 comments

Comments

@dschlaep
Copy link

Maybe I am misunderstanding the expected behavior of the argument show.legend in "ggplot2"

Should this layer be included in the legends? NA, the default, includes if any aesthetics are mapped. FALSE never includes, and TRUE always includes. It can also be a named logical vector to finely select the aesthetics to display.

It appears that show.legend behaves surprisingly with multiple geoms/layers in "ggplot2" (see first example below);
similarly, it does not work with ggnewscale::new_scale() (see further examples below).

Questions/problems related to show.legend have been documented by others: #31 and #32.

Instead of using show.legend, #31 (comment) provides a solution (or hack) via guide = "none" (see last examples below).

Could be helpful for users of the package to document the behavior of show.legend in combination with ggnewscale::new_scale() and provide examples using guide = "none"?

Thanks!

# Make up some values
nn <- 6
n <- 3 * 5 * nn

m <- data.frame(
  id1 = factor(rep(1:3, each = n / 3)),
  id2 = factor(rep(1:5, each = n / 5)),
  x = runif(n, 1, 80)
)

m[["y"]] <- 10 + m[["x"]] * runif(n, 1, 2)

# The behavior of `show.legend` with multiple geoms is surprising in ggplot2:
# Plot `id2` by colored lines and omit legend (`show.legend = FALSE`);
# add plot `id1` by colored points and show legend (`show.legend = TRUE`)
#   --> expected outcome: no legend for `id2`; legend for `id1` with colored points
#   --> actual outcome: legend for `id2` with colored points (instead of lines); no legend for `id1`
p1 <- ggplot2::ggplot(
  data = m,
  mapping = ggplot2::aes(x = x, y = y)
) +
  ggplot2::geom_line(
    ggplot2::aes(color = id2),
    show.legend = FALSE
  ) +
  ggplot2::geom_point(
    ggplot2::aes(color = id1),
    show.legend = TRUE
  ) +
  ggplot2::scale_color_viridis_d()

plot(p1)

# Plot `id2` by colored lines and omit legend (`show.legend = FALSE`);
#   --> expected outcome: no legend
#   --> actual outcome: as expected
p1a <- ggplot2::ggplot(
  data = m,
  mapping = ggplot2::aes(x = x, y = y)
) +
  ggplot2::geom_line(
    ggplot2::aes(color = id2),
    show.legend = FALSE
  ) +
  ggplot2::scale_color_viridis_d()

plot(p1a)

# Additionally, plot `id1` by new colors; show legend (`show.legend = TRUE`)
#   --> expected outcome: no legend for `id2`, legend for `id1`
#   --> actual outcome: legend for both `id1` and `id2` are shown
#       (with incorrect (black) color for `id2`)
p2 <- p1a +
  ggnewscale::new_scale_color() +
  ggplot2::geom_line(
    ggplot2::aes(color = id1),
    show.legend = TRUE
  ) +
  ggplot2::scale_color_viridis_d()

plot(p2)

# Similarly, plot `id1` by new colors and with different line width;
# show legend (`show.legend = TRUE`)
#   --> expected outcome: no legend for `id2`, legend for `id1`
#   --> actual outcome: legend for both `id1` and `id2` are shown
#       (with incorrect (blue) color and incorrect line width for `id2`;
#        note: color in legend of `id2` differs from previous example)
p3 <- p1a +
  ggnewscale::new_scale_color() +
  ggplot2::geom_smooth(
    linewidth = 1,
    ggplot2::aes(color = id1),
    show.legend = TRUE
  ) +
  ggplot2::scale_color_viridis_d()

plot(p3)
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# An analogous unexpected behavior occurs for the opposite, e.g.,
#   - Plot `id2` by color and show legend (`show.legend = TRUE`)
#   - Add plot `id1` by new colors and with different line width;
#     omit legend (`show.legend = FALSE`)
#   --> expected outcome: legend for `id2`, no legend for `id1`
#   --> actual outcome: legend for both `id1` and `id2` are shown
#       (with incorrect color and incorrect line width for `id1`)
p4 <- ggplot2::ggplot(
  data = m,
  mapping = ggplot2::aes(x = x, y = y)
) +
  ggplot2::geom_line(
    ggplot2::aes(color = id2),
    show.legend = TRUE
  ) +
  ggplot2::scale_color_viridis_d() +
  ggnewscale::new_scale_color() +
  ggplot2::geom_smooth(
    linewidth = 1,
    ggplot2::aes(color = id1),
    show.legend = FALSE
  ) +
  ggplot2::scale_color_viridis_d()

plot(p4)
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# Solution/Hack
# from https://github.com/eliocamp/ggnewscale/issues/31#issuecomment-834398072
# --- to omit a legend, don't use `show.legend = FALSE`,
#     instead use `guide = "none"`
p5a <- ggplot2::ggplot(
  data = m,
  mapping = ggplot2::aes(x = x, y = y)
) +
  ggplot2::geom_line(
    ggplot2::aes(color = id2)
  ) +
  ggplot2::scale_color_viridis_d(guide = "none") +
  ggnewscale::new_scale_color() +
  ggplot2::geom_smooth(
    linewidth = 1,
    ggplot2::aes(color = id1),
    show.legend = TRUE
  ) +
  ggplot2::scale_color_viridis_d()

plot(p5a)
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# respectively
p5b <- ggplot2::ggplot(
  data = m,
  mapping = ggplot2::aes(x = x, y = y)
) +
  ggplot2::geom_line(
    ggplot2::aes(color = id2),
    show.legend = TRUE
  ) +
  ggplot2::scale_color_viridis_d() +
  ggnewscale::new_scale_color() +
  ggplot2::geom_smooth(
    linewidth = 1,
    ggplot2::aes(color = id1)
  ) +
  ggplot2::scale_color_viridis_d(guide = "none")

plot(p5b)
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# Session info
print(packageVersion("ggnewscale"))
#> [1] '0.4.9'
print(packageVersion("ggplot2"))
#> [1] '3.4.4'

Created on 2023-11-28 with reprex v2.0.2

@eliocamp
Copy link
Owner

The issues are fixed in the development version of ggnewscale (remotes::install_github(“eliocamp/ggnewscale”)).

I probably won't be sending it to CRAN soon because I think I rather publish a rewrite of the internals that fixes other bugs and makes the code much more easy to follow. But it requires a change in ggplot2 which is not available yet.

# Make up some values
library(ggplot2)
library(ggnewscale)

nn <- 6
n <- 3 * 5 * nn

m <- data.frame(
  id1 = factor(rep(8:10, each = n / 3)),  # No overlap to distinguish them more easily
  id2 = factor(rep(1:5, each = n / 5)),
  x = runif(n, 1, 80)
)

m[["y"]] <- 10 + m[["x"]] * runif(n, 1, 2)


ggplot(m, aes(x, y)) +
  geom_line(aes(color = id2), show.legend = FALSE) +
  geom_point(aes(color = id1), show.legend = TRUE) +
  scale_color_viridis_d()

Here, the line doesn’t contribute to the legend so the legend is only drawn with points instead of points and lines. The title of the legend inherits from the first layer. The legend is not drawn “for id2” nor id1, it’s drawn for the colour aesthetic, which combines all the unique values of id1 and id2.

The following examples work in the development version of ggnewscale

Default: shows both legends with lines

ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1)) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2)) +
  scale_color_viridis_d(name = "id2") 

Remove legend for first layer: shows only legend for second layer

ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1), show.legend = FALSE) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2)) +
  scale_color_viridis_d(name = "id2") 

Remove legend for second layer: shows only legend for first layer

ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1)) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2), show.legend = FALSE) +
  scale_color_viridis_d(name = "id2") 

Remove legends for both layers: shows no legend.

ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1), show.legend = FALSE) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2), show.legend = FALSE) +
  scale_color_viridis_d(name = "id2") 

Adds a smooth layer: shows legend only for that layer, with correct width.

ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1), show.legend = FALSE) +
  geom_smooth(aes(color = id1)) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2), show.legend = FALSE) +
  scale_color_viridis_d(name = "id2") 
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Created on 2023-11-28 with reprex v2.0.2

@dschlaep
Copy link
Author

This is great, thanks so much for the fix and response! I installed the dev-version and the examples work nicely.

In your example "Remove legend for first layer: shows only legend for second layer", if I drop in an (unnecessary but possible) show.legend = TRUE, then it reverts back to showing unexpected legends.

library(ggplot2)
library(ggnewscale)

nn <- 6
n <- 3 * 5 * nn

m <- data.frame(
  id1 = factor(rep(8:10, each = n / 3)),
  id2 = factor(rep(1:5, each = n / 5)),
  x = runif(n, 1, 80)
)

m[["y"]] <- 10 + m[["x"]] * runif(n, 1, 2)


# Remove legend for first layer: shows only legend for second layer
# this works
ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1), show.legend = FALSE) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2)) +
  scale_color_viridis_d(name = "id2")

# Remove legend for first layer: shows only legend for second layer
# this doesn't work
ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1), show.legend = FALSE) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2), show.legend = TRUE) +
  scale_color_viridis_d(name = "id2")

Created on 2023-11-28 with reprex v2.0.2

@eliocamp
Copy link
Owner

Thank you for the last example. I'll take it into account for the future refactoring. I suspect is something that could be fixed by the new logic.

@eliocamp
Copy link
Owner

Looking into this, show.legend = TRUE instructs ggplot2 to include the geom in the legend even if it doesn't have an aesthetic mapping. So, take this example:

ggplot(m, aes(x, y)) +
  geom_line(aes(color = id1)) +
  scale_color_viridis_d(name = "id1") +
  new_scale_color() +
  geom_line(aes(color = id2), show.legend = TRUE) +
  scale_color_viridis_d(name = "id2")

image

The id2 line is being drawn in the legend corresponding to the id1 line. But since the id2 line is mapped to the second colour slot, it's drawn as a black line (the default colour, I guess), and since it's the second layer, it's drawn on top of the id1 line.

So in fact it seems that this is all working as written.

The issue is that the default guide takes "any" aes, so this part doesn't do anything.

# Change available aesthetics
new$available_aes <- change_name(new$available_aes, old_aes, new_aes)
new$available_aes[new$available_aes %in% old_aes] <- new_aes

I could replace and make it take only the new_aes, but that might cause other problems (the guide will not take fill aesthetics, for example).

@bennotkin
Copy link

I'm running into the issue of the last two comments, where show.legend = T in the second layer removes the color from the first scale.

df <- tibble(
  x = 1:10,
  y1 = runif(10), fill1 = rep(letters[1:5], 2),
  y2 = rnorm(10), fill2 = rep(letters[6:7], 5))

ggplot(df) +
  geom_area(aes(x = x, y = y1, fill = fill1)) +
  ggnewscale::new_scale_fill() +
  geom_area(aes(x = x, y = y2, fill = fill2),  show.legend = T)

image

whereas if I remove show.legend = T, the legend shows both scales appropriately.

image

Including show.legend = T is helpful/needed when I want the legend to include all factor levels, even if not included in the chart:

df <- tibble(
  x = 1:10,
  y1 = runif(10), fill1 = rep(letters[1:5], 2),
  y2 = rnorm(10), fill2 = rep(factor(letters[6:7], levels = letters[6:8]), 5))

ggplot(df) +
  geom_area(aes(x = x, y = y1, fill = fill1)) +
  ggnewscale::new_scale_fill() +
  geom_area(aes(x = x, y = y2, fill = fill2),  show.legend = T) +
  scale_fill_manual(values = c("f" = "red", "g" = "yellow", "h" = "blue"), drop = F)

For additional info, I've not yet been able to minimally reproduce this, but in my actual use case, the fill color is still visible around a grey box in the legend:

image

I'm using v0.5.0, and also tried the latest Github release, which I believe is the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants