13

I struggle to understand (and control) the blending of alphas - unfortunately, alpha values don't simply "add up" (0.5 + 0.5 is not 1). But how could I achieve that?

The aim is to define the (absolute) grey value of overlapping areas relative to the total number of observations. See example below.

I tried to set scale_alpha(range = c(0,1)) to no avail, maybe I did not use it correctly.

library(ggplot2)
library(ggforce)

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

ggplot()+
geom_circle(data = grid_df, mapping = aes(x0 = x,  y0 = y, r = r), alpha = 0.33, fill = 'black') + 
  coord_fixed() 

enter image description here

tjebo
  • 21,977
  • 7
  • 58
  • 94
  • Maybe I don't understand the question, because the alphas seem to me to be approximately 33%, 66% and 100% to me. How would you rate the alphas now? Or is there something else you want to achieve? – P1storius Aug 07 '19 at 13:01
  • My question might not be clear enough. apologies. I’d like to set the opacity as per the percentages in the image - 100% would be black, i.e. full opacity – tjebo Aug 07 '19 at 13:03
  • 2
    Related recent blog: [My mental model of how alpha works is wrong](https://coolbutuseless.github.io/2020/02/14/my-mental-model-of-how-alpha-works-is-wrong/) – zx8754 Feb 17 '20 at 15:46
  • 1
    @zx8754 I have only now seen this link. That's a nice blog post !! Thanks for sharing – tjebo Aug 09 '20 at 14:33
  • 1
    Yeah, they need to change the name to "cool **and** useful". – zx8754 Aug 09 '20 at 19:43

3 Answers3

14

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")

camille
  • 16,432
  • 18
  • 38
  • 60
8

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.

Jon Spring
  • 55,165
  • 4
  • 35
  • 53
  • Could we just fork ggplot2 to change the formula of alpha blending to a simple addition ? – lrnv Feb 17 '20 at 11:34
7

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.

P1storius
  • 917
  • 5
  • 12