rggplot2colors3drgl

How to create heatmap illustraing mesh differences controlling the position of center color for divergence color palette?


I have two 3D meshes of human faces and I wish to use heatmap to illustrate differences. I want to use red-blue divergent color scale.

My data can be found here. In my data, "vb1.xlsx" and "vb2.xlsx" contain 3D coordinates of the two meshes. "it.xlsx" is the face information. The "dat_col.xlsx" contains pointwise distances between the two meshes based on which heatmap could be produced. I used the following code to generate the two meshes based on vertex and face information. I then used the meshDist function in Morpho package to calculate distances between each pair of vertex on the two meshes.

library(Morpho)
library(xlsx)
library(rgl)
library(RColorBrewer)
library(tidyverse)


mshape1 <- read.xlsx("...\\vb1.xlsx", sheetIndex = 1, header = F)
mshape2 <- read.xlsx("...\\vb2.xlsx", sheetIndex = 1, header = F)

it <- read.xlsx("...\\it.xlsx", sheetIndex = 1, header = F)

  
# Calculate mesh distance using meshDist function in Morpho package
mD <- meshDist(mesh1smooth, mesh2smooth)
pd <- mD$dists

The pd, containing information on pointwise distances between the two meshes, can be found in the first column of the "dat_col.xlsx" file.

A heatmap is generated from the meshDist function as follows: enter image description here

I wish to have better control of the heatmap by using red-blue divergent color scale. More specifically, I want positive/negative values to be colored blue/red using 100 colors from the RdBu color pallete in the RColorBrewer package. To do so, I first cut the range of pd values into 99 intervals of equal lengths. I then determined which of the 99 intervals does each pd value lie in. The code is as below:

nlevel <- 99

breaks <- NULL
for (i in 1:(nlevel - 1)) {
    breaks[i] <- min(pd) + ((max(pd) - min(pd))/99) * i
}

breaks <- c(min(pd), breaks, max(pd))

pd_cut <- cut(pd, breaks = breaks, include.lowest = TRUE)

dat_col <- data.frame(pd = pd, pd_cut = pd_cut, group = as.numeric(pd_cut))

The pd_cut is the inteval corresponding to each pd and group is the interval membership of each pd. Color is then assgined to each pd according to the value in group with the following code:

dat_col <- dat_col %>%
           mutate(color = colorRampPalette(
                            brewer.pal(n = 9, name = "RdBu"))(99)[dat_col$group])

The final heatmap is as follows:

open3d()    
shade3d(mesh1smooth, col=dat_col$color, specular = "#202020", polygon_offset = 1)

enter image description here

Since I have 99 intervals, the middle interval is the 50th, (-3.53e-05,-1.34e-05]. However, it is the 51th interval, (-1.34e-05,8.47e-06], that contains the 0 point.

Following my way of color assignment (colorRampPalette(brewer.pal(n = 9, name = "RdBu"))(99)[dat_col$group]), the center color (the 50th color imputed from colorRampPalette) is given to pds belonging to the 50th interval. However, I want pds that belong to the 51th interval, the interval that harbors 0, to be assgned the center color.

I understand that in my case, my issue won't affect the appearance of heatmap too much. But I believe this is not a trivial issue and can significantly affect the heatmap when the interval that contains 0 is far from the middle interval. This could happen when the two meshes under comparison is very different. It makes more sense to me to assign center color to the interval that contains 0 rather than the one(s) that lie in the middle of all intervals.

Of course I can manually replace hex code of the 50th imputed color to the desired center color as follows:

color <- colorRampPalette(brewer.pal(n = 9, name = "RdBu"))(99)
color2 <- color
color2[50] <- "#ffffff" #assume white is the intended center color

But the above approach affected the smoothness of color gradient since the color that was originally imputed by some smooth function is replaced by some arbitrary color. But how could I assign center color to pds that lie in the interval that transgresses 0 while at the same time not affecting the smoothness of the imputed color?


Solution

  • There are a couple of things to fix to get what you want.

    First, the colours. You base the colours on this code:

    color <- colorRampPalette(brewer.pal(n = 9, name = "RdBu"))(99)
    

    You can look at the result of that calculation, and you'll see that there is no white in it. The middle color is color[50] which evaluates to "#F7F6F6", i.e. a slightly reddish light gray colour. If you look at the original RdBu palette, the middle colour was "#F7F7F7", so this change was done by colorRampPalette(). To me it looks like a minor bug in that function: it truncates the colour values instead of rounding them, so the values

    [50,] 247.00000 247.00000 247.00000
    

    convert to "#F7F6F6", i.e. red 247, green 246, blue 246. You can avoid this by choosing some other number of colours in your palette. I see "F7F7F7" as the middle colour with both 97 and 101 colours. But being off by one probably doesn't matter much, so I wouldn't worry about this.

    The second problem is your discretization of the range of the pd values. You want zero in the middle bin. If you want the bins all to be of equal size, then it needs to be symmetric: so instead of running from min(pd) to max(pd), you could use this calculation:

    limit <- max(abs(pd))
    breaks <- -limit + (0:nlevel)*2*limit/nlevel
    

    This will put zero exactly in the middle of the middle bin, but some of the bins at one end or the other might not be used. If you don't care if the bins are of equal size, you could get just as many negatives as positives by dividing them up separately. I like the above solution better.

    Edited to add: For the first problem, a better solution is to use

    color <- hcl.colors(99, "RdBu")
    

    with the new function in R 3.6.0. This does give a light gray as the middle color.