ggplot2 icon indicating copy to clipboard operation
ggplot2 copied to clipboard

Feature request: midpoint argument in scale_color_gradientn()

Open EmilHvitfeldt opened this issue 4 years ago • 9 comments

Since there are now divergent continuous color palettes available in different packages, it would be nice to be able to specify a midpoint without manually having to calculate the limits.

related issue: https://github.com/thomasp85/scico/issues/6

EmilHvitfeldt avatar Jan 16 '20 06:01 EmilHvitfeldt

It's not clear to me that adding a mid argument to scale_color_gradientn() is the right approach, versus adding a new scale function, e.g. scale_color_gradientn_div(). Sequential and diverging scales are quite different conceptually, and I think it's important to emphasize this in the API rather than muddle things together. (If you think about how you'd implement the requested feature, you'd probably end up with a parameter mid with default NULL and would write two entirely different function bodies depending on whether mid is NULL or not.)

In the same vein, maybe scale_color_gradient2() should be deprecated in favor of something like scale_color_gradient_div().

clauswilke avatar Jan 16 '20 14:01 clauswilke

Just a thought on your last idea. I don’t think gradient2 is used for diverging scales as these usually have a midpoint colour, not just two extremes. I see no reason to deprecate it despite adding a sequential scale type (which I think is the right approach to this)

thomasp85 avatar Jan 16 '20 14:01 thomasp85

scale_*_gradient() sets up a sequential scale between two extremes: https://github.com/tidyverse/ggplot2/blob/214f3148d8a9a25ce80859645dbef38f9632b4fa/R/scale-gradient.r#L74-L78

scale_*_gradient2() sets up a diverging scale between two extremes, with a midpoint: https://github.com/tidyverse/ggplot2/blob/214f3148d8a9a25ce80859645dbef38f9632b4fa/R/scale-gradient.r#L93-L99

You just made my case for renaming scale_*_gradient2() :-)

clauswilke avatar Jan 16 '20 14:01 clauswilke

Haha - I guess I have been starring at code for too long

thomasp85 avatar Jan 16 '20 15:01 thomasp85

As it happens, I'm trying to get my head around using scales::rescale() for this purpose (a warming stripes plot.

rescale_div <- function(x) {
  scales::rescale(x,
    from = c(-max(abs(x), na.rm = TRUE), max(abs(x), na.rm = TRUE)))
}

This seems to scale my vector correctly, but I'm having trouble figuring out how I pop it into ggplot2:

library(tidyverse)
#> Warning: package 'ggplot2' was built under R version 3.6.1
library(lubridate)
#> 
#> Attaching package: 'lubridate'
#> The following object is masked from 'package:base':
#> 
#>     date
library(scales)
#> 
#> Attaching package: 'scales'
#> The following object is masked from 'package:purrr':
#> 
#>     discard
#> The following object is masked from 'package:readr':
#> 
#>     col_factor

# import data (set years to july 1)
aus_temps <-
  read_table(
    'http://www.bom.gov.au/web01/ncc/www/cli_chg/timeseries/tmean/0112/aus/latest.txt',
    col_names = c('year', 'anomaly'), col_types = 'cd') %>%
  mutate(year = ymd(substr(year, 1, 8)) + months(6) - days(18))

rescale_div <- function(x) {
  scales::rescale(x,
    from = c(-max(abs(x), na.rm = TRUE), max(abs(x), na.rm = TRUE)))
}

ggplot(aus_temps) +
  geom_tile(
    aes(x = year, y = 1, fill = anomaly)) +
  scale_fill_gradientn(
   rescaler = rescale_div,
    colours =
     c('#2166ac', '#4393c3', '#92c5de', '#d1e5f0',
       '#ffffff', '#fddbc7', '#f4a582', '#d6604d', '#b2182b'))
#> Error in f(...): unused argument (from = limits)

Created on 2020-01-23 by the reprex package (v0.3.0)

scale_fill_gradient2() is probably the better solution in my specific case, but since I'm trying to nail the warming stripes look, sometimes it's easier to just push a larger vector of colours in.

jimjam-slam avatar Jan 23 '20 00:01 jimjam-slam

@rensa Your comment doesn't really belong into this issue, nor into the ggplot2 issue tracker more generally. In general, I suggest you ask such questions on the RStudio forums or on stackoverflow.com. The issue tracker is for genuine bugs in the code and for feature requests.

In your specific case, the problem is that your function rescale_div() doesn't accept arguments to and from.

clauswilke avatar Jan 23 '20 03:01 clauswilke

No worries! Sorry about that :)

jimjam-slam avatar Jan 23 '20 03:01 jimjam-slam

Here's how I approached this problem, following the example from this related Stack Overflow question:

There's an internal function in ggplot2, mid_rescaler() that I find to be very useful - it's visible in @clauswilke's post above. I have adapted it a bit here:

library("ggplot2")

mid_rescaler <- function(mid = 0) {
  function(x, to = c(0, 1), from = range(x, na.rm = TRUE)) {
    scales::rescale_mid(x, to, from, mid)
  }
}

grid <- expand.grid(lon = seq(0, 360, by = 2), lat = seq(-90, 0, by = 2))
grid$z <- with(grid, cos(lat*pi/180) - .7)

ggplot(grid, aes(lon, lat)) +
  geom_raster(aes(fill = z)) +
  scale_fill_distiller(palette = "RdBu", rescaler = mid_rescaler())

Created on 2020-02-08 by the reprex package (v0.3.0)

As you know, the ... in color- and fill-scale functions get passed on to discrete_scale() or continuous_scale(). The continuous_scale() function has a rescaler argument which, itself, expects a function.

The mid_rescaler() function lets me specify the midpoint to build a rescaler function. To me, it seems like a minimally-disruptive solution, which ggplot2 is itself already using.

Could it be useful to make such a function available, publicly, in ggplot2?

Also thanks to @dpseidel, who listened very patiently as I waved my hands trying to describe this at rstudio::conf, while the right approach was to make a reprex.

ijlyttle avatar Feb 08 '20 16:02 ijlyttle

Hi, say I built a plot where the Y axis maps a numerical variable and the X axis a factor variable of 4 levels (i.e. 4 groups). Then I decide to map the data with a geom_jitter and colour the plot with a divergent gradient. My question is: is there a way for the midpoint of the divergent scale colour to start exactly at the median of each level (for this of course the legend would not make sense but as a way of visualising medians with colours).

image

ThomasDelSantoONeill avatar Feb 11 '22 13:02 ThomasDelSantoONeill

Because the rescaler argument accepts lambda syntax, one doesn't even need to write a new function for this.

library("ggplot2")

grid <- expand.grid(lon = seq(0, 360, by = 2), lat = seq(-90, 0, by = 2))
grid$z <- with(grid, cos(lat*pi/180) - .7)

ggplot(grid, aes(lon, lat)) +
  geom_raster(aes(fill = z)) +
  scale_fill_distiller(
    palette = "RdBu", 
    rescaler = ~ scales::rescale_mid(.x, mid = 0)
  )

Created on 2022-12-03 by the reprex package (v2.0.1)

teunbrand avatar Dec 03 '22 14:12 teunbrand