Control alpha blending / opacity of n overlapping areas

Adding to @MKBakker's answer, one could use a function to predict the resulting alpha from any number of layers and alpha values:

alpha_out <- function(alpha, num = 1) {
  result = alpha
  if(num == 1)  return(result)
  for(i in 2:num) { result = result + alpha * (1-result) }
  return (result)
}

alpha_out(0.33, 1)
#[1] 0.33
alpha_out(0.33, 2)
#[1] 0.5511
alpha_out(0.33, 3)
#[1] 0.699237

This makes it easier to see that alpha asymptotically approaches 1 with more layers.

alpha_out(0.33, 40)
#[1] 0.9999999

If one presumes that 0.99 is "close enough," you need to use 0.8 to get there with three layers

alpha_out(0.8, 3)
#[1] 0.992

EDIT: Added chart of results

We can see what results we'd get from a range of alphas and layers:

library(tidyverse)
alpha_table <- 
  tibble(
    alpha = rep(0.01*1:99, 10),
    layers = rep(1:10, each = 99)
  )

alpha_table <- alpha_table %>%
  rowwise() %>%
  mutate(result = alpha_out(alpha, layers))

ggplot(alpha_table, aes(alpha, result, color = as_factor(layers),
                    group = layers)) +
geom_line()

enter image description here

And we can also see how much alpha we need to pass a threshold of combined opacity, given each number of layers. For instance, here's how much alpha you need to reach 0.99 total opacity for a given number of layers. For 5 layers, you need alpha = 0.61, for instance.

alpha_table %>%
  group_by(layers) %>%
  filter(result >= 0.99) %>%
  slice(1)
## A tibble: 10 x 3
## Groups:   layers [10]
#   alpha layers result
#   <dbl>  <int>  <dbl>
# 1  0.99      1  0.99 
# 2  0.9       2  0.99 
# 3  0.79      3  0.991
# 4  0.69      4  0.991
# 5  0.61      5  0.991
# 6  0.54      6  0.991
# 7  0.49      7  0.991
# 8  0.44      8  0.990
# 9  0.41      9  0.991
#10  0.37     10  0.990

All this to say that I don't think there is a simple implementation to get what you're looking for. If you want 100% dark in the overlapped area, you might try these approaches:

  • image manipulation after the fact (perhaps doable using imagemagick) to apply a brightness curve to make the dark areas 100% black and make the others scale to the darkness levels you expect.

  • convert the graph to an sf object and analyze the shapes to somehow count how many shapes are overlapping at any given point. You could then manually map those to the darkness levels you want.


Alpha can be added using the following approach (https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending)

The alpha of two merged shapes is calculated as follows:
A(out) = A(src) + A(dst) * 1-A(src)

Hence, for A(src) = A(dst) = 0.33, we get:

x = 0.33  
y = x + x*(1-x)  
y

[1] 0.5511

And if we have three shapes, with A = 0.33, we induce:

y = x + x*(1-x) + x*(1-(x + x*(1-x)))
y

[1] 0.699237

I could go on about which values will result in 1 when adding 2 or 3 shapes together, but the most useful comment is that alphas are not combined in an additive way.


First off, +1 to @JonSpring—this is just an expansion of the idea at the end of their answer. If you make an sf object, you can easily get the intersections of polygons. What you end up plotting isn't the circles themselves, but the polygons that come from splitting apart the intersecting pieces.

Starting from your grid, make a point for each row, convert that to a sf data frame, then take the buffer of the points at the radius given in the column r. This turns each point into a circle centered at the point's coordinates, and is flexible for different radii. Between the 3 circles are 6 intersecting polygons, as shown in the result.

library(dplyr)
library(sf)
library(ggplot2)
library(ggforce)

grid_df <- data.frame(x = c(1:2, 2.5), y = rep(1,3), r = 1)

grid_sf <- grid_df %>%
  mutate(geometry = purrr::map2(x, y, ~st_point(c(.x, .y)))) %>%
  st_as_sf() %>%
  st_buffer(dist = .$r, nQuadSegs = 60) %>%
  st_intersection()

grid_sf
#> Simple feature collection with 6 features and 5 fields
#> geometry type:  GEOMETRY
#> dimension:      XY
#> bbox:           xmin: 0 ymin: 0 xmax: 3.5 ymax: 2
#> epsg (SRID):    NA
#> proj4string:    NA
#>       x y r n.overlaps origins                       geometry
#> 1   1.0 1 1          1       1 POLYGON ((1.5 0.1339746, 1....
#> 1.1 1.0 1 1          2    1, 2 POLYGON ((1.75 0.3386862, 1...
#> 2   2.0 1 1          1       2 MULTIPOLYGON (((2.258819 0....
#> 1.2 1.0 1 1          3 1, 2, 3 POLYGON ((2 1, 1.999657 0.9...
#> 2.1 2.0 1 1          2    2, 3 POLYGON ((3 1, 2.999657 0.9...
#> 3   2.5 1 1          1       3 MULTIPOLYGON (((3.5 1, 3.49...

Use that n.overlaps column that comes from st_intersection to assign alpha. By default, alpha will scale from 0 to 1, but I figure you don't actually want a 0 alpha for the outer, non-overlapped parts of circles, so I scale it to get a minimum alpha.

alpha_range <- range(grid_sf$n.overlaps) / max(grid_sf$n.overlaps)

grid_sf  %>%
  ggplot() +
  geom_sf(aes(alpha = n.overlaps), fill = "black") +
  scale_alpha(range = alpha_range)

Just to expand a bit further and make the different polygons a bit more clear, take a look with a discrete fill scale instead of alpha:

grid_sf  %>%
  ggplot() +
  geom_sf(aes(fill = as.factor(n.overlaps))) +
  scale_fill_brewer(palette = "YlGnBu")

Tags:

Alpha

R

Ggplot2