4

I'm plotting a sort of chloropleth of up to three selectable species abundances across a research area. This toy code behaves as expected and does almost what I want:

library(dplyr)
library(ggplot2)
square <- expand.grid(X=0:10, Y=0:10)
sq2 <- square[rep(row.names(square), 2),] %>%
  arrange(X,Y) %>%
  mutate(SPEC = rep(c('red','blue'),len=n())) %>%
  mutate(POP = ifelse(SPEC %in% 'red', X, Y)) %>%
  group_by(X,Y) %>% 
  mutate(CLR = rgb(X/10,0,Y/10)) %>% ungroup()

ggplot(sq2, aes(x=X, y=Y, fill=CLR)) + geom_tile() +
  scale_fill_identity("Species", guide="legend",
    labels=c('red','blue'), breaks=c('#FF0000','#0000FF'))

Producing this:

X=red; Y=blue; plotted as gradient tiling

A modified version properly plots the real map, appropriately mixing the RGBs to show the species proportions per map unit. But given that mixing, the real data does not necessarily include the specific values listed in breaks, in which case no entry appears in the legend for that species. If you change the last line of the example to

labels=c('red','blue','green'), breaks=c('#FF0000','#0000FF','#00FF00'))

you get the same legend as shown, with only 'red' and 'blue' displayed, as there is no green in it. Searching the data for each max(Species) and assigning those to the legend is possible but won't make good legend keys for species that only occur in low proportions. What's needed is for the legend to display the idea of the entities present, not their attested presences -- three colors in the legend even if only one species is detected.

I'd think that scale_fill_manual() or the override.aes argument might help me here but I haven't been able to make any combination work.

Edit:
Episode IV -- A New Dead End

(Thanks @r2evans for fixing my omission of packages.) I thought I might be able to trick the legend by mutating a further column into the df in the processing pipe called spCLR to represent the color ('#FF0000', e.g.) that codes each entry's species (redundant info, but fine). Now the plotting call in my real version goes:

df %>% [everything] %>%
    ggplot(aes(x = X, y = Y, height = WIDTH, width = WIDTH, fill = CLR)) +
      geom_tile() +
      scale_fill_identity("Species", guide="legend",
        labels=spCODE, breaks=spCLR)

But this gives the error: Error in check_breaks_labels(breaks, labels) : object 'spCLR' not found. That seems weird since spCLR is indeed in the pipe-modified df, and of all the values supplied to the ggplot functions spCODE is the only one present in the original df -- so if there's some kind of scope problem I don't get it. [Re-edit -- I see that neither labels nor breaks wants to look at df$anything. Anyway.]

I assume (rightly?) there's some way to make this one work [?], but it still wouldn't make the legend show 'red', 'blue' and 'green' in my toy example -- which is what my original question is really about -- because there is still no actual green-data present in that. So to reiterate, isn't there any way to force a ggplot2 legend to show the things you want to talk about, rather than just the ones that are present in the data?

uhClem
  • 95
  • 10
  • Relevant code with one solution: http://lenkiefer.com/2017/04/24/bivariate-map/ – Jon Spring Oct 21 '19 at 18:33
  • @JonSpring Thanks for that; I love what he's done there but that's not really it for me. Doing a _tri_ variate (triangular? _cuboid?_) minigraph for a legend is an attractively crazy idea for production work but overkill for eyeballing... and to be honest a trivariate heatmap isn't something you should be asking people to look at much anyway. These plots are really just for my preanalysis -- so it's helpful if they've got a legend that reminds me what I was looking at. – uhClem Oct 22 '19 at 12:21
  • A simpler solution: https://cran.r-project.org/web/packages/biscale/vignettes/biscale.html – Jon Spring Oct 22 '19 at 23:34

2 Answers2

2

I have belatedly discovered that my question is a near-duplicate of this. The accepted answer there (from @joran) doesn't work for this but the second answer (from @Axeman) does. So the way for me to go here is that the last line should be

labels=c('red','blue','green'), limits=c('#FF0000','#0000FF','#00FF00'))

calling limits() instead of breaks(), and now my example and my real version work as desired.

I have to say I spent a lot of time digging around in the ggplot2 reference without ever gaining a suspicion that limits() was the correct alternative to breaks() -- which is explicitly mentioned in that ref page while limits() does not appear. The ?limits() page is quite uninformative, and I can't find anything that lays out the distinctions between the two: when this rather than that.

uhClem
  • 95
  • 10
1

I assume from the heatmap use case that you have no other need for colour mapping in the chart. In this case, a possible workaround is to leave the fill scale alone, & create an invisible geom layer with colour aesthetic mapping to generate the desired legend instead:

ggplot(sq2, aes(x=X, y=Y)) +
  geom_tile(aes(fill = CLR)) + # move fill mapping here so new point layer doesn't inherit it
  scale_fill_identity() + # scale_*_identity has guide set to FALSE by default

  # add invisible layer with colour (not fill) mapping, within x/y coordinates within
  # same range as geom_tile layer above
  geom_point(data = . %>% 
               slice(1:3) %>% 
               # optional: list colours in the desired label order
               mutate(col = forcats::fct_inorder(c("red", "blue", "green"))),
             aes(colour = col), 
             alpha = 0) +

  # add colour scale with alpha set to 1 (overriding alpha = 0 above),
  # also make the shape square & larger to mimic the default legend keys
  # associated with fill scale
  scale_color_manual(name = "Species",
                     values = c("red" = '#FF0000', "blue" = '#0000FF', "green" = '#00FF00'),
                     guide = guide_legend(override.aes = list(alpha = 1, shape = 15, size = 5)))

plot

Z.Lin
  • 28,055
  • 6
  • 54
  • 94
  • Well, this doesn't *exactly* answer the question -- I was hoping there would be a more inside-the-box sort of solution -- but I admire the ingeniousness. And besides that, it was you who answered my last question too! Amaze. – uhClem Oct 25 '19 at 16:14