10

I'd like to reproduce this color scale in ggplot2: enter image description here (Source)

In the past I have found that creating discrete color scales with labels in-between in ggplot2 can be tricky.

Can this be accomplished at all? A similar, but not completely identical question I have recently posed is this one.

AF7
  • 3,160
  • 28
  • 63
  • Possibly related/helpful: https://stackoverflow.com/questions/18487369/ggplot-set-scale-color-gradientn-manually?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa – LAP May 24 '18 at 10:31
  • Another possibly helpful thread: https://stackoverflow.com/questions/30352412/how-do-you-create-a-gradient-of-colors-for-a-discrete-variable-in-ggplot2?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa – LAP May 24 '18 at 10:40
  • isn't it sth. like `scale_color_gradient2(low="yellow",mid = "orange",high = "red",midpoint = 1000)`? – Roman May 24 '18 at 14:11
  • @Tjebo this would involve modifying the `ggplot` source code tho, something that is way above my capabilities unfortunately. – AF7 May 25 '18 at 10:16
  • For future searches to answer the question how to position the label in the middle of the bins https://stackoverflow.com/a/67358007/7941188 – tjebo May 02 '21 at 15:31

5 Answers5

12

EDIT: since ggplot 3.3.0, binned scales are now built-in to ggplot. This answer is obsolete, refer to tjebo's second answer for a more detailed example.

Thanks to Tjebo's answer, I managed to create a function that plots a nice colorbar, to be added to plots by using cowplot, patchwork or other similar packages like in his example.

Here it is: EDIT: you can find it also on github

plot_discrete_cbar = function(
    breaks, # Vector of breaks. If +-Inf are used, triangles will be added to the sides of the color bar
    palette = "Greys", # RColorBrewer palette to use
    colors = RColorBrewer::brewer.pal(length(breaks) - 1, palette), # Alternatively, manually set colors
    direction = 1, # Flip colors? Can be 1 or -1
    spacing = "natural", # Spacing between labels. Can be "natural" or "constant"
    border_color = NA, # NA = no border color
    legend_title = NULL,
    legend_direction = "horizontal", # Can be "horizontal" or "vertical"
    font_size = 5,
    expand_size = 1, # Controls spacing around legend plot
    spacing_scaling = 1, # Multiplicative factor for label and legend title spacing
    width = 0.1, # Thickness of color bar
    triangle_size = 0.1 # Relative width of +-Inf triangles
) {
    require(ggplot2)
    if (!(spacing %in% c("natural", "constant"))) stop("spacing must be either 'natural' or 'constant'")
    if (!(direction %in% c(1, -1))) stop("direction must be either 1 or -1")
    if (!(legend_direction %in% c("horizontal", "vertical"))) stop("legend_direction must be either 'horizontal' or 'vertical'")
    breaks = as.numeric(breaks)
    new_breaks = sort(unique(breaks))
    if (any(new_breaks != breaks)) warning("Wrong order or duplicated breaks")
    breaks = new_breaks
    if (class(colors) == "function") colors = colors(length(breaks) - 1)
    if (length(colors) != length(breaks) - 1) stop("Number of colors (", length(colors), ") must be equal to number of breaks (", length(breaks), ") minus 1")
    if (!missing(colors)) warning("Ignoring RColorBrewer palette '", palette, "', since colors were passed manually")

    if (direction == -1) colors = rev(colors)

    inf_breaks = which(is.infinite(breaks))
    if (length(inf_breaks) != 0) breaks = breaks[-inf_breaks]
    plotcolors = colors

    n_breaks = length(breaks)

    labels = breaks

    if (spacing == "constant") {
        breaks = 1:n_breaks
    }

    r_breaks = range(breaks)

    cbar_df = data.frame(stringsAsFactors = FALSE,
        y = breaks,
        yend = c(breaks[-1], NA),
        color = as.character(1:n_breaks)
    )[-n_breaks,]

    xmin = 1 - width/2
    xmax = 1 + width/2

    cbar_plot = ggplot(cbar_df, aes(xmin=xmin, xmax = xmax, ymin = y, ymax = yend, fill = factor(color, levels = 1:length(colors)))) +
        geom_rect(show.legend = FALSE,
                  color=border_color)

    if (any(inf_breaks == 1)) { # Add < arrow for -Inf
        firstv = breaks[1]
        polystart = data.frame(
            x = c(xmin, xmax, 1),
            y = c(rep(firstv, 2), firstv - diff(r_breaks) * triangle_size)
        )
        plotcolors = plotcolors[-1]
        cbar_plot = cbar_plot +
            geom_polygon(data=polystart, aes(x=x, y=y),
                        show.legend = FALSE,
                        inherit.aes = FALSE,
                        fill = colors[1],
                        color=border_color)
    }
    if (any(inf_breaks > 1)) { # Add > arrow for +Inf
        lastv = breaks[n_breaks]
        polyend = data.frame(
            x = c(xmin, xmax, 1),
            y = c(rep(lastv, 2), lastv + diff(r_breaks) * triangle_size)
        )
        plotcolors = plotcolors[-length(plotcolors)]
        cbar_plot = cbar_plot +
            geom_polygon(data=polyend, aes(x=x, y=y),
                        show.legend = FALSE,
                        inherit.aes = FALSE,
                        fill = colors[length(colors)],
                        color=border_color)
    }

    if (legend_direction == "horizontal") { #horizontal legend
        mul = 1
        x = xmin
        xend = xmax
        cbar_plot = cbar_plot + coord_flip()
        angle = 0
        legend_position = xmax + 0.1 * spacing_scaling
    } else { # vertical legend
        mul = -1
        x = xmax
        xend = xmin
        angle = -90
        legend_position = xmax + 0.2 * spacing_scaling
    }

    cbar_plot = cbar_plot +
        geom_segment(data=data.frame(y = breaks, yend = breaks),
            aes(y=y, yend=yend),
            x = x - 0.05 * mul * spacing_scaling, xend = xend,
            inherit.aes = FALSE) +
        annotate(geom = 'text', x = x - 0.1 * mul * spacing_scaling, y = breaks,
                label = labels,
                size = font_size) +
        scale_x_continuous(expand = c(expand_size,expand_size)) +
        scale_fill_manual(values=plotcolors) +
        theme_void()

    if (!is.null(legend_title)) { # Add legend title
        cbar_plot = cbar_plot +
            annotate(geom = 'text', x = legend_position, y = mean(r_breaks),
                label = legend_title,
                angle = angle,
                size = font_size)
    }

    cbar_plot
}

Example usage:

plot_discrete_cbar(c(1:10))

enter image description here

plot_discrete_cbar(c(0,2,5,10,20, Inf), palette="Reds")

enter image description here

plot_discrete_cbar(c(0,2,5,10,20, Inf), colors=rainbow, legend_direction="vertical", legend_title="A title! WOW!", border_color="red")

enter image description here

plot_discrete_cbar(c(-Inf, -8, -4, -2, -1, 1, 2, 4, 8, Inf), palette="BrBG", legend_title="Precipitation bias (mm/day)")

enter image description here

plot_discrete_cbar(c(-Inf, -8, -4, -2, -1, 1, 2, 4, 8, Inf), palette="BrBG", legend_title="Precipitation bias (mm/day)", spacing="constant")

enter image description here

AF7
  • 3,160
  • 28
  • 63
  • 2
    @Tjebo Thanks! I've asked Hadley in the past to implement something like this (but way more refined, as a built-in type of color guide), but he said that the usecase was not compelling enough. I'll get around to opening a more detailed issue sooner or later, showing how most other plotting systems allow for this kind of color bar. – AF7 May 27 '18 at 19:29
  • 2
    @Tjebo good news! Hadley said this kind of scales could be incorporated in ggplot! https://github.com/tidyverse/ggplot2/issues/2673 – AF7 May 31 '18 at 05:41
  • @tjebo sorry, That was not my intention. There is now a better answer (using the new ggplot built-ins), which I added to my own (at the beginning, with an edit). I don't know which is the right procedure in this case, should I create a new answer? – AF7 May 08 '22 at 13:37
  • 1
    @tjebo, oh right, I got confused too. So I accepted your second answer and will add an edit to my own pointing to it! – AF7 May 10 '22 at 10:16
6

Inspired by @Henrik 's answer in this question, a possible workaround is to make a plot that looks like a legend :)

require(ggplot2)
require(cowplot)


values <- c(0,1,2,5,10) # this vector is needed not only for the data frame cbar, but also for plotting
group <- letters[1:5]
diff_values <- c(0, diff(values))
cbar_df <- data.frame(x = 1, y = values, diff_values,group,  stringsAsFactors = FALSE)
#that's for the fake legend

iris2 <- iris #don't want to mess with your iris data set
              #I used iris because you hadn't provided data
iris2$cuts <- cut(iris2$Petal.Length, values) #the already offered 'cut-approach' 


p1  <- ggplot(iris2, aes(Sepal.Length, y = Sepal.Width, color = cuts))+ 
          geom_point() +
          scale_color_brewer("", palette = "Reds")

cbar_plot <- ggplot(cbar_df, aes(x, y = diff_values, fill = c(NA, rev(group[2:5])))) + 
  # I had to do implement this 'fill=' workaround 
  # in creating a new vector introducing an NA, 
  # and I had to flip the fills in order to fit to the scale... 
    geom_col(width = 0.1, show.legend = FALSE)  +
    geom_segment(y = values, yend = values, x = 0.9, xend = 1.05) +
    annotate(geom = 'text', x = 0.85, y = values, label = values) +
  # the numbers are quasi-randomly chosen but define the length of your ticks, obviously
    scale_x_continuous(expand = c(1,1)) + 
  # you might need to play around with the expand argument for the width of your legend
    scale_fill_brewer("", palette = "Reds", direction = -1) +  
  # don't know why you have to flip this again... 
    coord_flip() +
    theme_void()

plot_grid(p1, cbar_plot, nrow = 2)

enter image description here

I mean - there is certainly a lot of room for improvement (making the legend plot smaller etc...). But what do you think?

#P.S. 
sessionInfo() 
cowplot_0.9.2 ggplot2_2.2.1
tjebo
  • 21,977
  • 7
  • 58
  • 94
  • Very interesting solution. Combining plots in `ggplot` is always a bit tricky, since spacing, alignement etc. always pose some problems... but this is really a great start. Do you think this solution could be encompassed in a generic function, or does it require too much playing around with the numerical constants you used (e.g. `expand`)? – AF7 May 25 '18 at 10:00
  • What about "Create discrete color bar with varying interval widths?" – AF7 May 25 '18 at 10:56
  • I'm working on a function to generalize this solution. – AF7 May 25 '18 at 13:57
  • I have conjured together this: https://pastebin.com/Yw78bC0A. It works beautifully. Do you mind if I paste and accept an answer with that content, mentioning yours? – AF7 May 25 '18 at 15:16
  • Am at the beach, so cannot check this right now :D Sure go for it. But I would recommend a better function name - as recommended by Hadley etc, I would suggest using a verb for the name, which tells what the function is doing. Such as “create_cbar_discrete” (odd name, was just an example) – tjebo May 25 '18 at 15:35
  • Yeah, that weird function name was definitely just a placeholder :) – AF7 May 25 '18 at 17:18
  • posted an answer, see if you like it! Thanks. – AF7 May 26 '18 at 07:57
6

I think the following answer is sufficiently different to merit a second answer. ggplot2 has massively changed in the last 2 years (!), and there are now new functions such as scale_..._binned, and specific gradient creating functions such as scale_..._fermenter

This has made the creation of a discrete gradient bar fairly straight forward.

For a "full separator" instead of ticks, see user teunbrands post.

library(ggplot2)

ggplot(iris, aes(Sepal.Length, y = Sepal.Width, fill = Petal.Length))+
  geom_point(shape = 21) +
  scale_fill_fermenter(breaks = c(1:3,5,7), palette = "Reds") + 
  guides(fill = guide_colorbar(
    ticks = TRUE, 
    even.steps = FALSE,
    frame.linewidth = 0.55, 
    frame.colour = "black", 
    ticks.colour = "black",
    ticks.linewidth = 0.3)) +
  theme(legend.position = "bottom")

tjebo
  • 21,977
  • 7
  • 58
  • 94
1

You can use

scale_fill_gradientn (guide = guide_colourbar(nbin = 255))

to achieve.

you can set nbin to whatever you like.

tjebo
  • 21,977
  • 7
  • 58
  • 94
Ru Xu
  • 11
  • 1
0

You can try. For the first answer see also here

# data
set.seed(1324)
dat <- data.frame(x=0:100, y=runif(101, 0, 10), z=seq(0, 12, len=101))
# add discrete values
dat$col <- cut(include.lowest = T,
  dat$z, 
  breaks=c(0, 2, 4, 6, 8, max(dat$z)), 
  labels=c(2, 4, 6, 8, 10)
)
# Discete
ggplot(dat, aes(x,y,fill=col)) + 
  geom_point(aes(col=col),size=8, show.legend = F) +   
  geom_col(alpha=0)+
  scale_fill_brewer("", palette = "Reds")+
  scale_colour_brewer("", palette="Reds")+
  scale_alpha_discrete(range=c(0,1))+
  guides(fill = guide_legend(nrow=1,override.aes = list(alpha = 1), 
                             label.position="bottom",
                             label.hjust = -0.05)) +
  theme(legend.position="bottom",
        legend.key.width = unit(3, "cm"), 
        legend.key.height = unit(1, "cm"))

enter image description here

# Continuous
ggplot(dat, aes(x,y,color=y)) + 
  geom_point(size=8) +   
  scale_colour_continuous("",limits=c(0,10), 
                          breaks=c(0, 2, 4, 6, 8, 10),low = "white", high = "red")+
  theme(legend.position="bottom",
        legend.key.width = unit(3, "cm"), 
        legend.key.height = unit(1, "cm"))

enter image description here

Roman
  • 17,008
  • 3
  • 36
  • 49
  • This is not much different from what I already stated in the question, as you can see the "found" link. Thanks anyway! – AF7 May 25 '18 at 09:55