Heatmap within a triangle

Viewed 214

Consider the following example:

triangle_lines <- data.frame(
  X   = c(0,0,1,1,0.5,0.5),
  Y   = c(0,0,0,0,1,  1),
  grp = c(1,2,1,3,2,3)
)

df <- 
  matrix(c(c(0.2,0.5,0.8), c(0.3,0.5,0.1), c(1,5,10)), ncol=3) %>%
  as.data.frame()
colnames(df) <- c("x","y", "amount")

ggplot(df, aes(x=x, y=y)) +
  geom_point(aes(colour = amount)) +
  geom_line(data=triangle_lines, aes(X, Y, group = grp)) +
  theme_void() 

Plot

We have three values: the x coordinates, the y coordinates, and amount that decides the colour values of the scatter points. The points are not equally spaced apart.

Also, I have a triangle. I am attempting to create a gradient heatmap inside this triangle only, filling the entire triangle with heatmap colours based on df$amount. Any idea how to solve this?

P.S. While I am exemplifying with only three points here, this plot will have tens or hundreds of points in real applications.

1 Answers

Not the cleanest solution, but it works.

First we initiate an empty list.

Then we create a sequence of lambda values, one for each axis, that is used to create a weighted average between two points on that scale (namely, between 0 and 1). In this example, there are 101 lambdas per axis, creating 101^2 iterations.

We then check if the coordinate generated lies inside the triangle with sp::point.in.polygon. Note that this method applies to more shapes than just a triangle, so this solution is general for multiple shapes.

If the coordinate generated from the lambda values lies inside the polygon, then we calculate the distances from this coordinate to every coordinate in df. Note that we take sqrt(2) minus the distance because the lower the distance, the more weight that point should carry. Hence, we take the maximum distance (sqrt(2)) and subtract the distance. sqrt(2) is not a definite number that must be set, but it does prevent negative values. Other values provide other results.

In the next step, we scale the distances so that they sum to 1. That allows us to create a weighted average, which is defined in amount.

After running the loops, we bind the lists into a data frame and create the plot.

To ensure that the edges are smooth, we make somewhat thick white lines that have rounded edges.

gradient_list <- list()
for (lambda_x in seq(0,1,by=0.01)) {
  for (lambda_y in seq(0,1,by=0.01)) {
    x_value <- lambda_x*0 + (1-lambda_x)*1
    y_value <- lambda_y*0 + (1-lambda_y)*1
    
    inside_polygon <- sp::point.in.polygon(x_value, y_value, triangle_lines$X, triangle_lines$Y) %>% as.logical()
    
    if (inside_polygon) {
      point <- c(x_value, y_value)
      distances <- sqrt(2) - sqrt((df$x - point[1])^2 + (df$y - point[2])^2)
      weighted_distances <- distances/sum(distances)
      amount <- sum(weighted_distances * df$z)
      gradient_list <- append(gradient_list, list(c(point, amount)))
    }
  }
}

gradient_df <- do.call(rbind, gradient_list) %>% as.data.frame()
colnames(gradient_df) <- c("x","y","amount")


ggplot(gradient_df, aes(x=x, y=y)) + 
  geom_point(aes(colour = amount), size=2) +
  theme_void() + 
  geom_line(data=triangle_lines, aes(X, Y, group = grp), size=3, colour="white", lineend="round")

enter image description here

Related