#' Displays the annotation of the wells in a plateLayout #' @param plateData a data.table object containing the information #' of the plate. This must contain a "WellID". #' @param plateName The plate name #' @param valueVariable The name of the variable in 'plateData' to #' be visualized in a plate layout. #' @param textVariable The name of the variable in 'plateData' to be #' shown in the wells of the plate layout. If NULL, the valueVariable #' is shown. #' @param colours A named character vector containing the colours #' for the different levels of the valuevariable. The names should #' correspond to the dose levels. if not specified, a scheme of blues #' will be provided. #' @param breaks Numeric vector indicating breaks for plot coloring. #' @param colourWellText Colour to display the text in the wells. #' @param layout Integer vector of length two with number of rows and #' colums in a plate, e.g. \code{c(16,24)} #' @param legend.title A title for the legend #' @param plot.title A title for the plot, will be contracted #' with the plate name #' @param ... additional arguments for \code{plateLayout.default} function #' @import data.table #' @importFrom platetools fill_plate #' @export plateLayout.annotation <- function( plateData, plateName = character(), valueVariable = "Dose", textVariable = NULL, breaks = NULL, colours = NULL, colourWellText = "black", layout = c(16, 24), legend.title = "Dose", plot.title = "Plate Annotation - ", textFontSize = 9, ... ) { WellID <- Label <- NULL if (!(all(c("WellID", "SampleName") %in% colnames(plateData)))) { stop(" 'WellID' and 'SampleName' column required in plateData object") } #Check WellID Format checkWellID <- grepl("^[[:upper:]]{1,2}[[:digit:]]{1,2}$", plateData$WellID) if(!all(checkWellID)){ stop("WellID does not have the correct format") } plateData[, WellID := paste0( sub(".*([[:alpha:]]).+", "\\1", plateData$WellID), sprintf( "%02d", as.numeric(sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID)) ) )] plateData <- platetools::fill_plate(plateData, "WellID", plate = layout[1]*layout[2]) plateData$column <- factor( sprintf( "%02d", as.numeric(sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID)) ), levels = sprintf("%02d", seq(1, layout[2])) ) plateData$row <- factor(sub(".*([[:alpha:]]).+", "\\1", plateData$WellID), levels = LETTERS[seq(1, layout[1])]) if (!is.null(valueVariable)){ plateData[, values := as.character(plateData[, ..valueVariable][[1]])] valueVar <- "values" }else{ plateData[, values := "grey"] valueVar <- "values" colours <- setNames("grey", "grey") } if (is.null(colours)) { blues <- colorRampPalette(c("#d6e0ff", "#2171B5")) greens <- colorRampPalette(c("light green", "dark green")) numLevels <- sort(as.numeric(as.character(unique(plateData[, values])[ grepl( "^[[:digit:]]+([.][[:digit:]]+)?$", trimws(unique(plateData[, values])) ) ]))) otherLevels <- sort(as.character(unique(plateData[, values])[ !grepl( "^[[:digit:]]+([.][[:digit:]]+)?$", trimws(unique(plateData[,values])) ) ])) colours <- c(blues(length(numLevels)), greens(length(otherLevels)), "red") names(colours) <- c(numLevels, otherLevels, "failed") } if (!is.null(textVariable)) { plateData[, Label := do.call(paste, c(.SD, sep = "\n ")), .SDcols = textVariable ] plateData[, Label := gsub("-", "-\n", Label)] plateData[, Label := gsub("_", "_\n", Label)] textVar <- "Label" } else { textVar <- NULL } if (is.null(breaks)){ breaks <- seq_len(length(colours)) } plateLayout( plateData = plateData, valueVariable = valueVar, textVariable = textVar, plateName = plateName, breaks = breaks, colourWellText = colourWellText, legend.title = legend.title, layout = layout, colours = colours, plot.title = plot.title, textFontSize = textFontSize, ... ) } #' Create a heatmap of values in a plateLayout view. The values can be #' library sizes, number of genes, qcScore (0/1) or a factor. #' @param plateData A data.table of the values to be visualized with #' at least the column of interest (specified in 'varOfInterest') #' and a 'WellID' column indicating the wells in the plate. The WellID #' is a combination of a letter (row in the plate) and an integer #' (column in the plate). #' @param valueVariable The name of the variable in 'plateData' #' to be visualized in a plate layout #' @param textVariable The name of the variable in 'plateData' #' to be shown in the wells of the plate layout. Defaults to the #' valueVariable and if NULL, no text will be displayed. #' @param breaks Numeric vector indicating breaks for plot coloring. #' @param colours Colours to be used for levels specified by #' the breaks. If NULL, a colour scheme of purples is shown. #' @param colourWellText Colour to display the text in the wells. #' @param layout Integer vector of length two with number of rows #' and colums in a plate, e.g. \code{c(16,24)} #' @param makeContourColours Logical, whether or not the plate #' layout will contain a contour colours for the wells based on the #' parameters in 'contourColours' and 'categories' #' @param contourVariable The variable used for the contour colouring #' @param contourColours Character vector specifying a colour for #' each range in 'categories' #' @param labelsCategories Character vector specifying the names #' (labels) for each range in 'categories' #' @param categories if contour Variable is not a factor, a numeric #' vector specifying the categories to divide the 'varOfInterest', #' including the lower and upper limits. #' @param plateName The plate name #' @param plot.title A title for the plot, will be contracted with #' the plate name #' @param legend.title A title for the legend #' @param displayHeatmap Logical, whether to display the plateLayout heatmap #' @param saveHeatmap Logical, whether to save the plateLayout heatmap #' @param outputDir The directory where the plateLayout heatmap should be saved #' @param prefix The prefix to the file name of the saved plateLayout heatmap #' @param ... additional arguments for \code{ComplexHeatmap::Heatmap} function #' @importFrom platetools fill_plate #' @importFrom RColorBrewer brewer.pal #' @importFrom ComplexHeatmap Heatmap #' @importFrom circlize colorRamp2 #' @importFrom grid grid.text grid.rect gpar legendGrob gpar #' @importFrom grDevices dev.off png #' @importFrom graphics title #' @export plateLayout <- function( plateData, valueVariable, textVariable = valueVariable, breaks = NULL, colours = NULL, colourWellText = "white", textFontSize = 6, layout = c(16, 24), makeContourColours = FALSE, contourVariable = character(), contourColours = c("red", "orange", "seagreen3"),lwdContours = c(1, 1, 1), labelsCategories = c('1', '2', '3'), categories = NULL, plateName = character(), plot.title = character(), legend.title = NULL, legendFontSize = 15, row_split = rep("A", 16), col_split = rep("A", 24), legendFontSizeTitle = 15, displayHeatmap = TRUE, saveHeatmap = FALSE, outputDir = ".", prefix = "" ) { WellID <- NULL if (!(all(c("WellID", "SampleName") %in% colnames(plateData)))) { stop(" 'WellID' and 'SampleName' column required in plateData object") } plateData[, WellID := paste0( sub(".*([[:alpha:]]).+", "\\1", plateData$WellID), sprintf( "%02d", as.numeric(sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID)) ) )] plateData <- platetools::fill_plate(plateData, "WellID", plate = 384) plateData$column <- factor( sprintf("%02d", as.numeric( sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID) )), levels = sprintf("%02d", seq(1, layout[2])) ) plateData$row <- factor(sub(".*([[:alpha:]]).+", "\\1", plateData$WellID), levels = LETTERS[seq(1, layout[1])]) plateValues <- plateLayoutFormat( plateData, varOfInterest = valueVariable, rows = layout[1], cols = layout[2] ) if (!is.null(textVariable)) { plateText <- plateLayoutFormat( plateData, varOfInterest = textVariable, rows = layout[1], cols = layout[2] ) } plot.title <- gsub( "^([a-z])", "\\U\\1", gsub("([A-Z])", " \\1", plot.title, perl = TRUE), perl = TRUE ) mainTitle <- paste0(plot.title, plateName) plateContourColours <- matrix("", nrow = layout[1], ncol = layout[2]) if (makeContourColours) { contourData <- plateData[WellType %in% c("nonEmpty", "Treated Wells"), ] if (is.numeric(contourData[, ..contourVariable][[1]])) { contourData$contours <- cut( contourData[, ..contourVariable][[1]], categories, left = TRUE, right = TRUE, labels = labelsCategories) } else { contourData$contours <- contourData[, ..contourVariable][[1]] } names(contourColours) <- labelsCategories names(lwdContours) <- labelsCategories for (i in seq_len(layout[1])) { for (j in seq_len(layout[2])) { tryCatch({ sampleHit <- which( as.character(contourData$WellID) == paste0( LETTERS[i], sprintf("%02d", j) ) ) if (length(sampleHit) == 1) { plateContourColours[i, j] <- as.character( contourData[sampleHit,'contours'][[1]] ) } }, error = function(e) { print(paste0(LETTERS[i], sprintf("%02d", j), " is missing.")) } ) } } } plateValues$contours <- plateContourColours colnames(plateValues$values) <- seq_len(ncol(plateValues$values)) if (is.null(breaks)) { breakValues <- plateValues$values breakValues[which(is.na(breakValues))] <- 0 if (all(breakValues >= 0)) { breaks <- computeBreaks(7, max(plateValues$values, na.rm = TRUE)) } else { breaks <- quantile(plateValues$values, probs = seq(0, 1, 0.125)) } } if (is.null(colours)) { colours <- tryCatch({ circlize::colorRamp2( breaks = breaks, colors = brewer.pal(length(breaks), "Purples") ) }, error = function(cond){ message("Recomputed breaks for proper colour mapping") breakValues <- plateValues$values breakValues[which(is.na(breakValues))] <- 0 if (all(breakValues >= 0)) { breaks <- computeBreaks(7, max(plateValues$values, na.rm = TRUE)) } else { breaks <- quantile(plateValues$values, probs = seq(0, 1, 0.125)) } circlize::colorRamp2( breaks = breaks, colors = brewer.pal(length(breaks), "Purples") ) }) } ht <- Heatmap( plateValues$values, column_title = mainTitle, column_title_side = "top", rect_gp = gpar(lwd = 0.4), cluster_rows = FALSE, cluster_columns = FALSE, col = colours, row_title = NULL, row_split = row_split, column_split = col_split, row_names_side = "left", cluster_row_slices = FALSE, cluster_column_slices = FALSE, show_heatmap_legend = TRUE, heatmap_legend_param = list( title = ifelse( is.null(legend.title), paste0(valueVariable, "\n"), paste0(legend.title, "\n") ), grid_height = unit(9, "mm"), border = "black", labels_gp = gpar(fontsize = legendFontSize), title_gp = gpar(fontsize = legendFontSizeTitle) ), cell_fun = function(j, i, x, y, width, height, fill) { if (is.na(plateValues$values[i, j])) { grid.rect( x, y, width, height, gp = gpar(fill = "white", alpha = 0.7, lwd = 0.7, col = "white") ) } else if (!is.null(textVariable)) { grid.text( plateText$values[i, j], x, y, just = "centre", gp = gpar(fontsize = textFontSize, col = colourWellText) ) } if (makeContourColours) { if (!is.na(plateValues$contours[i, j])) { grid.rect( x, y, width, height, gp = gpar( col = contourColours[as.character(plateValues$contours[i, j])], fill = NA, lwd = lwdContours[as.character(plateValues$contours[i, j])] ) ) } } } ) if (displayHeatmap) { print(ht) } if (saveHeatmap) { png( file.path( outputDir, paste0(prefix,gsub(" |-", "",plot.title), "_", plateName, ".png") ), width = 30, height = 10, units = "cm", res = 1200 ) print(ht) dev.off() } return(ht) } #' Return numerical matrix with number of reads that corresponds to the #' plate layout #' @param data A data.frame of the values to be visualized with at least #' the columnof interest (specified in 'varOfInterest') and a 'WellID' column #' indicating the wells in the plate. The WellID is a combination of a #' letter (row in the plate) and an integer (column in the plate). #' @param varOfInterest The name of the variable in 'data' to be visualized #' in a plate layout #' @param rows number of rows in a plate layout #' @param cols number of columns in a plate layout #' @param verbose if \code{TRUE}, samples missing from the plate #' will be reported #' @export plateLayoutFormat <- function( data, varOfInterest, rows = 16, cols = 24, verbose = FALSE ) { plateValues <- matrix(NA, nrow = rows, ncol = cols) for (i in seq_len(rows)) { for (j in seq_len(cols)) { tryCatch({ sampleHit <- which( as.character(data$WellID) == paste0(LETTERS[i], sprintf("%02d", j)) ) if(length(sampleHit) == 1){ plateValues[i, j] <- data[sampleHit, ..varOfInterest][[1]] } }, error = function(e) { if (verbose == TRUE) { print(paste0(LETTERS[i], sprintf("%02d", j), " is missing.")) } } ) } } row.names(plateValues) <- LETTERS[1:rows] return(list("values" = plateValues)) } #' Helper function to automate break selection for raw count data #' #' This function creates an exponentially increasing vector for given number #' breaks between zero and some element of choice. It is particularly useful for #' raw counts or raw counts per million. #' #' @param nBreaks Number of breaks to be generated #' @param maxElement Maximum value of data entries #' @export computeBreaks <- function(nBreaks, variable) { maxElement <- max(variable, na.rm = TRUE) if (length(unique(variable)) == 1) { breaks <- c(0, 0.5, ifelse(maxElement < 1, 1, maxElement)) } else { coefSystem <- solve( rbind(c(1, 1), c(1, (nBreaks - 1)))) %*% c(0, log(maxElement) ) coefExp <- c(exp(coefSystem[1]), coefSystem[2]) breaks <- coefExp[1] * exp((1:(nBreaks - 1)) * coefExp[2]) breaks <- unique(c(0, breaks)) } return(breaks) }