Skip to content

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

Open

Description

@dschlaep

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

Activity

  1. eliocamp commented on Nov 28, 2023

    @eliocamp
    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

  2. dschlaep commented on Nov 28, 2023

    @dschlaep
    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

  3. eliocamp commented on Nov 28, 2023

    @eliocamp
    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.

  4. eliocamp commented on Jul 18, 2024

    @eliocamp
    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).

  5. bennotkin commented on Nov 4, 2024

    @bennotkin

    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.

  6. michaelhallquist commented on Feb 3, 2025

    @michaelhallquist

    I have a very similar problem for an R package that re-uses the fill aesthetic to layer brain images for visualization: https://github.com/michaelhallquist/ggbrain. Starting with ggplot2 3.5.0, when I want to show unused factor levels, I must now set show.legend=TRUE, per tidyverse/ggplot2#5728. This undermines earlier layer settings. Any suggestions for how to go about debugging this would be most welcome! I feel like I'm close, but I don't quite know the innards of the object well enough.

    Thanks,
    Michael

      
    
    data1 <- expand.grid(dim1=1:10, dim2=1:10)
    data1$value1 <- rnorm(100)
    
    data2 <- expand.grid(dim1=1:10, dim2=1:10)
    data2$value2 <- factor(sample(c(NA, "f1", "f2", "f3", "f4"), size = 100, replace=TRUE), levels = paste0("f", 1:5))
    
    data3 <- expand.grid(dim1=1:10, dim2=1:10)
    data3$value3 <- factor(sample(c(NA, "i1", "i2", "i3", "i4"), size = 100, replace=TRUE), levels = paste0("i", 1:5))
    
    # leaving as NA shows the right layers in legend, but the missing level is not plotted
    # https://github.com/tidyverse/ggplot2/issues/5728
    
    ggplot(mapping=aes(x=dim1, y=dim2)) +
      geom_raster(data=data1, mapping=aes(fill=value1), show.legend = FALSE) +
      scale_fill_gradient(low = "grey8", high = "grey92") +
      ggnewscale::new_scale_fill() +
      geom_raster(data=data2, mapping=aes(fill=value2), show.legend=NA) +
      scale_fill_brewer("layer2", palette = "Set3", drop=FALSE, na.value = "transparent", na.translate=FALSE) +
      ggnewscale::new_scale_fill() +
      geom_raster(data=data3, mapping=aes(fill=value3), show.legend = NA) +
      scale_fill_brewer("layer3", palette = "Dark2", drop=FALSE, na.value = "transparent", na.translate=FALSE)
    

    Image

    
    # if I provide show.legend=TRUE as now recommended in ggplot2 3.5.0+, it undermines the lower layers
    
    ggplot(mapping=aes(x=dim1, y=dim2)) +
      geom_raster(data=data1, mapping=aes(fill=value1), show.legend = FALSE) + # will become fill_ggnewscale_2
      scale_fill_gradient(low = "grey8", high = "grey92") +
      ggnewscale::new_scale_fill() +
      geom_raster(data=data2, mapping=aes(fill=value2), show.legend=TRUE) +
      scale_fill_brewer("layer2", palette = "Set3", drop=FALSE, na.value = "transparent", na.translate=FALSE) + # will become fill_ggnewscale_1
      ggnewscale::new_scale_fill() +
      geom_raster(data=data3, mapping=aes(fill=value3), show.legend = TRUE) +
      scale_fill_brewer("layer3", palette = "Dark2", drop=FALSE, na.value = "transparent", na.translate=FALSE)
    

    Image

    
    # I tried to be more specific about the show.legend argument, per
    # https://github.com/tidyverse/ggplot2/issues/5728
    # but this has no effect
    
    ggplot(mapping=aes(x=dim1, y=dim2)) +
      geom_raster(data=data1, mapping=aes(fill=value1), show.legend = c(fill_ggnewscale_2=FALSE)) + # will become fill_ggnewscale_2
      scale_fill_gradient(low = "grey8", high = "grey92") +
      ggnewscale::new_scale_fill() +
      geom_raster(data=data2, mapping=aes(fill=value2), show.legend=c(fill_ggnewscale_1=TRUE)) +
      scale_fill_brewer("layer2", palette = "Set3", drop=FALSE, na.value = "transparent", na.translate=FALSE) + # will become fill_ggnewscale_1
      ggnewscale::new_scale_fill() +
      geom_raster(data=data3, mapping=aes(fill=value3), show.legend = c(fill=TRUE)) +
      scale_fill_brewer("layer3", palette = "Dark2", drop=FALSE, na.value = "transparent", na.translate=FALSE)
    
    

    Image

  7. eliocamp commented on Jan 7, 2026

    @eliocamp
    Owner

    Unfortunately I don't think there's anything I can do on ggnewscale side to fix this issue. Notice that this happens even when just using ggplot2 without ggnewscale at all

    library(ggplot2)
    df <- data.frame(
      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)) +
      geom_area(aes(x = x, y = y2),  show.legend = TRUE) 

    Created on 2026-01-07 with reprex v2.1.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions