R/plot_two_drugs.R

Defines functions .Pt2mm .RoundValues .Extract2DrugPlotData Plot2DrugSurface Plot2DrugContour Plot2DrugHeatmap

Documented in .Extract2DrugPlotData Plot2DrugContour Plot2DrugHeatmap Plot2DrugSurface .Pt2mm .RoundValues

# Copyright Shuyu Zheng and Jing Tang - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Shuyu Zheng <shuyu.zheng@helsinki.fi>, March 2021
#
# SynergyFinder
#
# Functions in this page:
# Plot2DrugHeatmap: Heatmap Plot for 2-drug Combination Dose-Response/Synergy
#                   Scores
# Plot2DrugSurface: 3D surface Plot for 2-drug Combination Dose-Response/Synergy
#                   Scores
# Auxiliary functions:
# .Extract2DrugPlotData: Extract Data for 2 Drug combination plots
# .RoundValues: Round the Numbers for Plotting
# .Pt2mm: Round the Numbers for Plotting
# .ExtendedScores: Convert Font Size from pt to mm

#' Heatmap Plot for 2-drug Combination Dose-Response/Synergy Scores
#' 
#' This function will generate a plot for 2-drug combinations. The axes are the
#' dosage at which drugs are tested. The values could be observed response,
#' synergy scores or the reference effects calculated from different models.
#'
#' @param data A list object generated by function \code{\link{ReshapeData}}.
#' @param plot_block A character/integer. It indicates the block ID for the
#'   block to visualize.
#' @param drugs A vector of characters or integers with length of 2. It contains
#'   the index for two drugs to plot. For example, \code{c(1, 2)} indicates to
#'   plot "drug1" and "drug2" in the input \code{data}.
#' @param plot_value A character value. It indicates the value to be visualized.
#'   If the \code{data} is the direct output from \link{ReshapeData}, the values
#'   for this parameter are:
#'   \itemize{
#'     \item \strong{response_origin} The original response value in input data.
#'     It might be \% inhibition or \% viability.
#'     \item \strong{response} The \% inhibition after preprocess by function 
#'     \link{ReshapeData}
#'   }
#'   If the \code{data} is the output from \link{CalculateSynergy}, following
#'   values are also available:
#'   \itemize{
#'     \item \strong{ZIP_ref, Bliss_ref, HSA_ref, Loewe_ref} The reference
#'     additive effects predicted by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_synergy, Bliss_synergy, HSA_synergy, Loewe_synergy}
#'     The synergy score calculated by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_fit} The response fitted by ZIP model.
#'   }
#' @param statistic A character or NULL. It indicates the statistics printed
#'   in the plot while there are replicates in input data. Available values are:
#'   \itemize{
#'     \item \strong{sem} Standard error of mean;
#'     \item \strong{ci} 95\% confidence interval.
#'   }
#'   If it is \code{NULL}, no statistics will be printed.
#' @param summary_statistic A vector of characters or NULL. It indicates the
#'   summary statistics for all the \code{plot_value} in whole combination
#'   matrix. Available values are:
#'   \itemize{
#'     \item \strong{mean} Median value for all the responses or synergy
#'     scores in the matrix;
#'     \item \strong{median} Median value for all the responses or synergy
#'     scores in the matrix;
#'     \item \strong{quantile_90} 90\% quantile. User could change the number to
#'     print different sample quantile. For example quantile_50 equal to median. 
#'   }
#'   If it is \code{NULL}, no statistics will be printed.
#' @param plot_title A character value. It specifies the plot title. If it is
#'   \code{NULL}, the function will automatically generate a title.
#' @param dynamic A logical value. If it is \code{TRUE}, this function will
#'   use \link[plotly]{plot_ly} to generate an interactive plot. If it is
#'   \code{FALSE}, this function will use \link[lattice]{wireframe} to generate
#'   a static plot.
#' @param col_range A vector of two integers. They specify the starting and 
#'   ending concentration of the drug on x-axis. Use e.g., c(1, 3) to specify
#'   that only from 1st to 3rd concentrations of the drug on x-axis are used. By
#'   default, it is NULl so all the concentrations are used.
#' @param row_range A vector of two integers. They specify the starting and
#'   ending concentration of the drug on y-axis. Use e.g., c(1, 3) to specify
#'   that only from 1st to 3rd concentrations of the drug on y-axis are used. By
#'   default, it is NULl so all the concentrations are used.
#' @param high_value_color An R color value. It indicates the color for the
#'   high values.
#' @param low_value_color An R color value. It indicates the color for low
#'   values.
#' @param text_label_size_scale A numeric value. It is used to control the size
#'   text labels on the heatmap. The text size will multiply by this scale
#'   factor.
#' @param text_label_color NULL or an R color value. It indicates the color for
#'   text labels on the heatmap. If it is \code{NULL}, no text will be shown.
#' @param title_text_size_scale A numeric value. It is used to control the size
#'   of legend title, legend text, plot title, axis title, axis tick, subtitle.
#'   All the text size will multiply by this scale factor.
#'   
#' @return A ggplot plot object.
#'
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
#' @export
#'
#' @examples
#' data("mathews_screening_data")
#' data <- ReshapeData(mathews_screening_data)
#' Plot2DrugHeatmap(data)
Plot2DrugHeatmap <- function(data,
                             plot_block = 1,
                             drugs = c(1, 2),
                             plot_value = "response",
                             statistic = NULL,
                             summary_statistic = NULL,
                             dynamic = FALSE,
                             plot_title = NULL,
                             col_range = NULL,
                             row_range = NULL,
                             high_value_color = "#A90217",
                             low_value_color = "#2166AC",
                             text_label_size_scale = 1,
                             text_label_color = "#000000",
                             title_text_size_scale = 1) {
  # Extract plot data
  plot_data <- .Extract2DrugPlotData(
    data = data,
    plot_block = plot_block,
    drugs = drugs,
    plot_value = plot_value,
    statistic = statistic
  )
  plot_table <- plot_data$plot_table
  drug_pair <- plot_data$drug_pair
  
  # Subset plot matrix
  if (!is.null(row_range)) {
    selected_rows <- levels(plot_table$conc2)[row_range[1]:row_range[2]]
    plot_table <- plot_table[plot_table$conc2 %in% selected_rows, ]
    plot_table$conc2 <- factor(plot_table$conc2)
  }
  
  if (!is.null(col_range)) {
    selected_cols <- levels(plot_table$conc1)[col_range[1]:col_range[2]]
    plot_table <- plot_table[plot_table$conc1 %in% selected_cols, ]
    plot_table$conc1 <- factor(plot_table$conc1)
  }
  
  # Generate plot title and legend title
  if (plot_value == "response") {
    if (is.null(plot_title)){
      plot_title <- paste(
        "Dose Response Matrix",
        sep = " "
      )
    }
    legend_title <- "Inhibition (%)"
  } else if (plot_value == "response_origin") {
    if (is.null(plot_title)){
      plot_title <- paste(
        "Dose Response Matrix",
        sep = " "
      )
    }
    legend_title <- paste(stringr::str_to_title(drug_pair$input_type), "%")
  } else {
    if (is.null(plot_title)){
      plot_title <- switch(
        sub(".*_", "", plot_value),
        "ref" = sub("_ref", " Reference Additive Effect", plot_value),
        "fit" = sub("_fit", " Fitted Effect", plot_value),
        "synergy" = sub("_synergy", " Synergy Score", plot_value)
      )
    }
    legend_title <- switch(
      sub(".*_", "", plot_value),
      "ref" = "Inhibition (%)",
      "fit" = "Inhibition (%)",
      "synergy" = "Synergy Score"
    )
  }
  # plot subtitle (summary statistics)
  plot_subtitle <- c()
  if (!is.null(summary_statistic)) {
    concs <- plot_table[, grepl("conc\\d+", colnames(plot_table))]
    if (endsWith(plot_value, "_synergy")) {
      concs_zero <- apply(concs, 2, function(x){x == 0})
      index <- rowSums(concs_zero) < 1
      summary_value_table <- plot_table[index, ]
    } else {
      summary_value_table <- plot_table
    }
    avail_value <- grepl("mean|median|quantile_\\d+", summary_statistic)
    if ("mean" %in% summary_statistic) {
      value <- .RoundValues(mean(summary_value_table$value))
      if (length(concs == 2) & drug_pair$replicate) {
        p_value <- data$drug_pairs[data$drug_pairs$block_id == plot_block,
                                   paste0(plot_value, "_p_value")]
        if (p_value != "< 2e-324") {
          p_value <- paste0("= ", p_value)
        }
        plot_subtitle <- c(
          plot_subtitle, 
          paste0(
            "Mean: ",
            value,
            " (p ",
            p_value,
            ")"
          )
        )
      } else {
        plot_subtitle <- c(plot_subtitle, paste0("Mean: ", value))
      }
    }
    if ("median" %in% summary_statistic) {
      value <- .RoundValues(stats::median(summary_value_table$value))
      plot_subtitle <-  c(plot_subtitle, paste0("Median: ", value))
    }
    qua <- grep("quantile_\\d+", summary_statistic, value = TRUE)
    if (length(qua) > 0) {
      for (q in qua) {
        pro <- as.numeric(sub("quantile_", "", q))
        value <- .RoundValues(stats::quantile(summary_value_table$value, 
                                              probs = pro / 100))
        plot_subtitle <-  c(plot_subtitle, paste0(pro, "% Quantile: ", value))
      }
    }
  }
  plot_subtitle <- paste(plot_subtitle, collapse = " | ")
  
  # Color palette
  color_range <- round(max(abs(plot_table$value)), -1) + 10
  start_point <- -color_range
  end_point <- color_range
  
  # plot heatmap for dose-response matrix
  if (dynamic){
    conc1 <- unique(plot_table$conc1)
    conc2 <- unique(plot_table$conc2)
    mat <- reshape2::acast(plot_table, conc1~conc2, value.var = "value")
    colnames(mat) <- seq(1, ncol(mat))
    rownames(mat) <- seq(1, nrow(mat))
    x_ticks_text <- as.character(sort(conc1))
    y_ticks_text <- as.character(sort(conc2))
    x <- data.frame(x = seq(1, length(conc1)),
                    ticks = as.character(sort(conc1)))
    y <- data.frame(y = seq(1, length(conc2)),
                    ticks = as.character(sort(conc2)))
    plot_table <- plot_table %>% 
      dplyr::left_join(x, by = c("conc1" = "ticks")) %>% 
      dplyr::left_join(y, by = c("conc2" = "ticks")) 
    x_axis_title <- paste0(drug_pair$drug1, " (", drug_pair$conc_unit1, ")")
    y_axis_title <- paste0(drug_pair$drug2, " (", drug_pair$conc_unit2, ")")
    
    # Hover text
    concs <- expand.grid(conc1, conc2)
    hover_text <- NULL
    for (i in 1:nrow(concs)) {
      hover_text <- c(
        hover_text,
        paste0(
          drug_pair[, c("drug1", "drug2")],
          ": ",
          sapply(concs[i, ], as.character),
          " ",
          drug_pair[, c("conc_unit1", "conc_unit1")],
          "<br>",
          collapse = ""
        )
      )
    }
    hover_text <- paste0(
      hover_text,
      "Value: ",
      .RoundValues(plot_table$value),
      sep = ""
    )
    hover_text <- matrix(hover_text, nrow = length(conc1))
    
    p <- plotly::plot_ly(
      x = ~plot_table$x,
      y = ~plot_table$y,
      z = ~plot_table$value,
      zmin = start_point,
      zmax = end_point,
      type = "heatmap",
      # text = hover_text,
      hoverinfo = "",
      autocolorscale = FALSE,
      colorscale = list(
        c(0, low_value_color),
        c(0.5, "white"),
        c(1, high_value_color)
      ),
      colorbar = list(
        x = 1,
        y = 0.75,
        align = "center",
        outlinecolor = "#FFFFFF",
        tickcolor = "#FFFFFF",
        title = legend_title,
        titlefont = list(size = 12 * title_text_size_scale, family = "arial"),
        tickfont = list(size = 12 * title_text_size_scale, family = "arial")
      )) %>% 
      plotly::layout(
        title = list(
          text = paste0("<b>", plot_title, "</b>"),
          font = list(size = 18 * title_text_size_scale, family = "arial"),
          y = 0.99
        ),
        xaxis = list(
          title = paste0("<i>", x_axis_title, "</i>"),
          tickfont = list(size = 12 * title_text_size_scale, family = "arial"),
          titlefont = list(size = 12 * title_text_size_scale, family = "arial"),
          ticks = "none",
          showspikes = FALSE,
          showgrid = FALSE,
          tickmode = "array", 
          tickvals = x$x,
          ticktext = x$ticks
        ),
        yaxis = list(
          title = paste0("<i>", y_axis_title, "</i>"),
          tickfont = list(size = 12 * title_text_size_scale, family = "arial"),
          titlefont = list(size = 12 * title_text_size_scale, family = "arial"),
          ticks = "none",
          showspikes = FALSE,
          showgrid = FALSE,
          tickmode = "array", 
          tickvals = y$y,
          ticktext = y$ticks
        ),
        margin = list(
          l = 50,
          r = 50,
          b = 50,
          t = 60,
          pad = 4
        )
      ) %>%
      plotly::add_annotations(
        text = plot_subtitle,
        x = 0.5,
        y = 1.1,
        yref = "paper",
        xref = "paper",
        xanchor = "middle",
        yanchor = "top",
        showarrow = FALSE,
        font = list(size = 15 * title_text_size_scale, family = "arial")
      )
      
    if (!is.null(text_label_color)) {
      p <- p %>% 
        plotly::add_annotations(
          x = ~plot_table$x,
          y = ~plot_table$y,
          text = ~plot_table$text,
          showarrow = FALSE,
          font = list(
            color = text_label_color,
            size = 12 * text_label_size_scale
          ),
          ax = 20,
          ay = -20
        ) 
    }
    p <- p %>%
      plotly::config(
        toImageButtonOptions = list(
          format = "svg",
          filename = plot_title,
          width = 500,
          height = 500,
          scale = 1
        )
      ) 
  } else{
    p <- ggplot2::ggplot(
      data = plot_table,
      aes(x = conc1, y = conc2, fill = value)
    ) +
      ggplot2::geom_tile() +
      ggplot2::geom_text(
        ggplot2::aes(label = text),
        size = .Pt2mm(7) * text_label_size_scale
      ) +
      ggplot2::scale_fill_gradient2(
        high= high_value_color,
        mid = "#FFFFFF",
        low = low_value_color,
        midpoint = 0,
        name = legend_title,
        limits = c(start_point, end_point)
      ) +
      ggplot2::guides(
        fill = ggplot2::guide_colorbar(
          barheight = 10,
          barwidth = 1.5,
          ticks = FALSE
        )
      ) +
      ggplot2::xlab(paste0(drug_pair$drug1, " (", drug_pair$conc_unit2, ")")) +
      ggplot2::ylab(paste0(drug_pair$drug2, " (", drug_pair$conc_unit1, ")")) +
      # Add the title for heatmap
      ggplot2::ggtitle(
        label = paste0(
          plot_title
        ),
        subtitle = plot_subtitle
      ) +
      ggplot2::theme(
        plot.title = ggplot2::element_text(
          size = 13.5 * title_text_size_scale,
          face = "bold",
          hjust = 0.5
        ),
        plot.subtitle = ggplot2::element_text(
          size = 12 * title_text_size_scale,
          hjust = 0.5
        ),
        panel.background = ggplot2::element_blank(),
        # Set label's style of heatmap
        axis.text = ggplot2::element_text(
          size = 10 * title_text_size_scale
        ),
        axis.title = ggplot2::element_text(
          face = "italic",
          size = 10 * title_text_size_scale
        ),
        legend.title = ggplot2::element_text(
          size = 10 * title_text_size_scale
        ),
        legend.text = ggplot2::element_text(
          size = 10 * title_text_size_scale
        ),
        legend.background = element_rect(color = NA)
      )
    }
  return(p)
}

#' 2D Contour Plot for 2-drug Combination Dose-Response/Synergy Scores
#' 
#' This function will generate a contour level plot for 2-drug combinations.
#' The axes are the dosage for each drug. The values could be observed response,
#' synergy scores or the reference effects calculated from different models.
#'
#' @param data A list object generated by function \code{\link{ReshapeData}}.
#' @param plot_block A character/integer. It indicates the block ID for the
#'   block to visualize.
#' @param drugs A vector of characters or integers with length of 2. It contains
#'   the index for two drugs to plot. For example, \code{c(1, 2)} indicates to
#'   plot "drug1" and "drug2" in the input \code{data}.
#' @param plot_value A character value. It indicates the score or response value
#'   to be visualized. If the \code{data} is the direct output from
#'   \link{ReshapeData}, the available values for this parameter are:
#'   \itemize{
#'     \item \strong{response_origin} The original response value in input data.
#'     It might be \% inhibition or \% viability.
#'     \item \strong{response} The \% inhibition after preprocess by function 
#'     \link{ReshapeData}
#'   }
#'   If the \code{data} is the output from \link{CalculateSynergy}, following
#'   values are also available:
#'   \itemize{
#'     \item \strong{ZIP_ref, Bliss_ref, HSA_ref, Loewe_ref} The reference
#'     additive effects predicted by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_synergy, Bliss_synergy, HSA_synergy, Loewe_synergy}
#'     The synergy score calculated by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_fit} The response fitted by ZIP model.
#'   }
#' @param summary_statistic A vector of characters or NULL. It indicates the
#'   summary statistics for all the \code{plot_value} in whole combination
#'   matrix. Available values are:
#'   \itemize{
#'     \item \strong{mean} Median value for all the responses or synergy
#'     scores in the matrix;
#'     \item \strong{median} Median value for all the responses or synergy
#'     scores in the matrix;
#'     \item \strong{quantile_90} 90\% quantile. User could change the number to
#'     print different sample quantile. For example quantile_50 equal to median. 
#'   }
#'   If it is \code{NULL}, no statistics will be printed.
#' @param plot_title A character value. It specifies the plot title. If it is
#'   \code{NULL}, the function will automatically generate a title.
#' @param interpolate_len An integer. It specifies how many values need to be
#'    interpolated between two concentrations. It is used to control the 
#'    smoothness of the synergy surface.
#' @param col_range A vector of two integers. They specify the starting and 
#'   ending concentration of the drug on x-axis. Use e.g., c(1, 3) to specify
#'   that only from 1st to 3rd concentrations of the drug on x-axis are used. By
#'   default, it is NULl so all the concentrations are used.
#' @param row_range A vector of two integers. They specify the starting and
#'   ending concentration of the drug on y-axis. Use e.g., c(1, 3) to specify
#'   that only from 1st to 3rd concentrations of the drug on y-axis are used. By
#'   default, it is NULl so all the concentrations are used.
#' @param dynamic A logical value. If it is \code{TRUE}, this function will
#'   use \link[plotly]{plot_ly} to generate an interactive plot. If it is
#'   \code{FALSE}, this function will use \link[lattice]{wireframe} to generate
#'   a static plot.
#' @param high_value_color An R color value. It indicates the color for the
#'   high values.
#' @param low_value_color An R color value. It indicates the color for low
#'   values.
#' @param text_size_scale A numeric value. It is used to control the size
#'   of text in the plot. All the text size will multiply by this scale factor.
#'   
#' @return If \code{dynamic = FALSE}, this function will return a plot project 
#'   recorded by \link[grDevices]{recordPlot}. If \code{dynamic = FALSE}, this
#'   function will return a plotly plot object.
#'
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
#' @export
#'
#' @examples
#' data("mathews_screening_data")
#' data <- ReshapeData(mathews_screening_data)
#' Plot2DrugContour(data)
Plot2DrugContour <- function(data,
                             plot_block = 1,
                             drugs = c(1, 2),
                             plot_value = "response",
                             interpolate_len = 2,
                             summary_statistic = NULL,
                             dynamic = FALSE,
                             plot_title = NULL,
                             col_range = NULL,
                             row_range = NULL,
                             high_value_color = "#A90217",
                             low_value_color = "#2166AC",
                             text_size_scale = 1) {
  # Extract plot data
  plot_data <- .Extract2DrugPlotData(
    data = data,
    plot_block = plot_block,
    drugs = drugs,
    plot_value = plot_value,
    statistic = NULL
  )
  plot_table <- plot_data$plot_table
  drug_pair <- plot_data$drug_pair
  
  # Subset plot matrix
  if (!is.null(row_range)) {
    selected_rows <- levels(plot_table$conc2)[row_range[1]:row_range[2]]
    plot_table <- plot_table[plot_table$conc2 %in% selected_rows, ]
    plot_table$conc2 <- factor(plot_table$conc2)
  }
  
  if (!is.null(col_range)) {
    selected_cols <- levels(plot_table$conc1)[col_range[1]:col_range[2]]
    plot_table <- plot_table[plot_table$conc1 %in% selected_cols, ]
    plot_table$conc1 <- factor(plot_table$conc1)
  }
  # Generate plot title and legend title
  if (plot_value == "response") {
    if (is.null(plot_title)){
      plot_title <- paste(
        "Dose Response Matrix",
        sep = " "
      )
    }
    legend_title <- "Inhibition (%)"
    z_axis_title <- "Response (% inhibition)"
  } else if (plot_value == "response_origin") {
    if (is.null(plot_title)){
      plot_title <- paste(
        "Dose Response Matrix",
        sep = " "
      )
    }
    legend_title <- paste0(stringr::str_to_title(drug_pair$input_type), " (%)")
    z_axis_title <- paste0("Response (% ",drug_pair$input_type,")")
  } else {
    if (is.null(plot_title)){
      plot_title <- switch(
        sub(".*_", "", plot_value),
        "ref" = sub("_ref", " Reference Additive Effect", plot_value),
        "fit" = sub("_fit", " Fitted Effect", plot_value),
        "synergy" = sub("_synergy", " Synergy Score", plot_value)
      )
    }
    legend_title <- switch(
      sub(".*_", "", plot_value),
      "ref" = "Inhibition (%)",
      "fit" = "Inhibition (%)",
      "synergy" = "Synergy Score"
    )
    z_axis_title <- switch(
      sub(".*_", "", plot_value),
      "ref" = "Response (% inhibition)",
      "fit" = "Response (% inhibition)",
      "synergy" = "Synergy Score"
    )
  }
  # plot subtitle (summary statistics)
  plot_subtitle <- c()
  if (!is.null(summary_statistic)) {
    concs <- plot_table[, grepl("conc\\d+", colnames(plot_table))]
    if (endsWith(plot_value, "_synergy")) {
      concs_zero <- apply(concs, 2, function(x){x == 0})
      index <- rowSums(concs_zero) < 1
      summary_value_table <- plot_table[index, ]
    } else {
      summary_value_table <- plot_table
    }
    avail_value <- grepl("mean|median|quantile_\\d+", summary_statistic)
    if ("mean" %in% summary_statistic) {
      value <- .RoundValues(mean(summary_value_table$value))
      if (length(concs == 2) & drug_pair$replicate) {
        p_value <- data$drug_pairs[data$drug_pairs$block_id == plot_block,
                                   paste0(plot_value, "_p_value")]
        if (p_value != "< 2e-324") {
          p_value <- paste0("= ", p_value)
        }
        plot_subtitle <- c(
          plot_subtitle, 
          paste0(
            "Mean: ",
            value,
            " (p ",
            p_value,
            ")"
          )
        )
      } else {
        plot_subtitle <- c(plot_subtitle, paste0("Mean: ", value))
      }
    }
    if ("median" %in% summary_statistic) {
      value <- .RoundValues(stats::median(summary_value_table$value))
      plot_subtitle <-  c(plot_subtitle, paste0("Median: ", value))
    }
    qua <- grep("quantile_\\d+", summary_statistic, value = TRUE)
    if (length(qua) > 0) {
      for (q in qua) {
        pro <- as.numeric(sub("quantile_", "", q))
        value <- .RoundValues(stats::quantile(summary_value_table$value, 
                                              probs = pro / 100))
        plot_subtitle <-  c(plot_subtitle, paste0(pro, "% Quantile: ", value))
      }
    }
  }
  plot_subtitle <- paste(plot_subtitle, collapse = " | ")
  
  conc1 <- unique(plot_table$conc1)
  conc2 <- unique(plot_table$conc2)
  
  # Interpolation by kriging
  mat <- reshape2::acast(plot_table, conc1~conc2, value.var = "value")
  extended_mat <- .ExtendedScores(mat, len = interpolate_len)
  colnames(extended_mat) <- seq(1, ncol(extended_mat))
  rownames(extended_mat) <- seq(1, nrow(extended_mat))
  x_ticks <- seq(1, nrow(extended_mat), by = interpolate_len + 1)
  y_ticks <- seq(1, ncol(extended_mat), by = interpolate_len + 1)
  x_ticks_text <- as.character(sort(conc1))
  y_ticks_text <- as.character(sort(conc2))
  x_axis_title <- paste0(drug_pair$drug1, " (", drug_pair$conc_unit1, ")")
  y_axis_title <- paste0(drug_pair$drug2, " (", drug_pair$conc_unit2, ")")

  if (dynamic){
    y <- seq(1, ncol(extended_mat))
    x <- seq(1, nrow(extended_mat))
    # Color palette
    color_range <- round(max(abs(extended_mat)), -1) + 10
    start_point <- -color_range
    end_point <- color_range
    # Hover text
    concs <- expand.grid(conc1, conc2)
    hover_text <- NULL
    for (i in 1:nrow(concs)) {
      hover_text <- c(
        hover_text,
        paste0(
          drug_pair[, c("drug1", "drug2")],
          ": ",
          sapply(concs[i, ], as.character),
          " ",
          drug_pair[, c("conc_unit1", "conc_unit1")],
          "<br>",
          collapse = ""
        )
      )
    }
    hover_text <- paste0(
      hover_text,
      "Value: ",
      .RoundValues(plot_table$value),
      sep = ""
    )
    hover_text <- matrix(hover_text, nrow = length(conc1))
    hover_text_mat <- matrix(rep(NA, length(extended_mat)),
                             nrow = nrow(extended_mat))
    for (i in 1:length(x_ticks)) {
      for (j in 1:length(y_ticks)) {
        hover_text_mat[x_ticks[i], y_ticks[j]] <- hover_text[i, j]
      }
    }
    p <- plotly::plot_ly(
      x = x,
      y = y,
      z = t(extended_mat),
      zmin = start_point,
      zmax = end_point,
      type = "contour",
      line = list(width = 0),
      text = t(hover_text_mat),
      hoverinfo = "text",
      autocolorscale = FALSE,
      colorscale = list(
        c(0, low_value_color),
        c(0.5, "white"),
        c(1, high_value_color)
      ),
      colorbar = list(
        x = 1,
        y = 0.75,
        align = "center",
        outlinecolor = "#FFFFFF",
        tickcolor = "#FFFFFF",
        title = legend_title,
        titlefont = list(size = 12 * text_size_scale, family = "arial"),
        tickfont = list(size = 12 * text_size_scale, family = "arial")
      ),
      contours = list(
        x = list(
          # highlight = FALSE,
          show = TRUE,
          color = 'black',
          width = 1,
          start = 1,
          end = max(x),
          size = 1 * text_size_scale
        ),
        y = list(
          # highlight = FALSE,
          show = TRUE,
          color = 'black',
          width = 1,
          start = 1,
          end = max(y),
          size = 1 * text_size_scale
        )
      )) %>% 
      plotly::add_annotations(
        text = plot_subtitle,
        x = 0.5,
        y = 1.1,
        yref = "paper",
        xref = "paper",
        xanchor = "middle",
        yanchor = "top",
        showarrow = FALSE,
        font = list(size = 15 * text_size_scale)
      ) %>% 
      plotly::layout(
        title = list(
          text = paste0("<b>", plot_title, "</b>"),
          font = list(size = 18 * text_size_scale, family = "arial"),
          y = 0.99
        ),
        xaxis = list(
          title = paste0("<i>", x_axis_title, "</i>"),
          tickfont = list(size = 12 * text_size_scale, family = "arial"),
          titlefont = list(size = 12 * text_size_scale, family = "arial"),
          ticks = "none",
          showspikes = FALSE,
          showgrid = FALSE,
          tickmode = "array", 
          tickvals = x_ticks,
          ticktext = x_ticks_text
        ),
        yaxis = list(
          title = paste0("<i>", y_axis_title, "</i>"),
          tickfont = list(size = 12 * text_size_scale, family = "arial"),
          titlefont = list(size = 12 * text_size_scale, family = "arial"),
          ticks = "none",
          showspikes = FALSE,
          showgrid = FALSE,
          tickmode = "array", 
          tickvals = y_ticks,
          ticktext = y_ticks_text
        ),
        margin = list(
          l = 50,
          r = 50,
          b = 50,
          t = 60,
          pad = 4
        )
      ) %>% 
      plotly::config(
        toImageButtonOptions = list(
          format = "svg",
          filename = plot_title,
          width = 500,
          height = 500,
          scale = 1
        )
      ) 
  } else{
    extended_df <- reshape2::melt(extended_mat)
    colnames(extended_df) <- c("x", "y", "value")
    
    # Color palette
    color_range <- round(max(abs(extended_mat)), -1) + 10
    start_point <- -color_range
    end_point <- color_range
    p <- ggplot2::ggplot(extended_df) +
      metR::geom_contour_fill(
        ggplot2::aes(x = x, y = y, z = value)
      ) + 
      ggplot2::scale_x_continuous(
        breaks = x_ticks,
        labels = x_ticks_text
      ) +
      ggplot2::scale_y_continuous(
        breaks = y_ticks,
        labels = y_ticks_text
      ) +
      ggplot2::scale_fill_gradient2(
        high= high_value_color,
        mid = "#FFFFFF",
        low = low_value_color,
        midpoint = 0,
        name = legend_title,
        limits = c(start_point, end_point),
      ) +
      ggplot2::guides(
        fill = ggplot2::guide_colorbar(
          barheight = 10,
          barwidth = 1.5,
          ticks = FALSE
        )
      ) +
      ggplot2::xlab(x_axis_title) +
      ggplot2::ylab(y_axis_title) +
      ggplot2::ggtitle(
        label = paste0(
          plot_title
        ),
        subtitle = plot_subtitle
      ) +
      ggplot2::theme(
        plot.title = ggplot2::element_text(
          size = 13.5 * text_size_scale,
          face = "bold",
          hjust = 0.5
        ),
        plot.subtitle = ggplot2::element_text(
          size = 12 * text_size_scale,
          hjust = 0.5
        ),
        panel.background = ggplot2::element_blank(),
        # Set label's style of heatmap
        axis.text = ggplot2::element_text(
          size = 10 * text_size_scale
        ),
        axis.title = ggplot2::element_text(
          face = "italic",
          size = 10 * text_size_scale
        ),
        legend.title = ggplot2::element_text(
          size = 10 * text_size_scale
        ),
        legend.text = ggplot2::element_text(
          size = 10 * text_size_scale
        ),
        legend.background = element_rect(color = NA)
      )
  }
  p
  return(p)
}

#' 3D Surface Plot for 2-drug Combination Dose-Response/Synergy Scores
#' 
#' This function will generate a surface plot for 2-drug combinations. The axes
#' are the dosage for each drug. The values could be observed response,
#' synergy scores or the reference effects calculated from different models.
#'
#' @param data A list object generated by function \code{\link{ReshapeData}}.
#' @param plot_block A character/integer. It indicates the block ID for the
#'   block to visualize.
#' @param drugs A vector of characters or integers with length of 2. It contains
#'   the index for two drugs to plot. For example, \code{c(1, 2)} indicates to
#'   plot "drug1" and "drug2" in the input \code{data}.
#' @param plot_value A character value. It indicates the score or response value
#'   to be visualized. If the \code{data} is the direct output from
#'   \link{ReshapeData}, the available values for this parameter are:
#'   \itemize{
#'     \item \strong{response_origin} The original response value in input data.
#'     It might be \% inhibition or \% viability.
#'     \item \strong{response} The \% inhibition after preprocess by function 
#'     \link{ReshapeData}
#'   }
#'   If the \code{data} is the output from \link{CalculateSynergy}, following
#'   values are also available:
#'   \itemize{
#'     \item \strong{ZIP_ref, Bliss_ref, HSA_ref, Loewe_ref} The reference
#'     additive effects predicted by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_synergy, Bliss_synergy, HSA_synergy, Loewe_synergy}
#'     The synergy score calculated by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_fit} The response fitted by ZIP model.
#'   }
#' @param summary_statistic A vector of characters or NULL. It indicates the
#'   summary statistics for all the \code{plot_value} in whole combination
#'   matrix. Available values are:
#'   \itemize{
#'     \item \strong{mean} Median value for all the responses or synergy
#'     scores in the matrix;
#'     \item \strong{median} Median value for all the responses or synergy
#'     scores in the matrix;
#'     \item \strong{quantile_90} 90\% quantile. User could change the number to
#'     print different sample quantile. For example quantile_50 equal to median. 
#'   }
#'   If it is \code{NULL}, no statistics will be printed.
#' @param plot_title A character value. It specifies the plot title. If it is
#'   \code{NULL}, the function will automatically generate a title.
#' @param interpolate_len An integer. It specifies how many values need to be
#'   interpolated between two concentrations. It is used to control the 
#'   smoothness of the synergy surface.
#' @param col_range A vector of two integers. They specify the starting and 
#'   ending concentration of the drug on x-axis. Use e.g., c(1, 3) to specify
#'   that only from 1st to 3rd concentrations of the drug on x-axis are used. By
#'   default, it is NULl so all the concentrations are used.
#' @param row_range A vector of two integers. They specify the starting and
#'   ending concentration of the drug on y-axis. Use e.g., c(1, 3) to specify
#'   that only from 1st to 3rd concentrations of the drug on y-axis are used. By
#'   default, it is NULl so all the concentrations are used.
#' @param dynamic A logical value. If it is \code{TRUE}, this function will
#'   use \link[plotly]{plot_ly} to generate an interactive plot. If it is
#'   \code{FALSE}, this function will use \link[lattice]{wireframe} to generate
#'   a static plot.
#' @param grid A logical value. It indicates whether to add grids on the 
#'   surface.
#' @param high_value_color An R color value. It indicates the color for the
#'   high values.
#' @param low_value_color An R color value. It indicates the color for low
#'   values.
#' @param text_size_scale A numeric value. It is used to control the size
#'   of text in the plot. All the text size will multiply by this scale factor.
#'
#' @return If \code{dynamic = FALSE}, this function will return a plot project 
#'   recorded by \link[grDevices]{recordPlot}. If \code{dynamic = FALSE}, this
#'   function will return a plotly plot object.
#'
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
#' @export
#'
#' @examples
#' data("mathews_screening_data")
#' data <- ReshapeData(mathews_screening_data)
#' Plot2DrugSurface(data)
#' Plot2DrugSurface(data, dynamic = TRUE)
Plot2DrugSurface <- function(data,
                             plot_block = 1,
                             drugs = c(1, 2),
                             plot_value = "response",
                             summary_statistic = NULL,
                             plot_title = NULL,
                             interpolate_len = 2,
                             col_range = NULL,
                             row_range = NULL,
                             dynamic = FALSE,
                             grid = TRUE,
                             high_value_color = "#A90217",
                             low_value_color = "#2166AC",
                             text_size_scale = 1) {
  # Extract plot data
  plot_data <- .Extract2DrugPlotData(
    data = data,
    plot_block = plot_block,
    drugs = drugs,
    plot_value = plot_value,
    statistic = NULL
  )
  plot_table <- plot_data$plot_table
  drug_pair <- plot_data$drug_pair
  
  # Subset plot matrix
  if (!is.null(row_range)) {
    selected_rows <- levels(plot_table$conc2)[row_range[1]:row_range[2]]
    plot_table <- plot_table[plot_table$conc2 %in% selected_rows, ]
    plot_table$conc2 <- factor(plot_table$conc2)
  }
  
  if (!is.null(col_range)) {
    selected_cols <- levels(plot_table$conc1)[col_range[1]:col_range[2]]
    plot_table <- plot_table[plot_table$conc1 %in% selected_cols, ]
    plot_table$conc1 <- factor(plot_table$conc1)
  }
  # Generate plot title and legend title
  if (plot_value == "response") {
    if (is.null(plot_title)){
      plot_title <- paste(
        "Dose Response Matrix",
        sep = " "
      )
    }
    legend_title <- "Inhibition (%)"
    z_axis_title <- "Response (% inhibition)"
  } else if (plot_value == "response_origin") {
    if (is.null(plot_title)){
      plot_title <- paste(
        "Dose Response Matrix",
        sep = " "
      )
    }
    legend_title <- paste0(stringr::str_to_title(drug_pair$input_type), " (%)")
    z_axis_title <- paste0("Response (% ",drug_pair$input_type,")")
  } else {
    if (is.null(plot_title)){
      plot_title <- switch(
        sub(".*_", "", plot_value),
        "ref" = sub("_ref", " Reference Additive Effect", plot_value),
        "fit" = sub("_fit", " Fitted Effect", plot_value),
        "synergy" = sub("_synergy", " Synergy Score", plot_value)
      )
    }
    legend_title <- switch(
      sub(".*_", "", plot_value),
      "ref" = "Inhibition (%)",
      "fit" = "Inhibition (%)",
      "synergy" = "Synergy Score"
    )
    z_axis_title <- switch(
      sub(".*_", "", plot_value),
      "ref" = "Response (% inhibition)",
      "fit" = "Response (% inhibition)",
      "synergy" = "Synergy Score"
    )
  }
  
  # plot subtitle (summary statistics)
  plot_subtitle <- c()
  if (!is.null(summary_statistic)) {
    concs <- plot_table[, grepl("conc\\d+", colnames(plot_table))]
    if (endsWith(plot_value, "_synergy")) {
      concs_zero <- apply(concs, 2, function(x){x == 0})
      index <- rowSums(concs_zero) < 1
      summary_value_table <- plot_table[index, ]
    } else {
      summary_value_table <- plot_table
    }
    avail_value <- grepl("mean|median|quantile_\\d+", summary_statistic)
    if ("mean" %in% summary_statistic) {
      value <- .RoundValues(mean(summary_value_table$value))
      if (length(concs == 2) & drug_pair$replicate) {
        p_value <- data$drug_pairs[data$drug_pairs$block_id == plot_block,
                                   paste0(plot_value, "_p_value")]
        if (p_value != "< 2e-324") {
          p_value <- paste0("= ", p_value)
        }
        plot_subtitle <- c(
          plot_subtitle, 
          paste0(
            "Mean: ",
            value,
            " (p ",
            p_value,
            ")"
          )
        )
      } else {
        plot_subtitle <- c(plot_subtitle, paste0("Mean: ", value))
      }
    }
    if ("median" %in% summary_statistic) {
      value <- .RoundValues(stats::median(summary_value_table$value))
      plot_subtitle <-  c(plot_subtitle, paste0("Median: ", value))
    }
    qua <- grep("quantile_\\d+", summary_statistic, value = TRUE)
    if (length(qua) > 0) {
      for (q in qua) {
        pro <- as.numeric(sub("quantile_", "", q))
        value <- .RoundValues(
          stats::quantile(summary_value_table$value, probs = pro / 100)
        )
        plot_subtitle <-  c(plot_subtitle, paste0(pro, "% Quantile: ", value))
      }
    }
  }
  plot_subtitle <- paste(plot_subtitle, collapse = " | ")
  
  conc1 <- unique(plot_table$conc1)
  conc2 <- unique(plot_table$conc2)
  
  # kriging with kriging from  (which )
  mat <- reshape2::acast(plot_table, conc1~conc2, value.var = "value")
  extended_mat <- .ExtendedScores(mat, len = interpolate_len)
  colnames(extended_mat) <- seq(1, ncol(extended_mat))
  rownames(extended_mat) <- seq(1, nrow(extended_mat))
  x_ticks <- seq(1, nrow(extended_mat), by = interpolate_len + 1)
  y_ticks <- seq(1, ncol(extended_mat), by = interpolate_len + 1)
  x_ticks_text <- as.character(sort(conc1))
  y_ticks_text <- as.character(sort(conc2))
  x_axis_title <- paste0(drug_pair$drug1, " (", drug_pair$conc_unit1, ")")
  y_axis_title <- paste0(drug_pair$drug2, " (", drug_pair$conc_unit2, ")")
  
  if (dynamic) {
    y <- seq(1, ncol(extended_mat))
    x <- seq(1, nrow(extended_mat))
    # Color palette
    color_range <- max(abs(extended_mat)) + 5
    start_point <- -color_range
    end_point <- color_range
    # Hover text
    concs <- expand.grid(conc1, conc2)
    hover_text <- NULL
    for (i in 1:nrow(concs)) {
      hover_text <- c(
        hover_text,
        paste0(
          drug_pair[, c("drug1", "drug2")],
          ": ",
          sapply(concs[i, ], as.character),
          " ",
          drug_pair[, c("conc_unit1", "conc_unit1")],
          "<br>",
          collapse = ""
        )
      )
    }
    hover_text <- paste0(
      hover_text,
      "Value: ",
      .RoundValues(plot_table$value),
      sep = ""
    )
    hover_text <- matrix(hover_text, nrow = length(conc1))
    hover_text_mat <- matrix(rep(NA, length(extended_mat)),
                             nrow = nrow(extended_mat))
    for (i in 1:length(x_ticks)) {
      for (j in 1:length(y_ticks)) {
        hover_text_mat[x_ticks[i], y_ticks[j]] <- hover_text[i, j]
      }
    }
    
    p <- plotly::plot_ly() %>% 
      plotly::add_surface(
        name = "surface",
        x = ~x,
        y = ~y,
        z = t(extended_mat),
        text = t(hover_text_mat),
        hoverinfo = "text",
        colorscale = list(
          c(0, low_value_color),
          c(0.5, "white"),
          c(1, high_value_color)
        ),
        cauto = FALSE,
        colorbar = list(
          x = 1,
          y = 0.75,
          align = "center",
          outlinecolor = "#FFFFFF",
          tickcolor = "#FFFFFF",
          title = legend_title,
          titlefont = list(size = 12 * text_size_scale, family = "arial"),
          tickfont = list(size = 12 * text_size_scale, family = "arial")
        ),
        cmin = start_point,
        cmax = end_point,
        contours = list(
          x = list(
            # highlight = FALSE,
            show = grid,
            color = 'black',
            width = 1,
            start = 1,
            end = max(x),
            size = 1
          ),
          y = list(
            # highlight = FALSE,
            show = grid,
            color = 'black',
            width = 1,
            start = 1,
            end = max(y),
            size = 1
          )#,
          # z = list(highlight = FALSE)
        )
      ) %>% 
      plotly::add_annotations(
        text = plot_subtitle,
        x = 0.5,
        y = 1.1,
        yref = "paper",
        xref = "paper",
        xanchor = "middle",
        yanchor = "top",
        showarrow = FALSE,
        font = list(size = 15 * text_size_scale)
      ) %>% 
      plotly::layout(
        title = list(
          text = paste0("<b>", plot_title, "</b>"),
          font = list(size = 18 * text_size_scale, family = "arial"),
          y = 0.99
        ),
        scene = list(
          aspectratio = list(x=1, y=1, z=1),
          xaxis = list(
            title = paste0("<i>", x_axis_title, "</i>"),
            tickfont = list(size = 12 * text_size_scale, family = "arial"),
            titlefont = list(size = 12 * text_size_scale, family = "arial"),
            ticks = "none",
            showspikes = FALSE,
            tickmode = "array", 
            tickvals = x_ticks,
            ticktext = x_ticks_text
          ),
          yaxis = list(
            title = paste0("<i>", y_axis_title, "</i>"),
            tickfont = list(size = 12 * text_size_scale, family = "arial"),
            titlefont = list(size = 12 * text_size_scale, family = "arial"),
            ticks = "none",
            showspikes = FALSE,
            tickmode = "array", 
            tickvals = y_ticks,
            ticktext = y_ticks_text
          ),
          zaxis = list(
            title = paste0("<i>", z_axis_title, "</i>"),
            tickfont = list(size = 12 * text_size_scale, family = "arial"),
            titlefont = list(size = 12 * text_size_scale, family = "arial"),
            ticks = "none",
            tickmode = "array",
            showspikes = FALSE
          ),
          camera = list(eye = list(x = -1.25, y = -1.25, z = 1.25))
        ),
        margin = list(
          l = 50,
          r = 50,
          b = 50,
          t = 60,
          pad = 4
        )
      ) %>% 
      plotly::config(
        toImageButtonOptions = list(
          format = "svg",
          filename = plot_title,
          width = 500,
          height = 500,
          scale = 1
        )
      ) 
  } else { # static plot
    # Color palette
    color_range <- round(max(abs(plot_table$value)) + 5, 2)
    start_point <- -color_range
    end_point <- color_range
    color_level <- round(seq(start_point, end_point, by = 2), 0)
    col1 <- grDevices::colorRampPalette(c(
      "#FFFFFF",
      low_value_color
    ))(length(which(color_level <= 0)))
    col2 <- grDevices::colorRampPalette(c(
      "#FFFFFF",
      high_value_color
    ))(length(which(color_level >= 0)))
    col <- c(rev(col1), col2[-1])
    scale_par <- list(
      arrows = FALSE,
      distance = c(0.8, 0.8, 0.8),
      col = 1,
      z = list(
        tick.number = 6,
        cex = 0.75 * text_size_scale),
      x =  list(
        at = x_ticks,
        labels = x_ticks_text,
        cex = 0.75 * text_size_scale
      ),
      y = list(
        at = y_ticks,
        labels = y_ticks_text,
        cex = 0.75 * text_size_scale
      )
    )
  
    # Let the text able to shown outside the plot panel
    clip <- lattice::trellis.par.get("clip")
    clip$panel <- "off"
    lattice::trellis.par.set("clip", clip)
    p <- lattice::wireframe(
      extended_mat,
      scales = scale_par,
      drape = TRUE,
      colorkey = list(
        space = "right",
        width = 2,
        height = 0.4,
        labels = list(cex = 0.75 * text_size_scale)
      ),
      screen = list(z = 30, x = -55),
      zlab = list(
        z_axis_title,
        axis.key.padding = 0,
        cex = 0.75 * text_size_scale,
        rot = 93
      ),
      xlab = list(
        x_axis_title,
        cex = 0.75 * text_size_scale,
        rot = 23
      ),
      ylab = list(
        y_axis_title,
        cex = 0.75 * text_size_scale,
        rot = -53
      ),
      zlim = c(min(plot_table$value) - 5, max(plot_table$value) + 5),
      col.regions = col,
      main = list(
        label = plot_title,
        fontsize = 13.5 * text_size_scale
      ),
      at = lattice::do.breaks(
        c(start_point, end_point),
        length(col)
      ),
      par.settings = list(
        axis.line = list(col = "transparent")
      ),
      zoom = 1,
      aspect = 1,
      panel = function(...) {
        lattice::panel.wireframe(...)
        # subtitle
        grid::grid.text(
          label = plot_subtitle,
          x=unit(0.55, "npc"),
          y=unit(1, "npc"),
          gp = grid::gpar(
            col = "black",
            fontsize = 10.5 * text_size_scale
          ) # pt
        )
        # legend title
        grid::grid.text(
          label = legend_title,
          x = unit(1.05, "npc"),
          y = unit(0.75, "npc"),
          gp = grid::gpar(
            col = "black",
            fontsize = 8.5 * text_size_scale
          ) # pt
        )
        },
      pretty = TRUE
    )
    print(p)
    p <- grDevices::recordPlot()
  }
  return(p)
}

# Auxiliary functions -----------------------------------------------------

#' Extract Data for 2 Drug Combination Plots
#'
#' @param data A list object generated by function \code{\link{ReshapeData}}.
#' @param plot_block A character/integer. It indicates the block ID for the
#'   block to visualize.
#' @param drugs A vector of characters or integers with length of 2. It contains
#'   the index for two drugs to plot. For example, \code{c(1, 2)} indicates to
#'   plot "drug1" and "drug2" in the input \code{data}.
#' @param plot_value A character value. It indicates the value to be visualized.
#'   If the \code{data} is the direct output from \link{ReshapeData}, the values
#'   for this parameter are:
#'   \itemize{
#'     \item \strong{response_origin} The original response value in input data.
#'     It might be \% inhibition or \% viability.
#'     \item \strong{response} The \% inhibition after preprocess by function 
#'     \link{ReshapeData}
#'   }
#'   If the \code{data} is the output from \link{CalculateSynergy}, following
#'   values are also available:
#'   \itemize{
#'     \item \strong{ZIP_ref, Bliss_ref, HSA_ref, Loewe_ref} The reference
#'     additive effects predicted by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_synergy, Bliss_synergy, HSA_synergy, Loewe_synergy}
#'     The synergy score calculated by ZIP, Bliss, HSA or Loewe model,
#'     respectively.
#'     \item \strong{ZIP_fit} The response fitted by ZIP model.
#'   }
#' @param statistic A character or NULL. It indicates the statistics printed
#'   in the plot while there are replicates in input data. Available values are:
#'   \itemize{
#'     \item \strong{sem} Standard error of mean;
#'     \item \strong{ci} 95\% confidence interval.
#'   }
#'   If it is \code{NULL}, no statistics will be printed.
#'
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
#' @return A data frame. It contains the concentrations for selected drugs, the
#'   selected values for plotting, and the text for printing on the heatmap.
.Extract2DrugPlotData <- function(data,
                                  plot_block = 1,
                                  drugs = c(1, 2),
                                  plot_value = "response",
                                  statistic = NULL){
  # 1. Check the input data
  # Data structure of 'data'
  if (!is.list(data)) {
    stop("Input data is not in list format!")
  }
  if (!all(c("drug_pairs", "response") %in% names(data))) {
    stop("Input data should contain at least tow elements: 'drug_pairs' and 
         'response'. Please prepare your data with 'ReshapeData' function.")
  }
  # Parameter 'drugs'
  if (length(drugs) != 2) {
    stop("The length of 'drugs' parameter is not 2. Please chosed exactly 2
         drugs for heatmap.")
  }
  # Parameter 'plot_value'
  avail_value <- c("response", "response_origin", "ZIP_ref", "ZIP_fit",
                   "ZIP_synergy", "HSA_ref", "HSA_synergy", "Bliss_ref",
                   "Bliss_synergy", "Loewe_ref", "Loewe_synergy")
  if (!plot_value %in% avail_value) {
    stop("The parameter 'plot_value = ", plot_value, "' is not available.",
         "Avaliable values are '", paste(avail_value, collapse = ", "), "'.")
  }
  
  # Annotation data
  drug_pair <- data$drug_pairs[data$drug_pairs$block_id == plot_block, ] %>% 
    dplyr::select(
      drug1 = paste0("drug", drugs[1]),
      drug2 = paste0("drug", drugs[2]),
      conc_unit1 = paste0("conc_unit", drugs[1]),
      conc_unit2 = paste0("conc_unit", drugs[2]),
      replicate,
      input_type
    )
  
  # Parameter 'statistic'
  if (is.null(statistic)){
    statistic_table <- drug_pair$replicate
  } else {
    avail_statistic <- c("sem", "ci")
    if (!drug_pair$replicate) {
      warning("The selected block ", plot_block,
              " doesn't have the replicate data. Statistics is not available.")
      statistic_table <- FALSE
    } else if(!statistic %in% avail_statistic) {
      warning("The parameter 'statistic = ", statistic, "' is not available.",
              "Avaliable values are ", paste(avail_statistic, sep = ", "), ".")
      statistic_table <- FALSE
    } else {
      statistic_table <- TRUE
    }
  }
  
  # 1. Extract tables for plotting
  
  # Data table
  concs <- grep("conc\\d", colnames(data$response), value = TRUE)
  selected_concs <- paste0("conc", drugs)
  
  if (statistic_table){
    if (startsWith(plot_value, "response")){
      plot_table <- data$response_statistics
    } else {
      if (!"synergy_scores" %in% names(data)){
        stop("The synergy scores are not calculated. Please run function ",
             "'CalculateSynergy' first.")
      }
      plot_table <- data$synergy_scores_statistics
    }
    plot_table <- plot_table %>% 
      dplyr::filter(block_id == plot_block) %>% 
      dplyr::ungroup()
    if (is.null(statistic)) {
      plot_table <- plot_table %>% 
        dplyr::select(
          dplyr::starts_with("conc"), 
          value = !!paste0(plot_value, "_mean")
        ) %>%
        dplyr::mutate(
          text = as.character(.RoundValues(value))
        )
    } else if (statistic == "sem") {
      plot_table <- plot_table %>% 
        dplyr::select(
          dplyr::starts_with("conc"), 
          value = !!paste0(plot_value, "_mean"),
          statistic = !!paste0(plot_value, "_sem")
        ) %>%
        dplyr::mutate(
          text = paste(
            .RoundValues(value), "\n" ,
            "\u00B1",
            .RoundValues(statistic)
          )
        )
    } else if (statistic == "ci") {
      plot_table <- plot_table %>% 
        dplyr::select(
          dplyr::starts_with("conc"), 
          value = !!paste0(plot_value, "_mean"),
          left = !!paste0(plot_value, "_ci_left"),
          right = !!paste0(plot_value, "_ci_right")
        ) %>%
        dplyr::mutate(
          text = paste0(
            .RoundValues(value), "\n",
            "[",
            .RoundValues(left),
            ", ",
            .RoundValues(right),
            "]"
          )
        )
    }
  } else {
    if (startsWith(plot_value, "response")){
      plot_table <- data$response
    } else {
      if (!"synergy_scores" %in% names(data)){
        stop("The synergy scores are not calculated. Please run function ",
             "'CalculateSynergy' first.")
      }
      plot_table <- data$synergy_scores
    }
    plot_table <- plot_table %>% 
      dplyr::filter(block_id == plot_block) %>% 
      dplyr::select(
        dplyr::starts_with("conc"),
        value = !!plot_value
      ) %>%
      dplyr::mutate(
        text = as.character(.RoundValues(value))
      )
  }
  
  # Extract data for selected two drugs from multi-drug combination
  other_concs <- setdiff(concs, selected_concs)
  if (length(other_concs) > 0) {
    conc_zero <- apply(
      dplyr::select(plot_table, dplyr::all_of(concs)), 
      1,
      function(x) {
        sum(x != 0) <= 2
      })
    plot_table <- plot_table[conc_zero, ]
    other_concs_sum <- plot_table %>% 
      dplyr::ungroup() %>% 
      dplyr::select(dplyr::all_of(other_concs)) %>% 
      rowSums()
    plot_table <- plot_table[other_concs_sum == 0, ] %>% 
      dplyr::select(-dplyr::all_of(other_concs))
    colnames(plot_table) <- sapply(colnames(plot_table), function(x){
      if (x == selected_concs[1]){
        return("conc1")
      } else if (x == selected_concs[2]) {
        return("conc2")
      } else {
        return(x)
      }
    })
  }
  
  # Transform conc into factor
  plot_table[, c("conc1", "conc2")] <- lapply(
    plot_table[, c("conc1", "conc2")],
    function(x) {
      factor(.RoundValues(x))
    }
  )
  plot_table <- plot_table %>% 
    dplyr::select(conc1, conc2, value, text)
  return(list(plot_table = plot_table, drug_pair = drug_pair))
}

#' Round the Numbers for Plotting
#' 
#' This function will round the input numbers by 2 digits, if the absolute of 
#' number is larger than or equal to 1. It will take 2 significant digits, if
#' the absolute of number is smaller than 1. 
#'
#' @param numbers A vector of numeric values. It contains the numbers need to
#' be rounded.
#'
#' @return A vector of rounded numbers.
#'
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
.RoundValues <- function(numbers) {
  numbers[abs(numbers) >= 1 & !is.na(numbers)] <- round(
    numbers[abs(numbers) >= 1 & !is.na(numbers)],
    2
  )
  numbers[abs(numbers) < 1 & !is.na(numbers)] <- signif(
    numbers[abs(numbers) < 1 & !is.na(numbers)],
    2
  )
  return(numbers)
}

#' Convert Font Size from pt to mm
#'
#' This function converts font sizes from "pt" unite to "mm" unite.
#'  
#' @param x A numerical value. It is the font size in "pt" unite.
#'
#' @return A numerical value in "mm" unite
#' 
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
.Pt2mm <- function(x) {
  5 * x / 14
}

#' Make a Smooth Surface for Scores
#'
#' @param scores_mat a matrix contains scores which will be visualized
#' @param len length of the interval between plotted data points.
#'
#' @return a matrix which which contains interpolated points for input
#'   \code{scores_mat}.
#' 
#' @author
#' \itemize{
#'   \item Shuyu Zheng \email{shuyu.zheng@helsinki.fi}
#'   \item Jing Tang \email{jing.tang@helsinki.fi}
#' }
#' 
.ExtendedScores <-  function (scores_mat, len) {
  # len: how many values need to be predicted between two adjacent elements
  #      of scores.dose
  options(scipen = 999)
  nr <- nrow(scores_mat)
  nc <- ncol(scores_mat)
  
  ext.row.len <- (nr - 1) * (len + 2) - (nr - 2)
  ext.col.len <- (nc - 1) * (len + 2) - (nc - 2)
  
  extended.row.idx <- seq(1, nr, length = ext.row.len)
  extended.col.idx <- seq(1, nc, length = ext.col.len)
  
  krig.coord <- cbind(rep(extended.row.idx, each = ext.col.len),
                      rep(extended.col.idx, times = ext.row.len))
  extended.scores <- SpatialExtremes::kriging(data = c(scores_mat),
                             data.coord = cbind(rep(seq_len(nr), nc),
                                                rep(seq_len(nc), each=nr)),
                             krig.coord = krig.coord,
                             cov.mod = "whitmat", grid = FALSE,
                             sill = 1, range = 10,
                             smooth = 0.8)$krig.est
  extended.scores <- matrix(extended.scores, nrow = ext.row.len,
                            ncol = ext.col.len, byrow = TRUE)
  
  # extended.scores = data.frame(extended.scores)
  extended.scores <- round(extended.scores, 3)
  
  row.dose <- as.numeric(rownames(scores_mat))
  col.dose <- as.numeric(colnames(scores_mat))
  
  extend.row.dose <- mapply(function(x, y){seq(from = x, to = y,
                                               length.out = len + 2)},
                            row.dose[-nr], row.dose[-1])
  extend.row.dose <- unique(round(c(extend.row.dose), 8))
  
  extend.col.dose <- mapply(function(x, y){seq(from = x, to = y,
                                               length.out = len + 2)},
                            col.dose[-nc], col.dose[-1])
  extend.col.dose <- unique(round(c(extend.col.dose), 8))
  
  rownames(extended.scores) <- extend.row.dose
  colnames(extended.scores) <- extend.col.dose
  
  return(extended.scores)
}

Try the synergyfinder package in your browser

Any scripts or data that you put into this service are public.

synergyfinder documentation built on April 4, 2021, 6 p.m.