Everything from the basics up to beautiful mesh gradients
Published
February 24, 2025
We can do a lot of fancy stuff in ggplot2 with considered use of fonts, sizes and colours. But there are still likely finishing touches that have us reaching for tools like Illustrator every so often.
Until recently, sophisticated gradient effects were one of those things. I often blur photos or use other similar patterns as slide backgrounds to provide a bit (a bit!) of visual interest.
But now, it’s not hard to do something like that directly in ggplot2!
Gradients and patterns are looking reasonably mature now in R: they’ve been around since R 4.1 (released in 2021), and ggplot2 began supporting them in version 3.5.0 (released in early 2024).
That support comes via the {grid} package, which ships with R but isn’t preloaded. That means you’ll either need to call library(grid) or prefix a lot of functions with grid::.
I’m also going to define a quick theme to keep these plots looking tidy. It’s not really relevant to the post, though, so you can skip it if you’d like!
Some legacy R graphics devices don’t support gradients and patterns at all. Install the {ragg} package, which does — it’s better for just about everything anyway!
I also refer to the {systemfonts} package above. This is an optional one to make using fonts on your system—or ones you’ve downloaded—easier.
We get two basic gradient tools in {grid}: linearGradient() and radialGradient(). If you’ve used apps like Photoshop or Illustrator before, that might seem basic — it’s no mesh gradient.
But even this gives us a lot of power. With this new support, you can use a gradient or pattern as a static fill for a layer or theme element, or even as an aesthetic in some cases. (You can’t, as far as I’m aware, use one as the outer colour.)
You’re probably already realising that with this power comes the potential to make a lot of ugly charts—or, perhaps worse, unengaging or misleading ones!
We’ll be looking at a lot of gradients in this post. Let’s turn this example into a function we can re-use:
All of the rules that apply to the accessible use of colour in data visualisation apply to gradients and patterns as well. Ensure that plot elements have sufficient contrast to be distinguished, particularly if the meaning of the plot is lost without it.
But with some care, we can add sophisticated effects using these tools.
Gradient basics
The most basic part of linearGradient() and radialGradient() is specifying the colours they use. Supply a vector of colours the same way you would supply colours elsewhere in R.
If you don’t want those colours to be distributed evenly along the gradient, supply a vector of stops between 0 and 1. Here we squash the first half of the rainbow, spreading the second half out
By default, the two gradients take up the space of all of their parent: linearGradient() goes left-to-right and radialGradient() goes from the centre out to the (shorter) edge.
To change that, use the positional arguments. For linearGradient, those are x1 and y1 for the start position, and x2 and y2 for the end position. For example, to
If you simply provide these as numbers, the gradient will interpret these in “Normalised Parent Coordinates”, which basically means “percentages of the thing I’m being put on”. 0 is the (horizontal or vertical) start of the thing you’re painting, and 1 is the end.
Here’s a gradient that stretches from the bottom-left corner to the top-right:
One downside of this is that it makes getting a specific angle right difficult, because a wide object and a tall object have things that stretch differently:
```{r}#| fig-width: 0.5#| fig-height: 1.5#| fig-alt: A tall gradient, going from bottom-left to top-right.p1```
Figure 1
```{r}#| fig-width: 1.5#| fig-height: 0.5#| fig-alt: A wide gradient, going from bottom-left to top-right.p1```
Figure 2
But you can change the way these numbers are interpreted using default.units = "snpc". Square NPCs let us use the shorter side of the device for both height and width, so we’ll get the same angles regardless of how wide or tall something is.
```{r}#| fig-width: 0.5#| fig-height: 1.5#| fig-alt: A tall gradient. The gradient maintains a 45 degree angle.p2```
Figure 3
```{r}#| fig-width: 1.5#| fig-height: 0.5#| fig-alt: A wide gradient. The gradient maintains a 45 degree angle.p2```
Figure 4
You can also set default.units to a physical or other unit, like "cm" or "char". Check out ?unit for all the options. You can also supply a full unit() to these parameters if you want to mix units!
I find snpc and the default npc to be the most useful for gradients, but there are cases where you’ll want physical units—especially when we start making repeating patterns.
Sizing and positioning radial gradients
Radial gradients work in a similar way, but now we have a lot more parameters: cx1, cy1, cx2andcy2 for the centre points of the gradient, plus r1 and r2 for the radius of the start and end radii.
For most applications, you’ll want cx2 and cy2 to be the same as cx1 and cy1. Then imagine two concentric circles around that point, with radii r1 and r2. These rings are the start and end of your gradient.
In fact, let’s draw the circles explicitly to help us visualise:
The key to this is continuing to imagine those circles drawn around the two points. Before they were concentric, but now they have room to move.
The defaults for the second circle are cx2 = 0.5, cy2 = 0.5 and r2 = 0.5, which traces out a circle touching the edges of the parent. If you think of positioning and sizing the first point as positioning the spotlight within that circle, that might help you build a mental model of what to expect.
But if the centre of the first circle escapes (or grazes) the edge of the second circle, you get weirder effects:
There’s one other important parameter for linearGradient() and radialGradient().
The group parameter (available since R 4.2.0) controls whether a gradient applies to individual shapes or to a set of them. It’s TRUE by default. The difference is pretty obvious in some cases:
Without grouping, every bar gets to the intense red or blue at the end of the gradient. If you’re using the change in colour to reinforce the Y axis, this could be misleading!
Ready to go even further with gradients?
More advanced: stacking gradients
As web and print designers know, we can make some powerful effects by stacking gradients on top of each other. And the third tool we now have in R, pattern(), lets us do exactly that.
The secret to pattern()’s power is that it’s recursive. We can turn any graphical object (called a “grob”) into a repeating pattern.
That grob could be a picture you’ve provided separately, it could be a shape… or it could be a list of shapes!
@coolbutuselessexplored the possibilities of these recursive pattern()s a while back, but we can put them to good use with gradients too.
So here’s a helper function to help us “stack” gradients. It does two things.
The first is to wrap each pattern in a rectangle grob, or rectGrob(). Rectangles can be sized and positioned with x, y, width and height parameters, but if we’re stacking gradients on top of each other, we can leave them all at their defaults.
The second step is to do the stacking. We’ll use grobTree() to hold our list of rectangles before passing them to pattern().
We’ll also throw in some error checking to make sure that the arguments we’re passing are actually patterns. And since we’ll likely want to include at least one solid colour as well to serve as a background, let’s keep those as well.
In R, valid colours are either those named in colours() or 6- or 8-digit hex numbers prefixed with a hashtag. Things like:
"red" or #ff0000
#00ff00aa is a semi-transparent green
stack_patterns <-function(...) { patterns <-list(...)# helper function to check for solid colours is_valid_colour <-function(x) {is(x, "character") && (x %in%colours() ||grepl("^\\#[0-9a-fA-F]{6}$", x) ||grepl("^\\#[0-9a-fA-F]{8}$", x)) }# check if any are not a pattern or colourstopifnot("All supplied arguments must be patterns"= patterns |>sapply(\(x) is(x, "GridPattern") ||is_valid_colour(x)) |>all() )# wrap each gradient in a grob patterns |>lapply(\(x) grid::rectGrob(gp = grid::gpar(fill = x))) -> pattern_grobs# return as a compound pattern grid::pattern(do.call(grid::grobTree, pattern_grobs),extend ="none")}
We can use stack_patterns() anywhere we would’ve used a single gradient. But what sort of gradients should we stack?
There are lots of great tools for building pseudo-mesh gradients out of radial ones, like this generator on Colorffy.
Although CSS gradients aren’t specified in exactly the same way as R’s gradients, you can convert them pretty quickly. For example, here’s the CSS code for a splashy mix of bright colours:
On Colorffy, use the <> button to display the CSS code for the gradient.
In CSS, each radial gradient starts with its position, as at X Y. Those become our cx1, cy1, cx2 and cy2 arguments (remember that we repeat the values again for cx2 and cy2).
One wrinkle is that the y-coordinates need to be flipped from the web (ie. top becomes bottom), so you’ll want to subtract 1: 70% becomes 0.3, not 0.7!
The Colorffy generator ends each radial gradient at the 50% mark in transparency to get the splotchy look.
We don’t have a "transparent" keyword here. Instead, I’ll repeat the original colour but make it transparent by adding zero opacity (00) to the end of it. At the start they’re fully opaque (ff).
Once we have our stack of gradients, we can use the gradient pattern as a plot background or just about anything else in {ggplot2}:
preview_gradient(stacked_gradient_bg)
Of course, if you’re using this as a background for an actual plot, you might need to dial things back to make the content readable.
One easy way to do that is to just make all the colours semi-transparent:
Of course, you don’t have to stack radial gradients that fall off to transparency. You can combine all sorts of radial gradients, linear gradients and even other patterns composed of basic shapes.
If you’re looking for some pattern inspiration, @coolbutuseless’s series of posts on building patterns is a great place to start!
Your new gradient powers
We’ve covered a lot here! We looked at:
Creating linear and radial gradients of various colours and sizes
Using those gradients both as static plot elements and as aesthetics
Stacking gradients to create powerful visual effects
Ready to go out and add some colour to your plots?
Using a different colour space can be really useful, especially if your gradient uses colours that are far apart. Notice the grey colour in the middle of this gradient:
I haven’t yet found any option for setting the colour space in which to create a linearGradient() or a radialGradient(), and I assume that doing so would require the graphics device to support it (my understanding is that the current ones all work in RGB).
If you know of such an option, let me know!
In the mean time, the best solution is to add some stops that are pre-computed in the interpolation space you want. CSS Tricks links to an excellent app by Tom Quinonero that demonstrates the difference: