Three-Way Color Gradient Fill in R

three-way color gradient fill in r

Here is one way to do it - it's a bit of a hack, using points to plot the gradient piece by piece:

plot(NA,NA,xlim=c(0,1),ylim=c(0,1),asp=1,bty="n",axes=F,xlab="",ylab="")
segments(0,0,0.5,sqrt(3)/2)
segments(0.5,sqrt(3)/2,1,0)
segments(1,0,0,0)
# sm - how smooth the plot is. Higher values will plot very slowly
sm <- 500
for (y in 1:(sm*sqrt(3)/2)/sm){
for (x in (y*sm/sqrt(3)):(sm-y*sm/sqrt(3))/sm){
## distance from base line:
d.red = y
## distance from line y = sqrt(3) * x:
d.green = abs(sqrt(3) * x - y) / sqrt(3 + 1)
## distance from line y = - sqrt(3) * x + sqrt(3):
d.blue = abs(- sqrt(3) * x - y + sqrt(3)) / sqrt(3 + 1)
points(x, y, col=rgb(1-d.red,1 - d.green,1 - d.blue), pch=19)
}
}

And the output:

Sample Image

Did you want to use these gradients to represent data? If so, it may be possible to alter d.red, d.green, and d.blue to do it - I haven't tested anything like that yet though. I hope this is somewhat helpful, but a proper solution using colorRamp, for example, will probably be better.

EDIT: As per baptiste's suggestion, this is how you would store the information in vectors and plot it all at once. It is considerably faster (especially with sm set to 500, for example):

plot(NA,NA,xlim=c(0,1),ylim=c(0,1),asp=1,bty="n",axes=F,xlab="",ylab="")
sm <- 500
x <- do.call(c, sapply(1:(sm*sqrt(3)/2)/sm,
function(i) (i*sm/sqrt(3)):(sm-i*sm/sqrt(3))/sm))
y <- do.call(c, sapply(1:(sm*sqrt(3)/2)/sm,
function(i) rep(i, length((i*sm/sqrt(3)):(sm-i*sm/sqrt(3))))))
d.red = y
d.green = abs(sqrt(3) * x - y) / sqrt(3 + 1)
d.blue = abs(- sqrt(3) * x - y + sqrt(3)) / sqrt(3 + 1)
points(x, y, col=rgb(1-d.red,1 - d.green,1 - d.blue), pch=19)

R how to specify custom color gradients with breakpoints

You can set the exact points where a particular colour should be by using the values argument of the scale. In the example below, we want "darkblue" at 10, "lightblue" at 20 and "yellow" at 30.

The only catch is that the values argument works for rescaled values between 0 and 1, so we ought to fix the limits and apply the same rescaling to our breaks.

Lastly, because now values outside the 10-30 range are undefined, it might be best to repeat the colours at the extremes at the (rescaled) values 0 and 1.

library(ggplot2)

colour_breaks <- c(10, 20, 30)
colours <- c("darkblue", "lightblue", "yellow")

ggplot(mpg, aes(displ, hwy, colour = cty)) +
geom_point() +
scale_colour_gradientn(
limits = range(mpg$cty),
colours = colours[c(1, seq_along(colours), length(colours))],
values = c(0, scales::rescale(colour_breaks, from = range(mpg$cty)), 1),
)

Created on 2021-10-13 by the reprex package (v2.0.1)

As a small note: in your description it is unclear what the colour should be at 50: in one gradient it should be blue and in the other it should be lightblue. At best, you can set one of the colours (let's say blue) to 50 and use a 50 + a miniscule offset (e.g. .Machine$double.eps) for the lightblue break.

Add a gradient fill to geom_col

You can do this fairly easily with a bit of data manipulation. You need to give each group in your original data frame a sequential number that you can associate with the fill scale, and another column the value of 1. Then you just plot using position_stack

library(ggplot2)
library(dplyr)

diamonds %>%
group_by(cut) %>%
mutate(fill_col = seq_along(cut), height = 1) %>%
ggplot(aes(x = cut, y = height, fill = fill_col)) +
geom_col(position = position_stack()) +
scale_fill_viridis_c(option = "plasma")

Sample Image

Gradient fill area under curve

The following should be close to what you're looking for. The trick is to use scale_color_identity for the geom_segment, and passing to the color aesthetic an RGB string that represents each wavelength in your data frame.

ggplot(bq, aes(x=w.length, y=s.e.irrad)) +
geom_segment(aes(xend=w.length, yend=0, colour = nm_to_RGB(w.length)),
size = 1) +
geom_line() +
scale_colour_identity()

Sample Image

Or if you want a more muted appearance:

ggplot(bq, aes(x=w.length, y=s.e.irrad)) +
geom_area(fill = "black") +
geom_segment(aes(xend=w.length, yend=0,
colour = nm_to_RGB(w.length)),
size = 1, alpha = 0.3) +
geom_line() +
scale_colour_identity()

Sample Image

The only drawback being that you need to define nm_to_RGB: the function that converts a wavelength of light into a hex-string to represent a color. I'm not sure there's a "right" way to do this, but one possible implementation (that I translated from the javascript function here) would be:

nm_to_RGB <- function(wavelengths){
sapply(wavelengths, function(wavelength) {
red <- green <- blue <- 0
if((wavelength >= 380) & (wavelength < 440)){
red <- -(wavelength - 440) / (440 - 380)
blue <- 1
}else if((wavelength >= 440) & (wavelength<490)){
green <- (wavelength - 440) / (490 - 440)
blue <- 1
}else if((wavelength >= 490) && (wavelength<510)){
green <- 1
blue = -(wavelength - 510) / (510 - 490)
}else if((wavelength >= 510) && (wavelength<580)){
red = (wavelength - 510) / (580 - 510)
green <- 1
}else if((wavelength >= 580) && (wavelength<645)){
red = 1
green <- -(wavelength - 645) / (645 - 580)
}else if((wavelength >= 645) && (wavelength<781)){
red = 1
}
if((wavelength >= 380) && (wavelength<420)){
fac <- 0.3 + 0.7*(wavelength - 380) / (420 - 380)
}else if((wavelength >= 420) && (wavelength<701)){
fac <- 1
}else if((wavelength >= 701) && (wavelength<781)){
fac <- 0.3 + 0.7*(780 - wavelength) / (780 - 700)
}else{
fac <- 0
}
do.call(rgb, as.list((c(red, green, blue) * fac)^0.8))
})
}

Obviously, I don't have your data set, but the following code creates a plausible set of data over the correct ranges:


Data

set.seed(10)

bq <- setNames(as.data.frame(density(sample(rnorm(5, 600, 120)))[c("x", "y")]),
c("w.length", "s.e.irrad"))

bq$s.e.irrad <- bq$s.e.irrad * 1e5

Gradient of n colors ranging from color 1 and color 2

colorRampPalette could be your friend here:

colfunc <- colorRampPalette(c("black", "white"))
colfunc(10)
# [1] "#000000" "#1C1C1C" "#383838" "#555555" "#717171" "#8D8D8D" "#AAAAAA"
# [8] "#C6C6C6" "#E2E2E2" "#FFFFFF"

And just to show it works:

plot(rep(1,10),col=colfunc(10),pch=19,cex=3)

Sample Image

How merge two different scale color gradient with ggplot

Yes you could if you use the ggnewscale package:

a <- sample(nrow(iris), 75)

df1 <- iris[a,]
df2 <- iris[-a,]

library(ggnewscale)

ggplot(mapping = aes(Sepal.Width, Sepal.Length)) +
geom_point(data = df1, aes(colour = Petal.Length)) +
scale_colour_gradientn(colours = c("red", "black")) +
# Important: define a colour/fill scale before calling a new_scale_* function
new_scale_colour() +
geom_point(data = df2, aes(colour = Petal.Width)) +
scale_colour_gradientn(colours = c("blue", "white"))

Sample Image

Alternatives are the relayer package, or the scale_colour_multi/scale_listed from ggh4x (full disclaimer: I wrote ggh4x).

EDIT: Here are the alternatives:

library(ggh4x)

# ggh4x scale_colour_multi (for gradientn-like scales)
ggplot(mapping = aes(Sepal.Width, Sepal.Length)) +
geom_point(data = df1, aes(length = Petal.Length)) +
geom_point(data = df2, aes(width = Petal.Width)) +
scale_colour_multi(colours = list(c("red", "black"), c("blue", "white")),
aesthetics = c("length", "width"))

# ggh4x scale_listed (for any non-position scale (in theory))
ggplot(mapping = aes(Sepal.Width, Sepal.Length)) +
geom_point(data = df1, aes(length = Petal.Length)) +
geom_point(data = df2, aes(width = Petal.Width)) +
scale_listed(list(
scale_colour_gradientn(colours = c("red", "black"), aesthetics = "length"),
scale_colour_gradientn(colours = c("blue", "white"), aesthetics = "width")
), replaces = c("colour", "colour"))

library(relayer)

# relayer
ggplot(mapping = aes(Sepal.Width, Sepal.Length)) +
rename_geom_aes(geom_point(data = df1, aes(length = Petal.Length)),
new_aes = c("colour" = "length")) +
rename_geom_aes(geom_point(data = df2, aes(width = Petal.Width)),
new_aes = c("colour" = "width")) +
scale_colour_gradientn(colours = c("red", "black"), aesthetics = "length",
guide = guide_colourbar(available_aes = "length")) +
scale_colour_gradientn(colours = c("blue", "white"), aesthetics = "width",
guide = guide_colourbar(available_aes = "width"))

All the alternatives give warnings about unknown aesthetics, but this doesn't matter for the resulting plots. It is just a line of code in ggplot's layer() function that produces this warning and you can't go around this without either re-coding every geom wrapper or, as ggnewscale does, renaming the old aesthetic instead of providing a new aesthetic. The plots all look near-identical, so I figured I wouldn't have to post them again.

Gradient-color a line in ggplot2 R

One option to achieve that would be via ggforce::geom_link2:

library(ggforce)
#> Loading required package: ggplot2

d <- tibble::tibble(
`Country Name` = c('Estados Unidos', 'Estados Unidos'),
`Country Code` = c('USA', 'USA'),
Year = c(1961, 2020),
`CrecimientoPBI (%)` = c(2.3, -3.49),
`Inflación (%)` = c(1.07, 1.23),
`Desempleo (%)` = c(6.7, 8.05)
)

ggplot(d, aes(x = `Inflación (%)`, y = `Desempleo (%)`, color = factor(Year))) +
geom_point(aes(size = `CrecimientoPBI (%)`)) +
geom_link2(aes(group = 1)) +
scale_color_manual(values = c(`1961` = "gold", `2020` = "darkgreen")) +
theme_minimal() +
guides(size = "none")

Using a custom gradient fill based on two different columns

Here is an option without changing too much your code. I would divide the Apo by 3 instead of multiplying, like so all Apo are <5 and Qt>10:

> unique(usc_map_ex$Qt)
[1] NA 10 20 30 40 50 60 70 80 90 100 110 120 140
> unique(usc_map_ex$Apo)/3
[1] 0.3333333 NA 0.6666667 1.0000000 1.3333333 1.6666667 2.0000000 2.3333333 2.6666667 3.0000000 3.3333333 3.6666667 4.0000000 4.3333333

Then for the plot itself perhaps solid colors would be better than the palette so I just set the color to the last of your brewer.pal() call.

mcolor <- c(rep("#31A354", 3), rep("#3182BD", 3))

## plot
ggplot() +
# polygon for Qt
geom_polygon(data = {usc_map_ex %>% filter(is.na(Apo))},
aes(x,y, group = group, fill = (Qt), color = "")) +
# polygon for Apo (multiplyong by three to have a different range of values than Qt)
geom_polygon(data = {usc_map_ex %>% filter(is.na(Qt))},
aes(x,y, group = group, fill = (Apo)/3, color = "")) +
# adding the outline of each state
geom_polygon(data = us,
aes(x,y, group = group), fill = NA, color = "black") +
# removing the outline of counties
scale_colour_manual(values = 'transparent', guide = "none") +
# custom fill gradient to have different shades of colors for each variable
scale_fill_gradientn(name = "test",
breaks = c(1,5,10, 50),
colors = mcolor,
guide = guide_colorbar(barwidth = 0.8, barheight = 18),
trans = "log") +

theme_void()

Sample Image

Update with color shade

If you want the color shade from your code you would need to reverse order (rev). Currently the greens are going light to dark then blues light to dark again. By reversing the order of the greens, your color vector become dark green to light green/blue to dark blue.
Plus I don't think you need the colorRampPalette as the 3 colors are already a palette. New color vector :

mcolor <- c(rev(brewer.pal(3, "Greens")), brewer.pal(3, "Blues"))

Sample Image

Update 2 reverse order:

This way is probably best to highlight your data. By reverting the second set so the extreme value are in the darkest colors rather than light. (keep in mind that there is a little bias for the "Qt" as the 1 on the scale is effectively 13 for Qt value but that remain the lowest one therefore light color).

ggplot() + 
# polygon for Qt
geom_polygon(data = {usc_map_ex %>% filter(is.na(Apo))},
aes(x,y, group = group, fill = (Qt), color = "")) +
# polygon for Apo (multiplyong by three to have a different range of values than Qt)
geom_polygon(data = {usc_map_ex %>% filter(is.na(Qt))},
aes(x,y, group = group, fill = rev(Apo), color = "")) +
# adding the outline of each state
geom_polygon(data = us,
aes(x,y, group = group), fill = NA, color = "black") +
# removing the outline of counties
scale_colour_manual(values = 'transparent', guide = "none") +
# custom fill gradient to have different shades of colors for each variable
scale_fill_gradientn(name = "test",
breaks = c(1, 4,9, 13,25, 50, 100),
labels = c(13, 10,5, 1,25, 50, 100),
colors = mcolor,
guide = guide_colorbar(barwidth = 0.8, barheight = 18),
trans = "log") +
theme_void()

Sample Image

Manual scaling of color gradient with scale_color_gradientn() in R

Since you provided no value/colour pair for the upper limit of the colour scale, these got interpreted as NAs and became gray. Fixing this should be as easy as providing the upper limit too.

library(ggplot2)
library(scales)
#> Warning: package 'scales' was built under R version 3.6.3

df <- data.frame(x = seq(0, 15 , 0.001),
y = seq(0, 15, 0.001))

# Plot data
ggplot(df, aes(x=x, y=y, col = y)) +
geom_line() +
scale_color_gradientn(colours = c("green", "black", "red", "red"),
values = rescale(x = c(0, 2, 4, 15), from = c(0, 15)))

Created on 2020-04-24 by the reprex package (v0.3.0)



Related Topics



Leave a reply



Submit