96-well visualization plot

This is a simple method to plot scalable heat-map representing 96-well plate layout. Keep in mind that heat-maps are not the best way of data visualisation and should be avoided. In some cases however (i.e. documentation) it is convenient to have data presented with the exact same layout as in the microtitter plate.

Prepare data

Typically, values will be imported from the plate reader as matrix looking somehow like that simulated one:

m <- matrix(round(abs(rnorm(96)), 2), nrow = 8, ncol = 12)
m
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
## [1,] 0.48 0.22 2.98 0.11 1.08 1.08 0.13 1.42 0.69  1.31  1.38  0.26
## [2,] 2.40 0.05 0.44 1.91 0.15 0.22 0.54 0.08 1.23  1.59  0.88  0.15
## [3,] 0.69 0.96 1.01 0.25 0.24 1.11 0.61 0.76 0.12  1.87  0.80  0.79
## [4,] 1.05 0.82 1.11 0.74 1.18 0.23 1.08 0.08 0.81  0.54  0.37  0.06
## [5,] 0.61 0.19 1.09 1.41 0.37 1.40 0.70 0.55 0.48  0.65  1.35  0.65
## [6,] 0.29 1.59 0.63 1.01 1.10 1.34 0.67 0.82 0.77  1.37  2.01  2.06
## [7,] 1.37 0.81 0.97 0.35 0.87 1.02 1.07 0.89 0.56  1.34  0.78  1.10
## [8,] 1.58 0.14 0.99 0.25 0.03 1.22 1.84 2.11 0.80  0.21  0.06  0.06

Lets convert that to data frame:

df <- as.data.frame(m)

Now, transform to long data format, add row and column numbers and overall well name. Note that rows are represent by letters in microtiter plate, but lets keep them as integers. Continous scale will help later on with scaling.

library(tidyverse)

 df <- df %>% 
  mutate(row = 1:8) %>% 
  pivot_longer(-row, names_to = "col", values_to = "value") %>%
  mutate(col = as.integer(str_remove(col, "V"))) %>%
  mutate(well = paste0(LETTERS[row], col))
 
glimpse(df)
## Rows: 96
## Columns: 4
## $ row   <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2...
## $ col   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8...
## $ value <dbl> 0.48, 0.22, 2.98, 0.11, 1.08, 1.08, 0.13, 1.42, 0.69, 1.31, 1...
## $ well  <chr> "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", ...

Plot.

Use ggforce to plot circles of 0.45 radius (means 0.9 diameter). That will give nice (scalable!) margin between wells. (the alternative would be to use geom_point, which is also OK, but will give some problems with plot resize).

library(ggforce)
p <- ggplot(data = df) + 
  geom_circle(aes(x0 = col, y0 = row, r = 0.45, fill = value))
p

coord_equal() will fix the issue of smashed wells:

p <- p + coord_equal()
p

Fix the X and Y labels, so they represent the plate layout. Use reverse_trans() from scales library to invert the Y axis (so row A is in the top). Also, keep X and Y margin at 1% (expand = expansion(mult = c(0.01, 0.01))).

library(scales)
p <- p + 
scale_x_continuous(breaks = 1:12, expand = expansion(mult = c(0.01, 0.01))) +
  scale_y_continuous(breaks = 1:8, labels = LETTERS[1:8], expand = expansion(mult = c(0.01, 0.01)), trans = reverse_trans())
p

Add labs and colors, remove legend and grids:

p <- p + 
  scale_fill_gradient(low = "white", high = "purple") + 
  labs(title = "96 Well plate", subtitle = "Beautiful violet assay", x = "Col", y = "Row") + 
  theme_bw() + 
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        legend.position = "none")
p

Use geom_text to add exact values as labels:

p <- p + geom_text(aes(x = col, y = row, label = paste0(value)), size = 3)
p