2

I'd like to make a plot using ggplot2 where some of the fill values are clipped, i.e. values above or below the limits of the color scale are displayed as the minimum/maximum color. I can get this to work like this, using a combination of limit and oob (out of bounds):

library(ggplot2)
library(scales)
ggplot() + ... + scale_fill_viridis(na.value="white", limit=c(0, 10), oob=squish)

But there is no information in the colorbar that indicates there are values present outside of the limits. How can I reproduce this matplotlib example in ggplot: https://stackoverflow.com/a/32072348

Specifically, how to get the triangles at the end of the colorbar?

Ruben
  • 23
  • 2

3 Answers3

8

As far as I'm aware there is not a package that implements triangle ends for colourbars in ggplot2 (but please let me know if there is!). However, we can implement our own. We'd need a constructor for our custom guide and a way to draw it. Most of the stuff is already implemented in guide_colourbar() and methods for their class, so what we need to do is just tag on our own class and expand the guide_gengrob method. The code below should work for vertically oriented colourbars. You'd need to know some stuff about the grid package and gtable package to follow along.

library(ggplot2)
library(gtable)
library(grid)

my_triangle_colourbar <- function(...) {
  guide <- guide_colourbar(...)
  class(guide) <- c("my_triangle_colourbar", class(guide))
  guide
}

guide_gengrob.my_triangle_colourbar <- function(...) {
  # First draw normal colourbar
  guide <- NextMethod()
  # Extract bar / colours
  is_bar <- grep("^bar$", guide$layout$name)
  bar <- guide$grobs[[is_bar]]
  extremes <- c(bar$raster[1], bar$raster[length(bar$raster)])
  # Extract size
  width  <- guide$widths[guide$layout$l[is_bar]]
  height <- guide$heights[guide$layout$t[is_bar]]
  short  <- min(convertUnit(width, "cm",  valueOnly = TRUE),
                convertUnit(height, "cm", valueOnly = TRUE))
  # Make space for triangles
  guide <- gtable_add_rows(guide, unit(short, "cm"),
                           guide$layout$t[is_bar] - 1)
  guide <- gtable_add_rows(guide, unit(short, "cm"),
                           guide$layout$t[is_bar])
  
  # Draw triangles
  top <- polygonGrob(
    x = unit(c(0, 0.5, 1), "npc"),
    y = unit(c(0, 1, 0), "npc"),
    gp = gpar(fill = extremes[1], col = NA)
  )
  bottom <- polygonGrob(
    x = unit(c(0, 0.5, 1), "npc"),
    y = unit(c(1, 0, 1), "npc"),
    gp = gpar(fill = extremes[2], col = NA)
  )
  # Add triangles to guide
  guide <- gtable_add_grob(
    guide, top, 
    t = guide$layout$t[is_bar] - 1,
    l = guide$layout$l[is_bar]
  )
  guide <- gtable_add_grob(
    guide, bottom,
    t = guide$layout$t[is_bar] + 1,
    l = guide$layout$l[is_bar]
  )
  
  return(guide)
}

You can then use your custom guide as the guide argument in a scale.

g <- ggplot(mtcars, aes(mpg, wt)) +
  geom_point(aes(colour = drat))

g + scale_colour_viridis_c(
    limits = c(3, 4), oob = scales::oob_squish,
    guide = my_triangle_colourbar()
  )

There isn't really a natural way to colour out-of-bounds values differently, but you can make very small slices near the extremes a different colour.

g + scale_colour_gradientn(
    colours = c("red", scales::viridis_pal()(255), "hotpink"),
    limits = c(3, 4), oob = scales::oob_squish,
    guide = my_triangle_colourbar()
  )

Created on 2021-07-19 by the reprex package (v1.0.0)

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • This is truly awesome! Have you considered asking for this function to be included in ggplot? – Ruben Jul 19 '21 at 15:26
  • No, ggplot2 would typically defer this to an extension package, as the maintaining and improving the core of ggplot2 is already a lot of work. They [don't "officially" support guide extensions](https://ggplot2-book.org/extensions.html#new-guides), but there are signs that guides will become officially extendable in the future. – teunbrand Jul 19 '21 at 16:18
  • This is a very neat solution and works smoothly for continuous color scales, @teunbrand! Unfortunately, I failed to apply it for a discrete color scale (e.g. if drat in the example above was cut() with breaks defined as c(-Inf,seq(3,4,0.1),Inf)). Would it be possible to adapt your solution to print the triangles for the ending boxes of a discrete color scale? Many thanks in advance!!! – Jens Daniel Müller Apr 14 '23 at 09:15
  • I've been working on a more generalised solution than this, but it requires a specific PR to be accepted by ggplot2, so I don't have any solution at the moment. – teunbrand Apr 14 '23 at 09:19
  • Thanks anyways for the immediate reply! – Jens Daniel Müller Apr 14 '23 at 09:57
1
library(gg.layers)
library(ggplot2)
library(rcolors)

brk <- c(-Inf, -1, 0, 1, 3, 6, 9, Inf)
nbrk <- length(brk) - 1
cols <- get_color(rcolors$amwg256, nbrk)

g <- make_colorbar(
  at = brk, col = cols, height = 1,
  tck = 0.4,
  space = "right",
  legend.text.location = c(0.3, 0.5),
  legend.text.just = c(0.5, 0.5),
  # legend.text = list(fontfamily = "Times", cex = 1.1),
  hjust = 0.05
)

p <- ggplot(mtcars, aes(mpg, disp)) + geom_point()
p + g

https://github.com/rpkgs/gg.layers

enter image description here

Dongdong Kong
  • 361
  • 2
  • 13
0

Triangles? No idea. Colors? You can set a gradient with custom values where your normal range is manually defined and your extremes are something else.

library(ggplot2)
# example taken from ?viridis::scale_colour_viridis, even if I don't use that function
dsub <- subset(diamonds, x > 5 & x < 6 & y > 5 & y < 6)
dsub$diff <- with(dsub, sqrt(abs(x-y))* sign(x-y))
d <- ggplot(dsub, aes(x, y, colour=diff)) + geom_point()
d +
  scale_color_gradientn(
    colours=c("red", "red", "blue", "green", "yellow", "red", "red"),
    values = c(0, 0.1-1e-9, 0.1, 0.5, 0.9, 0.9+1e-9, 1),
    breaks = c(-0.51, -.4, 0, .4, .62),
    label = function(z) replace(z, c(1, length(z)), c("Min", "Max"))) +
  theme_bw()

ggplot2 with gradients and red ends

I doubled "red" on each end so that there would be no gradient transition with the neighboring colors. You can choose a different color for one end (while in this case it's clear if it's extreme-high or extreme-low).

I chose to manually control values= and labels= to include arbitrary points and labels for the extremes. This can be improved based on your preferences.

The disadvantage to this is that you have to define the viridis colors manually; should not be too difficult. I've hastily approximated it here, I'm confident you can choose better colors for the internal gradient portion.

r2evans
  • 141,215
  • 6
  • 77
  • 149