6

How can I add on the graph legend an NA or "0" label if I used trans="log" in the scale_fill_viridis or another continuous scale

> d1
       persona num.de.puntos puntos
    1       p1             1      3
    2       p1             2      4
    3       p1             3      0
    4       p1             4      4
    5       p1             5      2
    6       p2             1      2
    7       p2             2      3
    8       p2             3      0
    9       p2             4      0
    10      p2             5      4
    11      p3             1      0
    12      p3             2      1
    13      p3             3      0
    14      p3             4      5
    15      p3             5      8

p <- ggplot(d1, aes(persona, num.de.puntos, fill = puntos)) + 
  scale_fill_viridis(trans="log", option ="D", direction=1, na.value = "gray50", 
                     breaks=c(0,1,5,8),
                     name="Number of people",
                     guide=guide_legend(label.position = "bottom", 
                                        title.position = 'top', 
                                        nrow=1,
                                        label.theme = element_text(size = 6, 
                                                                   face = "bold", 
                                                                   color = "black"),
                                        title.theme = element_text(size = 6, 
                                                                   face = "bold", 
                                                                   color = "black"))) +
  geom_tile(colour="grey", show.legend = TRUE)

p

enter image description here

I want

enter image description here

Z.Lin
  • 28,055
  • 6
  • 54
  • 94
  • Not a dupe, but you might have some luck adjusting this answer: https://stackoverflow.com/q/31208175/5325862 – camille Mar 04 '19 at 18:31

1 Answers1

1

Note: Code below is run in R 3.5.1 and ggplot2 3.1.0. You might be using an older version of the ggplot2 package, since your code uses scale_fill_viridis instead of scale_fill_viridis_c.

TL;DR solution:

I'm pretty sure this is not orthodox, but assuming your plot is named p, setting:

p$scales$scales[[1]]$is_discrete <- function() TRUE

will get the NA value in your legend without changing any of the existing fill aesthetic mappings.

Demonstration:

p <- ggplot(d1, aes(persona, num.de.puntos, fill = puntos)) + 
  scale_fill_viridis_c(trans="log", option ="D", direction=1, 
                       na.value = "gray50",                   # optional, change NA colour here
                       breaks = c(0, 1, 5, 8),
                       labels = c("NA label", "1", "5", "8"), # optional, change NA label here
                       name="Number of people",
                       guide=guide_legend(label.position = "bottom", 
                                          title.position = 'top', 
                                          nrow=1,
                                          label.theme = element_text(size = 6, 
                                                                     face = "bold", 
                                                                     color = "black"),
                                          title.theme = element_text(size = 6, 
                                                                     face = "bold", 
                                                                     color = "black"))) +
  geom_tile(colour = "grey", show.legend = TRUE)

p # no NA mapping

p$scales$scales[[1]]$is_discrete <- function() TRUE

p # has NA mapping

without NA mapping

with NA mapping

Explanation:

I dug deep into the plotting mechanics by converting p into a grob object (via ggplotGrob), & running debug() on functions that affect the legend generation part of the plotting process.

After debugging through ggplot2:::ggplot_gtable.ggplot_built, ggplot2:::build_guides, and ggplot2:::guides_train, I got to ggplot2:::guide_train.legend, an un-exported function from the ggplot2 package:

> ggplot2:::guide_train.legend
function (guide, scale, aesthetic = NULL) 
{
    breaks <- scale$get_breaks()
    if (length(breaks) == 0 || all(is.na(breaks))) {
        return()
    }
    key <- as.data.frame(setNames(list(scale$map(breaks)), aesthetic %||% 
        scale$aesthetics[1]), stringsAsFactors = FALSE)
    key$.label <- scale$get_labels(breaks)
    if (!scale$is_discrete()) {
        limits <- scale$get_limits()
        noob <- !is.na(breaks) & limits[1] <= breaks & breaks <= 
            limits[2]
        key <- key[noob, , drop = FALSE]
    }
    if (guide$reverse) 
        key <- key[nrow(key):1, ]
    guide$key <- key
    guide$hash <- with(guide, digest::digest(list(title, key$.label, 
        direction, name)))
    guide
}

I saw that up until the key$.label <- scale$get_labels(breaks) step, key looks like this:

> key
     fill   .label
1  gray50 NA label
2 #440154        1
3 #72CC59        5
4 #FDE725        8

But scale$is_discrete() is FALSE, so !scale$is_discrete() is TRUE, and the row in key corresponding to the NA value gets dropped in the next step, since there is an NA value in the fill scale's breaks after log transformation:

> scale$get_breaks()
[1]       NA 0.000000 1.609438 2.079442

Thus, if we can have scale$is_discrete() evaluate to TRUE instead of FALSE, this step would be skipped, and we end up with the full legend including the NA value.

Z.Lin
  • 28,055
  • 6
  • 54
  • 94