R/ShinyPSA_R6.R

################################################################################
#
# Script Name:        ShinyPSA.R
# Module Name:        Economic/PSA
# Script Description: Defines an R6 class function that combines the
#                     functionalities of the ShinyPSA package.
# Author:             WM-University of Sheffield (wmamohammed1@sheffield.ac.uk)
#
################################################################################

# ShinyPSA R6 class: ----
#' R6 Class representing a PSA summarising machine.
#'
#' @description
#' An instance of this class is expected to produce summary plots and
#' tables.
#' @format An [R6::R6Class] object.
#' @name ShinyPSA
NULL
#'
#' @rdname ShinyPSA
#' @export
ShinyPSA <- R6::R6Class(
  # Object name:
  classname = "ShinyPSA",
  # Public elements:
  public = list(

    #' @field CEP_plot the Cost-Effectiveness plane.
    CEP_plot = NULL,
    #' @field CEAC_plot the Cost-Effectiveness Acceptability Curve.
    CEAC_plot = NULL,
    #' @field CEAF_plot the Cost-Effectiveness Acceptability Frontier.
    CEAF_plot = NULL,
    #' @field EVPI_plot the Expected Value of Perfect Information.
    EVPI_plot = NULL,
    #' @field eNMB_plot the expected Net Monitory Benefit.
    eNMB_plot = NULL,
    #' @field app a list to store shiny app elements
    app = NULL,

    #' @description
    #' Initialisation method (triggered when a new object is created).
    #' Summary plots and table(s) are created alongside the construction
    #' of the plot.
    #'
    #' @param .effs A matrix containing the \code{effects} from PSA.
    #' Number of \code{columns} is equal to the interventions while the
    #' number of \code{rows} is equal to the number of PSA simulations to
    #' be summarised.
    #' @param .costs A matrix containing the \code{costs} from PSA.
    #' Number of \code{columns} is equal to the interventions while the
    #' number of \code{rows} is equal to the number of PSA simulations to
    #' be summarised.
    #' @param .interventions A vector containing the names of all
    #' interventions. If not provided or less names than needed is
    #' provided, the function will generate generic names, for example
    #' \code{intervention 1}.
    #' @param .ref An integer indicating the index of the reference
    #' intervention. This parameter is ignored if more than two
    #' \code{interventions} are under analysis.
    #' @param .Kmax The maximum willingness-to-pay threshold to use in the
    #' analysis. This parameter is ignored if \code{wtp} is provided.
    #' @param .wtp A vector of numerical values declaring the
    #' willingness-to-pay (WTP) values to use in the analysis. If
    #' \code{NULL} (default) a range of WTP values (up to \code{.Kmax}
    #' will be used.
    #' @param .plot A boolean, FALSE (default), for whether to generate
    #' plots.
    #'
    #' @return A new `ShinyPSA` object.
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Vaccine_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Vaccine_PSA$c),
    #'                   .interventions = ShinyPSA::Vaccine_PSA$treats)
    #' }
    initialize = function(.effs, .costs, .interventions = NULL,
                          .ref = NULL, .Kmax = 100000, .wtp = NULL,
                          .plot = TRUE) {
      private$effects <- dplyr::as_tibble(.effs)
      private$costs <- dplyr::as_tibble(.costs)
      private$PSA_summary <- private$summarise_PSA_(
        .effs = .effs,
        .costs = .costs,
        .interventions = .interventions,
        .ref = .ref,
        .Kmax = .Kmax,
        .wtp = .wtp,
        .plot = .plot
      )

      invisible(self)

    },

    #' @description
    #' Get the default results summary table
    #'
    #' @param .wtp_ A numeric vector containing the willingness-to-pay
    #' value(s) to be considered in the summary table. Default values are
    #' \code{c(20,000, 30,000)}
    #' @param .units_ A character, the units to associate with the
    #' monitory values in the summary table. Default is sterling pounds
    #' (GBP) \code{£}.
    #' @param .effects_label The label or name to be given to the effects
    #' column in the summary table. Default is QALYs.
    #'
    #'
    #' @return A ggplot2 object
    #' @importFrom tidyselect vars_select_helpers
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Vaccine_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Vaccine_PSA$c),
    #'                   .interventions = ShinyPSA::Vaccine_PSA$treats)
    #'
    #' PSA_outputs$get_Summary_table()
    #' }
    get_Summary_table = function(.wtp_ = c(20000, 30000), .units_ = "£",
                                 .effects_label = "QALYs") {
      if(is.null(private$Summary_table)){
        if(is.null(.units_) | length(.units_) != 1) .units_ = "£"
        # ICER:
        ICER_tbl <- private$PSA_summary[["ICER"]]
        # eNMB:
        eNMB <- private$PSA_summary[["e.NMB"]] %>%
          dplyr::mutate('WTP' = private$PSA_summary[["WTPs"]]) %>%
          dplyr::filter(WTP %in% .wtp_) %>%
          dplyr::mutate(WTP = paste0("NMB @ ", .units_,
                             format(WTP, digits = 1, big.mark = ","))) %>%
          tidyr::pivot_longer(
            cols = -WTP,
            names_to = 'intervention',
            values_to = 'NMB') %>%
          tidyr::pivot_wider(
            id_cols = 'intervention',
            names_from = 'WTP',
            values_from = 'NMB')
        # CEAF:
        CEAF <- dplyr::tibble(
          'CEAF - values' = private$PSA_summary[["CEAF"]]$ceaf,
          'CEAF - WTP' = private$PSA_summary[["WTPs"]],
          'intervention' = private$PSA_summary[["best_name"]]) %>%
          dplyr::filter(`CEAF - WTP` %in% .wtp_) %>%
          dplyr::mutate(`CEAF - WTP` = paste0("Prob. CE @ ", .units_,
                                      format(`CEAF - WTP`, digits = 1,
                                             big.mark = ",")))

        # EVPI:
        EVPI <- dplyr::tibble(
          'EVPI - values' = private$PSA_summary[["EVPI"]],
          'EVPI - WTP' = private$PSA_summary[["WTPs"]],
          'intervention' = private$PSA_summary[["best_name"]]) %>%
          dplyr::filter(`EVPI - WTP` %in% .wtp_) %>%
          dplyr::mutate(`EVPI - WTP` = paste0("EVPI @ ", .units_,
                                      format(`EVPI - WTP`, digits = 1,
                                             big.mark = ",")))

        # Summary table:
        differential_col <- paste("Differential", .effects_label)
        ICER_tbl <- ICER_tbl %>%
          # join the expected NMB to the ICER results:
          dplyr::left_join(x = ., y = eNMB, by = 'intervention') %>%
          # join the probability of being cost-effective:
          dplyr::left_join(x = ., y = CEAF, by = 'intervention') %>%
          tidyr::pivot_wider(
            names_from = `CEAF - WTP`,
            values_from = `CEAF - values`) %>%
          # drop any NAs resulting from pivot_wider:
          dplyr::select(tidyselect::vars_select_helpers$where(
            fn = function(.x) !all(is.na(.x)))) %>%
          # join the EVPI:
          dplyr::left_join(x = ., y = EVPI, by = 'intervention') %>%
          tidyr::pivot_wider(
            names_from = `EVPI - WTP`,
            values_from = `EVPI - values`) %>%
          # drop any NAs resulting from pivot_wider:
          dplyr::select(tidyselect::vars_select_helpers$where(
            fn = function(.x) !all(is.na(.x)))) %>%
          # do some formatting:
          dplyr::mutate(dplyr::across(tidyselect::vars_select_helpers$where(
            is.numeric) & !c(qalys, delta.e, dplyr::starts_with("Prob.")),
            ~ round(.x, digits = 0))) %>%
          dplyr::mutate(dplyr::across(c(qalys, delta.e, dplyr::starts_with("Prob.")),
                        ~ round(.x, digits = 4))) %>%
          dplyr::select(-dplyr::any_of('dominance')) %>%
          dplyr::rename({{.effects_label}} := qalys,
                        Comparators = intervention,
                        {{differential_col}} := delta.e,
                        "Differential Costs" = delta.c,
                        "ICER information" = icer_label) %>%
          dplyr::rename_with(stringr::str_to_title, costs) %>%
          dplyr::rename_with(toupper, icer)

        private$Summary_table <- ICER_tbl
      }

      return(private$Summary_table)
    },

    #' @description
    #' Get the Cost-Effectiveness plane
    #'
    #' @param ... Extra arguments passed to the plotting functions
    #'
    #' @return A ggplot2 object
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Vaccine_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Vaccine_PSA$c),
    #'                   .interventions = ShinyPSA::Vaccine_PSA$treats)
    #' # Get default plot:
    #' PSA_outputs$get_CEP()
    #'
    #' PSA_outputs$get_CEP(
    #'   .ref = 1,
    #'   .show_ICER = T,
    #'   .legend_pos = c(0.8, 0.2),
    #'   .show_wtp = T,
    #'   .zoom = T,
    #'   .wtp_threshold = c(20000, 500, 100, 50),
    #'   .nudge_labels = c(0.1, -0.1),
    #'   .zoom_cords = c(-0.001, 0.001, -5, 5)
    #'   )
    #' }
    get_CEP = function(...) {
      # pass arguments to the plotting function:
      self$CEP_plot <- private$plot_CEplane_(...)

      # return default plot if no arguments were passed to the function:
      dots_ <- list(...)
      if(length(dots_) == 0)
        return(private$PSA_summary[["CEP_plot"]])

      # if any arguments exist, then return the new plot:
      return(self$CEP_plot)
    },

    #' @description
    #' Get the Cost-Effectiveness Acceptability Curve
    #'
    #' @param ... Extra arguments passed to the plotting functions
    #'
    #' @return A ggplot2 object
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #'                   .interventions = ShinyPSA::Smoking_PSA$treats)
    #'
    #' PSA_outputs$get_CEAC()
    #' }
    get_CEAC = function(...) {
      # pass arguments to the plotting function:
      self$CEAC_plot <- private$plot_CEAC_(...)

      # return default plot if no arguments were passed to the function:
      dots_ <- list(...)
      if(length(dots_) == 0)
        return(private$PSA_summary[["CEAC_plot"]])

      # if any arguments exist, then return the new plot:
      return(self$CEAC_plot)
    },

    #' @description
    #' Get the Cost-Effectiveness Acceptability Frontier
    #'
    #' @param ... Extra arguments passed to the plotting functions
    #'
    #' @return A ggplot2 object
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #'                   .interventions = ShinyPSA::Smoking_PSA$treats)
    #'
    #' PSA_outputs$get_CEAF()
    #' }
    get_CEAF = function(...) {
      # pass arguments to the plotting function:
      self$CEAF_plot <- private$plot_CEAF_(...)

      # return default plot if no arguments were passed to the function:
      dots_ <- list(...)
      if(length(dots_) == 0)
        return(private$PSA_summary[["CEAF_plot"]])

      # if any arguments exist, then return the new plot:
      return(self$CEAF_plot)
    },

    #' @description
    #' Get the expected Net Monitory Benefit
    #'
    #' @param ... Extra arguments passed to the plotting functions
    #'
    #' @return A ggplot2 object
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #'                   .interventions = ShinyPSA::Smoking_PSA$treats)
    #'
    #' PSA_outputs$get_eNMB()
    #' }
    get_eNMB = function(...) {
      # pass arguments to the plotting function:
      self$eNMB_plot <- private$plot_eNMB_(...)

      # return default plot if no arguments were passed to the function:
      dots_ <- list(...)
      if(length(dots_) == 0)
        return(private$PSA_summary[["eNMB_plot"]])

      # if any arguments exist, then return the new plot:
      return(self$eNMB_plot)
    },

    #' @description
    #' Get the Expected Value of Perfect Information
    #'
    #' @param ... Extra arguments passed to the plotting functions
    #'
    #' @return A ggplot2 object
    #' @export
    #'
    #' @examples
    #' \dontrun{
    #' # Instantiate a copy of class ShinyPSA:
    #' PSA_outputs <- ShinyPSA$new(
    #'                   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #'                   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #'                   .interventions = ShinyPSA::Smoking_PSA$treats)
    #'
    #' PSA_outputs$get_EVPI()
    #' }
    get_EVPI = function(...) {
      # pass arguments to the plotting function:
      self$EVPI_plot <- private$plot_EVPI_(...)

      # return default plot if no arguments were passed to the function:
      dots_ <- list(...)
      if(length(dots_) == 0)
        return(private$PSA_summary[["EVPI_plot"]])

      # if any arguments exist, then return the new plot:
      return(self$EVPI_plot)
    }

  ),

  # Private elements:
  private = list(

    effects = NULL,
    costs = NULL,
    PSA_summary = NULL,
    # Summary_table a summary table with differentials, ICER(S),
    # net benefits and probability being cost-effective.
    Summary_table = NULL,

    # Summarise PSA outputs and report results
    #
    # .effs A matrix containing the \code{effects} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .costs A matrix containing the \code{costs} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .interventions A vector containing the names of all
    # interventions. If not provided or less names than needed are
    # provided, the function will generate generic names, for example
    # \code{intervention 1}.
    # .ref An integer indicating the index of the reference
    # intervention. This parameter is ignored if more than two
    # \code{interventions} are under analysis.
    # .Kmax The maximum willingness-to-pay threshold to use in the
    # analysis. This parameter is ignored if \code{wtp} is provided.
    # .wtp A vector of numerical values declaring the
    # willingness-to-pay (WTP) values to use in the analysis. If
    # \code{NULL} (default) a range of WTP values (up to \code{.Kmax} will
    # be used.
    # .plot A boolean, FALSE (default), for whether to generate plots.
    #
    # A list of class \code{psa} with \code{24} elements.
    #
    # \dontrun{}
    summarise_PSA_ = function(.effs, .costs, .interventions = NULL,
                              .ref = NULL, .Kmax = 100000, .wtp = NULL,
                              .plot = FALSE) {

      # Stop if .effs & .costs have different dimensions:
      stopifnot('Unequal dimensions in .effs and .costs' =
                  dim(.effs) == dim(.costs),
                'PSA results for less than two interventions is supplied' =
                  ncol(.effs) >= 2)

      # Simulations & interventions analysed:
      n.sim <- nrow(.effs) # Number of simulations
      n.comparators <- ncol(.effs) # Number of interventions
      n.comparisons <- n.comparators - 1 # Number of least possible comparisons
      v.ints <- 1:n.comparators # Vector with index of interventions'

      # Check supplied interventions labels, create ones if any is missing:
      if(!is.null(.interventions) & length(.interventions) != n.comparators) {
        .interventions <- NULL
      }
      if(is.null(.interventions)) {
        .interventions <- paste("intervention", 1:n.comparators)
      }

      # Associate .interventions with number IDs for cleaner plots' labels:
      .interventions <- paste0(1:length(.interventions),
                               ": ",
                               .interventions)

      # Set missing values or remove ones to be ignored:
      if(n.comparators == 2){
        # If no reference was provided in a non-incremental analysis:
        if(is.null(.ref)){
          .ref <- 1
          message(paste0("You did not select a reference intervention. [",
                         .interventions[.ref], "] will be used as reference for differential values and plots."))
        }
        comp <- v.ints[-.ref]
      } else {
        # Ignore .ref if the analysis will be an incremental one:
        if(!is.null(.ref)) {
          .ref <- NULL
          message("More than two interventions, .ref is ignored")
        }
        comp <- NULL
      }

      # Set up willingness-to-pay:
      if (is.null(.Kmax)) {
        .Kmax <- 100000
      }
      if (!is.null(.wtp)) {
        .wtp <- sort(unique(.wtp))
        .Kmax <- max(.wtp)
        v.k <- .wtp
        n.k <- length(.wtp)
        names(v.k) <- paste0("£", format(v.k, big.mark = ","))
      } else {
        n.points <- .Kmax/100
        v.k <- seq(from = 0, to = .Kmax, length.out = n.points + 1)
        v.k <- c(v.k, 20000, 30000, 50000)
        v.k <- sort(unique(v.k))
        n.k <- length(v.k)
        names(v.k) <- paste0("£", format(v.k, big.mark = ","))
      }

      # Ensure .effs and .costs are tibbles and name columns appropriately:
      .effs <- .effs %>%
        dplyr::as_tibble(.name_repair = "unique") %>%
        `colnames<-`(.interventions)
      .costs <- .costs %>%
        dplyr::as_tibble(.name_repair = "unique") %>%
        `colnames<-`(.interventions)

      # Compute effects and costs differentials:
      if(n.comparators == 2) {
        delta.effs <- private$calculate_differentials_(.data = .effs, .ref = .ref)
        delta.costs <- private$calculate_differentials_(.data = .costs, .ref = .ref)
      } else {
        delta.effs <- NULL
        delta.costs <- NULL
      }

      # Compute ICER(s):
      ICER <- private$compute_ICERs_(.icer_data = NULL, .effs = .effs, .costs = .costs,
                                     .interventions = .interventions)

      # Compute NMB or iNMB, e.NMB or e.iNMB and best option for each k:
      nmbs <- private$compute_NMBs_(.effs = .effs, .costs = .costs,
                                    .interventions = .interventions, .Kmax = .Kmax,
                                    .wtp = .wtp)
      NMB <- nmbs$nmb
      e.NMB <- nmbs$e.nmb
      best <- nmbs$best_interv
      best_name <- nmbs$best_interv_name
      check <- nmbs$check
      kstar <- nmbs$wtp_star

      # Compute CEAC:
      CEAC <- private$compute_CEACs_(.nmb = NMB)

      # Compute CEAF:
      CEAF <- private$compute_CEAFs_(.ceac = CEAC)

      # Compute EVPI:
      EVPIs <- private$compute_EVPIs_(.effs = .effs, .costs = .costs, .Kmax = .Kmax,
                                      .interventions = .interventions, .wtp = .wtp)
      U <- EVPIs$U
      Ustar <- EVPIs$Ustar
      ol <- EVPIs$ol
      vi <- EVPIs$vi
      EVPI <- EVPIs$evi

      ## Outputs of the function
      results <- list(

        interventions = .interventions, ref = .ref, comp = comp,
        ICER = ICER, NMB = NMB, e.NMB = e.NMB, CEAC = CEAC, CEAF = CEAF,
        EVPI = EVPI, best_id = best, best_name = best_name, WTPs = v.k,
        WTPstar = kstar, U = U, Ustar = Ustar, vi = dplyr::as_tibble, ol = ol, e = .effs,
        c = .costs, delta.e = delta.effs, delta.c = delta.costs, n.sim = n.sim,
        n.comparators = n.comparators, step = n.k, Kmax = .Kmax
      )

      class(results) <- "psa"

      # If requested, develop and save plots:
      if(.plot == TRUE) {
        # Cost-Effectiveness plane:
        CEP_plot <- private$plot_CEplane_(.PSA_data = results, .ref = .ref)
        CEAC_plot <- private$plot_CEAC_(.PSA_data = results, .ref = .ref)
        CEAF_plot <- private$plot_CEAF_(.PSA_data = results)
        EVPI_plot <- private$plot_EVPI_(.PSA_data = results)
        eNMB_plot <- private$plot_eNMB_(.PSA_data = results)
        results <- c(results,
                     'CEP_plot' = list(CEP_plot),
                     'CEAC_plot' = list(CEAC_plot),
                     'CEAF_plot' = list(CEAF_plot),
                     'EVPI_plot' = list(EVPI_plot),
                     'eNMB_plot' = list(eNMB_plot))
      }

      return(results)
    },

    # Identify dominated interventions
    #
    # .icer_data A table containing average costs and QALYs data
    # .qalys Character indicating the name of the column containing
    # Quality Adjusted Life Years (QALYs) in \code{.icer_data}
    # .costs Character indicating the name of the column
    # containing cost data in \code{.icer_data}
    #
    # A table containing \code{.icer_data} in addition to identified
    # dominance
    #
    # \dontrun{}
    identify_dominance_ = function(.icer_data, .qalys = qalys,
                                   .costs = costs) {
      # Check if missing key columns and create them if so:
      .icer_data <- .icer_data %>%
        private$add_missing_columns_(.x = .,
                                     .characters = c("dominance", "icer_label"),
                                     .numerics = c(".id", "delta.e", "delta.c",
                                                   "icer"))

      # Identify dominated interventions:
      .icer_data <- .icer_data %>%
        dplyr::arrange({{.qalys}}) %>%
        dplyr::group_by(dominance) %>%
        dplyr::mutate(
          icer_label = dplyr::case_when(
            is.na(dominance) ~ dplyr::case_when(
              dplyr::lead({{.costs}}) < {{.costs}} ~ paste0("[dominated by ",
                                                     dplyr::lead(.id), "]")),
            TRUE ~ icer_label),
          dominance = dplyr::case_when(
            is.na(dominance) ~ dplyr::case_when(
              dplyr::lead({{.costs}}) < {{.costs}} ~ "dominated"),
            TRUE ~ dominance)) %>%
        dplyr::ungroup()

      return(.icer_data)
    },

    # Identify extendedly dominated interventions
    #
    # .icer_data A table containing average costs and QALYs data
    # .qalys Character indicating the name of the column containing
    # Quality Adjusted Life Years (QALYs) in \code{.icer_data}
    #
    # A vector stating whether any of the included interventions were
    # e.dominated
    #
    # \dontrun{}
    identify_e.dominance_ = function(.icer_data, .qalys = qalys) {
      # Check if missing key columns and create them if so:
      .icer_data <- .icer_data %>%
        private$add_missing_columns_(.x = .,
                                     .characters = c("dominance", "icer_label"),
                                     .numerics = c(".id", "delta.e", "delta.c", "icer"))

      # Identify extendedly dominated interventions:
      .icer_data <- .icer_data %>%
        dplyr::arrange({{.qalys}}) %>%
        dplyr::group_by(dominance) %>%
        dplyr::mutate(
          icer_label = dplyr::case_when(
            is.na(dominance) ~ dplyr::case_when(
              dplyr::lead(icer) < icer ~ paste0("[extendedly dominated by ",
                                         dplyr::lead(.id), "]")),
            TRUE ~ icer_label),
          dominance = dplyr::case_when(
            is.na(dominance) ~ dplyr::case_when(
              dplyr::lead(icer) < icer ~ "e.dominated"),
            TRUE ~ dominance)) %>%
        dplyr::ungroup()

      return(.icer_data)
    },

    # Calculate ICER(s) and effects and costs differentials
    #
    # .icer_data A table containing average costs and QALYs data
    # .qalys Character indicating the name of the column containing
    # Quality Adjusted Life Years (QALYs) data in .icer_data
    # .costs Character indicating the name of the column containing
    # cost data in .icer_data
    #
    # A table of \code{effects diffrentials}, \code{costs
    # differentials} & \code{icers}
    #
    # \dontrun{}
    calculate_ICERs_ = function(.icer_data, .qalys = qalys,
                                .costs = costs) {
      # Check if missing key columns and create them if so:
      .icer_data <- .icer_data %>%
        private$add_missing_columns_(.x = .,
                                     .characters = c("dominance", "icer_label"),
                                     .numerics = c(".id", "delta.e", "delta.c",
                                                   "icer"))

      # Compute Incremental Cost-Effectiveness Ratio (ICER):
      .icer_data <- .icer_data %>%
        dplyr::arrange({{.qalys}}) %>%
        dplyr::group_by(dominance) %>%
        dplyr::mutate(
          delta.e = dplyr::case_when(
            is.na(dominance) ~ c(NA, diff({{.qalys}}))),
          delta.c = dplyr::case_when(
            is.na(dominance) ~ c(NA, diff({{.costs}}))),
          icer = dplyr::case_when(
            is.na(dominance) ~ delta.c / delta.e),
          icer_label = dplyr::case_when(
            is.na(dominance) & !is.na(icer) ~ paste0("[ICER = £",
                                                     format(icer,
                                                            digits = 1,
                                                            big.mark = ","),
                                                     ", vs ",
                                                     dplyr::lag(.id), "]"),
            is.na(dominance) & is.na(icer) ~ dplyr::case_when(
              dplyr::n() > 1 ~ paste0("[ICER reference]"),
              TRUE ~ icer_label),
            TRUE ~ icer_label)) %>%
        dplyr::ungroup()

      return(.icer_data)
    },

    # Identify, iteratively, all dominated interventions
    #
    # .x A table containing average costs and QALYs data
    #
    # A dataframe with data from .x in addition to dominance
    # information, if any
    #
    # \dontrun{}
    dominance_wraper_ = function(.x) {
      # Check if missing key columns and create them if so:
      .x <- .x %>%
        private$add_missing_columns_(.x = .,
                                     .characters = c("dominance", "icer_label"),
                                     .numerics = c(".id", "delta.e", "delta.c",
                                                   "icer"))

      # Check for unidentified dominance
      while (any("dominated" %in%
                 (.x %>%
                  dplyr::filter(dplyr::if_any(dominance, ~ is.na(.))) %>%
                  private$identify_dominance_() %>%
                  dplyr::pull(dominance)))) {
        # Do until all dominated are identified
        .x <- .x %>%
          private$identify_dominance_()
      }

      return(.x)
    },

    # Identify, iteratively, all extendedly dominated interventions
    #
    # .x A table containing average costs and QALYs data
    #
    # A dataframe with data from \code{.x} in addition to extended
    # dominance information, if any
    #
    # \dontrun{}
    e.dominance_wraper_ = function(.x) {
      # Check if missing key columns and create them if so:
      .x <- .x %>%
        private$add_missing_columns_(.x = .,
                                     .characters = c("dominance", "icer_label"),
                                     .numerics = c(".id", "delta.e", "delta.c",
                                                   "icer"))

      # Check for any remaining e.dominance
      while (any("e.dominated" %in% (.x %>%
                                     dplyr::filter(dplyr::if_any(dominance, ~ is.na(.))) %>%
                                     private$identify_e.dominance_() %>%
                                     dplyr::pull(dominance)))) {
        # Do until all extendedly dominated are identified:
        .x <- .x %>%
          private$identify_e.dominance_() %>%
          private$calculate_ICERs_() # ICER(s) for un-dominated/e.dominated
      }

      return(.x)
    },

    # Compute ICER(s)
    #
    # .icer_data A table containing average costs and QALYs data
    # .effs A tibble containing the \code{effects} from PSA.
    # Number of \code{columns} is equal to the interventions while the
    # number of \code{rows} is equal to the number of PSA simulations
    # to be summarised.
    # .costs A tibble containing the \code{costs} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .interventions A vector containing the names of all
    # interventions. If not provided or less names than needed is
    # provided, the function will generate generic names, for example
    # \code{intervention 1}.
    #
    # A dataframe with data from icer_data in addition to
    # \code{qalys and costs diffrential(s)}, \code{dominance} &
    # \code{icer(s)}
    #
    # \dontrun{}
    compute_ICERs_ = function(.icer_data, .effs = NULL, .costs = NULL,
                              .interventions = NULL) {
      # If a summary table of costs, effects and intervention names supplied:
      if(!is.null(.icer_data)) {
        # Check if missing key columns and create them if so:
        icer_tmp <- .icer_data %>%
          private$add_missing_columns_(.x = .,
                                       .characters = c("dominance", "icer_label"),
                                       .numerics = c(".id", "delta.e", "delta.c",
                                                     "icer"))
      } else if(!is.null(.effs) & !is.null(.costs)) {

        # Stop if .effs & .costs are not of class tibble or have unequal dims:
        stopifnot('.effs is not a tibble' = "data.frame" %in% class(.effs),
                  '.costs is not a tibble' = "data.frame" %in% class(.costs),
                  '.effs and .costs have unequal dimensions' =
                    dim(.effs) == dim(.costs))

        # Get number of interventions in supplied matrix:
        n.comparators <- ncol(.effs) # Number of interventions

        # Check supplied interventions labels, create ones if any is missing:
        if(!is.null(.interventions) & length(.interventions) != n.comparators) {
          .interventions <- NULL
        }
        if(is.null(.interventions)) {
          .interventions <- paste("intervention", 1:n.comparators)
          # Associate .interventions with number IDs for cleaner plots' labels:
          .interventions <- paste0(1:length(.interventions),
                                   ": ",
                                   .interventions)
        }

        # Define ICER table:
        icer_tmp <- dplyr::tibble(
          'intervention' = .interventions,
          'qalys' = colMeans(.effs),
          'costs' = colMeans(.costs)) %>%
          private$add_missing_columns_(.x = .,
                                       .characters = c("dominance", "icer_label"),
                                       .numerics = c(".id", "delta.e", "delta.c",
                                                     "icer"))
      } else {
        stop("Please supply costs and effects from PSA, each in a separate
         tibble/dataframe, or a summary table with interventions' names,
         and corresponding mean costs and mean qalys")
      }

      # Identify dominated interventions:
      icer_tmp <- icer_tmp %>%
        private$dominance_wraper_()

      # Compute ICER(s), before extended dominance checking:
      icer_tmp <- icer_tmp %>%
        private$calculate_ICERs_()

      # Identify any extendedly dominated interventions, and recompute ICER(s):
      icer_tmp <- icer_tmp %>%
        private$e.dominance_wraper_()

      # Drop .id:
      icer_tmp <- icer_tmp %>%
        dplyr::select(-.id)

      return(icer_tmp)
    },

    # Compute Monetary Net-Benefit (NMB) or incremental NMB (iNMB)
    #
    # .effs A tibble containing the \code{effects} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .costs A tibble containing the \code{costs} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .interventions A vector containing the names of all
    # interventions. If not provided or less names than needed is
    # provided, the function will generate generic names, for example
    # \code{intervention 1}.
    # .Kmax The maximum willingness-to-pay threshold to use in the
    # analysis. This parameter is ignored if \code{wtp} is provided.
    # .wtp A vector of numerical values declaring the
    # willingness-to-pay (WTP) values to use in the analysis.
    # If \code{NULL} (default) a range of WTP values (up to \code{.Kmax}
    # will be used.
    #
    # A list containing the NMB list, eNMB tibble, WTP tibble and
    # other objects.
    #
    # \dontrun{}
    compute_NMBs_ = function(.effs, .costs, .interventions = NULL,
                             .Kmax = NULL, .wtp = NULL) {
      # Stop if .effs & .costs are not of class tibble or have unequal dims:
      stopifnot('.effs is a not tibble' = "data.frame" %in% class(.effs),
                '.costs is a not tibble' = "data.frame" %in% class(.costs),
                '.effs and .costs have unequal dimensions' =
                  dim(.effs) == dim(.costs))

      # Simulations & interventions analysed:
      n.comparators <- ncol(.effs) # Number of interventions
      n.comparisons <- n.comparators - 1 # Number of least possible comparisons
      v.ints <- 1:n.comparators # Vector with index of interventions'

      # Check supplied interventions labels, create ones if any is missing:
      if(!is.null(.interventions) & length(.interventions) != n.comparators) {
        .interventions <- NULL
      }
      if(is.null(.interventions)) {
        .interventions <- paste("intervention", 1:n.comparators)
        # Associate .interventions with number IDs for cleaner plots' labels:
        .interventions <- paste0(1:length(.interventions),
                                 ": ",
                                 .interventions)
      }

      # Name .effs and .costs columns appropriately:
      .effs <- .effs %>%
        `colnames<-`(.interventions)
      .costs <- .costs %>%
        `colnames<-`(.interventions)

      # Set up willingness-to-pay:
      if (is.null(.Kmax)) {
        .Kmax <- 100000
      }
      if (!is.null(.wtp)) {
        .wtp <- sort(unique(.wtp))
        .Kmax <- max(.wtp)
        v.k <- .wtp
        n.k <- length(.wtp)
        names(v.k) <- paste0("£", format(v.k, big.mark = ","))
      } else {
        n.points <- .Kmax/100
        v.k <- seq(from = 0, to = .Kmax, length.out = n.points + 1)
        v.k <- c(v.k, 20000, 30000, 50000)
        v.k <- sort(unique(v.k))
        n.k <- length(v.k)
        names(v.k) <- paste0("£", format(v.k, big.mark = ","))
      }

      # Compute monetary net benefit (NMB) (default):
      nmb <- purrr::map2(.x = .effs,
                  .y = .costs,
                  .f = function(.eff = .x, .cost = .y) {
                    purrr::map_dfc(as.list(v.k),
                            .f = function(.k = .x) {
                              .eff * .k - .cost})}) %>%
        purrr::transpose()

      # Compute expected net benefit (e.NMB):
      e.nmb <- nmb %>%
        purrr::map_dfr(.f = function(.x) {
          colMeans(dplyr::as_tibble(.x, .name_repair = "unique"))
        })

      # Select the best option for each willingness-to-pay value:
      best_interv <- e.nmb %>%
        max.col(ties.method = "first")
      best_interv_name <- .interventions[best_interv]

      # Finds the wtp value for which the optimal decision changes
      check <- c(0, diff(best_interv))
      wtp_star <- v.k[check != 0]

      return(list(nmb = nmb, e.nmb = e.nmb, check = check,
                  wtp_star = wtp_star, wtp = v.k,
                  best_interv = best_interv,
                  best_interv_name = best_interv_name))
    },

    # Compute Cost-Effectiveness Acceptability Curve (CEAC)
    #
    # .nmb A list (with similar features to a 3D-array) containing the
    # Net Monetary Benefits from each probabilistic sensitivity analysis
    # (PSA)
    # run for each intervention across a range of willingness-to-pay (WTP)
    # values. The dimensions of this list are:
    # \code{List:WTP, Tibble(Rows: PSA simulations, Cols: Interventions)}.
    # .effs A tibble containing the \code{effects} from PSA. Number of
    #  \code{columns} is equal to the interventions while the number of
    #  \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .costs A tibble containing the \code{costs} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .interventions A vector containing the names of all interventions.
    # If not provided or less names than needed is provided,
    # the function will generate generic names, for example
    # \code{intervention 1}.
    # .Kmax The maximum willingness-to-pay threshold to use in the
    # analysis. This parameter is ignored if \code{wtp} is provided.
    # .wtp A vector of numerical values declaring the
    # willingness-to-pay (WTP) values to use in the analysis. If
    # \code{NULL} (default) a range of WTP values (up to \code{.Kmax} will
    # be used.
    #
    # A tibble containing the probability of being cost-effective
    # for all interventions.
    #
    # \dontrun{}
    compute_CEACs_ = function(.nmb, .effs = NULL, .costs = NULL,
                              .interventions = NULL, .Kmax = NULL,
                              .wtp = NULL) {
      # If .nmb was not available but raw data were:
      if(is.null(.nmb) & !is.null(.effs) & !is.null(.costs)){
        .nmb <- private$compute_NMBs_(.effs = .effs,
                                      .costs = .costs,
                                      .interventions = .interventions,
                                      .Kmax = .Kmax,
                                      .wtp = .wtp)
        .nmb <- .nmb$nmb
      }

      # Stop if object .nmb is not of class list:
      stopifnot('.nmb is not a list' = "list" %in% class(.nmb))

      # CEAC in incremental analysis:
      ceac <- .nmb %>%
        purrr::map_dfr(.f = function(.x) {
          colMeans(do.call(pmax, dplyr::as_tibble(.x, .name_repair = "unique")) ==
                     dplyr::as_tibble(.x, .name_repair = "unique"))})

      return(ceac)
    },

    # Compute Cost-Effectiveness Acceptability Frontier
    #
    # .ceac A tibble containing the probability of being cost-effective
    # for all interventions.
    # .nmb A list (with similar features to a 3D-array) containing the
    # Net Monetary Benefits from each probabilistic sensitivity analysis
    # (PSA) run for each intervention across a range of willingness-to-pay
    # (WTP) values. The dimensions of this list are:
    # \code{List:WTP, Tibble(Rows: PSA simulations, Cols: Interventions)}.
    #
    # A tibble containing the probability of being cost-effective
    # for all interventions alongside the CEAF.
    #
    # \dontrun{}
    compute_CEAFs_ = function(.ceac, .nmb = NULL) {
      # Stop if object .ceac is not of class tibble:
      stopifnot('.ceac is a not tibble' = "data.frame" %in% class(.ceac))
      if(!is.null(.nmb))
        stopifnot('.nmb is not a list' = "list" %in% class(.nmb))

      # If .ceac was not available but .nmb was:
      if(is.null(.ceac) & !is.null(.nmb))
        .ceac <- private$compute_CEACs_(.nmb = .nmb)

      # Compute CEAF:
      ceaf <- .ceac %>%
        dplyr::mutate('ceaf' = if(any(rowSums(.) != 1)) NA_real_
               else do.call(pmax, .))

      return(ceaf)
    },

    # Compute the Expected Value of Perfect Information (EVPI)
    #
    # .effs A tibble containing the \code{effects} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .costs A tibble containing the \code{costs} from PSA. Number of
    # \code{columns} is equal to the interventions while the number of
    # \code{rows} is equal to the number of PSA simulations to be
    # summarised.
    # .interventions A vector containing the names of all interventions.
    # If not provided or less names than needed is provided,
    # the function will generate generic names, for example
    # \code{intervention 1}.
    # .Kmax The maximum willingness-to-pay threshold to use in the
    # analysis. This parameter is ignored if \code{wtp} is provided.
    # .wtp A vector of numerical values declaring the
    # willingness-to-pay (WTP) values to use in the analysis. If
    # \code{NULL} (default) a range of WTP values (up to \code{.Kmax} will
    # be used.
    #
    # A list containing the EVPI vector, value of information tibble,
    # opportunity lost tibble among others
    #
    # \dontrun{}
    compute_EVPIs_ = function(.effs, .costs, .interventions = NULL,
                              .Kmax = NULL, .wtp = NULL) {
      # Stop if .effs & .costs are not of class tibble or have unequal dims:
      stopifnot('.effs is not a tibble' = "data.frame" %in% class(.effs),
                '.costs is not a tibble' = "data.frame" %in% class(.costs),
                '.effs and .costs have unequal dimensions' =
                  dim(.effs) == dim(.costs))

      # Simulations & interventions analysed:
      n.comparators <- ncol(.effs) # Number of interventions

      # Check supplied interventions labels, create ones if any is missing:
      if(!is.null(.interventions) & length(.interventions) != n.comparators) {
        .interventions <- NULL
      }
      if(is.null(.interventions)) {
        .interventions <- paste("intervention", 1:n.comparators)
        # Associate .interventions with number IDs for cleaner plots' labels:
        .interventions <- paste0(1:length(.interventions),
                                 ": ",
                                 .interventions)
      }

      # Name .effs and .costs columns appropriately:
      .effs <- .effs %>%
        `colnames<-`(.interventions)
      .costs <- .costs %>%
        `colnames<-`(.interventions)

      # Set up willingness-to-pay:
      if (is.null(.Kmax)) {
        .Kmax <- 100000
      }
      if (!is.null(.wtp)) {
        .wtp <- sort(unique(.wtp))
        .Kmax <- max(.wtp)
        v.k <- .wtp
        n.k <- length(.wtp)
        names(v.k) <- paste0("£", format(v.k, big.mark = ","))
      } else {
        n.points <- .Kmax/100
        v.k <- seq(from = 0, to = .Kmax, length.out = n.points + 1)
        v.k <- c(v.k, 20000, 30000, 50000)
        v.k <- sort(unique(v.k))
        n.k <- length(v.k)
        names(v.k) <- paste0("£", format(v.k, big.mark = ","))
      }

      # Compute monetary net benefit (NMB) (default):
      nmb <- purrr::map2(.x = .effs, .y = .costs,
                  .f = function(.eff = .x, .cost = .y) {
                    purrr::map_dfc(as.list(v.k),
                            .f = function(.k = .x) {
                              .eff * .k - .cost})}) %>%
        purrr::transpose()

      # Compute expected net benefit (e.NMB):
      e.nmb <- nmb %>%
        purrr::map_dfr(.f = function(.x) {
          colMeans(dplyr::as_tibble(.x, .name_repair = "unique"))
        })

      # Identify the best option for each willingness-to-pay value:
      best_interv <- e.nmb %>%
        max.col(ties.method = "first")

      # Extract maximum nmb value at each iteration for each wtp/threshold:
      max_nmb_iter <- nmb %>%
        purrr::map_dfr(.f = function(.x) {
          do.call(pmax, dplyr::as_tibble(.x, .name_repair = "unique"))
        })

      # Compute opportunity loss (OL):
      ol <- purrr::pmap_dfc(.l = list(nmb, best_interv, max_nmb_iter),
                     .f = function(.x, .y, .z) {
                       .z - .x[[.y]]
                     })

      # Compute value-of-information (VI):
      vi <- purrr::map2_dfc(.x = max_nmb_iter, .y = nmb,
                     .f = function(.x, .y) {
                       .x - max(colMeans(dplyr::as_tibble(.y, .name_repair = "unique")))
                     })

      # Compute expected value-of-information (EVPI):
      evi <- colMeans(ol)

      return(list(U = nmb, Ustar = max_nmb_iter, vi = vi, ol = ol, evi = evi))
    },

    # Plot Cost Effectiveness Acceptability Curve (CEAC)
    #
    # .PSA_data A list of class shinyPSA that contains summary PSA
    # results.
    # ... Additional arguments that include:
    # reference intervention \code{.ref = NULL} rescales interventions on
    # CEP, legend position \code{.legend_pos = c(0.8, 0.85)},
    # willingness-to-pay threshold(s)
    # \code{.wtp_threshold = c(20000, 30000)},
    # show WTP threshold(s) lines \code{.show_wtp = TRUE},
    # show WTP threshold(s) labels \code{.label_wtp' = TRUE},
    # zoom to min/max values \code{.zoom = FALSE},
    # zoom to supplied coordinates values \code{.zoom_cords = NULL},
    # show 20 points/shapes along the lines \code{.show_shapes = FALSE},
    # and add Cost Effectiveness Acceptability Curve \code{.add_CEAF =
    # FALSE}.
    #
    # An object of class ggplot.
    #
    # \dontrun{
    # library(ShinyPSA)
    #
    # PSA_summary <- summarise_PSA_(
    #   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #   .interventions = ShinyPSA::Smoking_PSA$treats)
    #
    # p <- plot_CEAC_(PSA_summary,
    #                .ref = 1,
    #                .legend_pos = NULL,
    #                .wtp_threshold = c(2000, 10000, 20000, 25000),
    #                .show_wtp = TRUE,
    #                .label_wtp = FALSE,
    #                .zoom = FALSE,
    #                .zoom_cords = NULL,
    #                .show_shapes = TRUE,
    #                .add_CEAF = TRUE)
    #
    # p
    # }
    #
    plot_CEAC_ = function(.PSA_data = private$PSA_summary, ...) {
      # Grab the function's environment for correct assignment in assign():
      env_ = environment()
      # Define defaults:
      default_args <- list(
        '.ref' = NULL, # Integer 1:length(interventions)
        '.legend_pos' = c(0.8, 0.85), # c(x, y) double between 0:1
        '.wtp_threshold' = c(20000, 30000),
        '.show_wtp' = TRUE, # TRUE/FALSE
        '.label_wtp' = FALSE, # TRUE/FALSE
        '.zoom' = FALSE, # TRUE/FALSE
        '.zoom_cords' = NULL, # c(x, x) double min and max x axis values
        '.show_shapes' = FALSE, # TRUE/FALSE
        '.add_CEAF' = FALSE) # TRUE/FALSE
      # Grab additional arguments:
      args_ <- list(...)
      # Assign additional arguments:
      private$assign_extraArgs_(.default_args_ = default_args,
                                .args_ = args_,
                                .env_ = env_)
      # Override .ref if more than two interventions exist:
      if(!is.null(.ref) & (length(.PSA_data$interventions) > 2)) .ref = NULL
      # Function to remove intervention from plot data:
      drop_intervention <- function(.data_, .ref = .ref) {
        if(!is.null(.ref)) .data_ <- .data_ %>%
            select(-dplyr::all_of(.ref))
        else .data_ <- .data_
        return(.data_)
      }

      # Plot data:
      ceac_df = .PSA_data$CEAC %>%
        drop_intervention(.data_ = ., .ref = .ref) %>%
        dplyr::mutate('WTP threshold' = .PSA_data$WTPs) %>%
        tidyr::pivot_longer(cols = -`WTP threshold`,
                     names_to = 'Option',
                     values_to = 'Probability cost-effective')

      # Zoom:
      if(.zoom | (!is.null(.zoom_cords) & is.numeric(.zoom_cords))) {
        .zoom = TRUE
        if(is.null(.zoom_cords) |
           (!is.null(.zoom_cords) & length(.zoom_cords) != 2))
          .zoom_cords = c(0, 31000)
      }

      # CEAC main plot:
      p <- ggplot2::ggplot() +
        ggplot2::coord_cartesian(ylim = c(0, 1), xlim = .zoom_cords, expand = FALSE) +
        ggplot2::geom_hline(
          yintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_vline(
          xintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_line(
          data = ceac_df,
          ggplot2::aes(x = `WTP threshold`,
              y = `Probability cost-effective`,
              color = Option),
          size = 0.4) +
        ggplot2::scale_x_continuous(labels = scales::dollar_format(prefix = "£")) +
        ggplot2::scale_y_continuous(labels = scales::percent_format()) +
        ggplot2::theme(
          plot.title.position = "plot", # Start title from near the margin
          legend.position = .legend_pos,
          legend.title = ggplot2::element_blank(),
          # Control legend text alignment:
          legend.text.align = 0, # 0 left (default), 1 right
          # Remove background and box around the legend:
          legend.background = ggplot2::element_rect(fill = NA, color = NA),
          legend.spacing = ggplot2::unit(0, "cm"), # spacing between legend items
          legend.spacing.y = ggplot2::unit(-0.195, "cm"), # bring legends closer
          # Add a box around the keys:
          legend.key = ggplot2::element_rect(fill = "white", colour = "grey"),
          legend.key.size = ggplot2::unit(0.35, "cm"),
          # Add a border and space around the plot:
          panel.border = ggplot2::element_rect(colour = 'black', fill = NA),
          plot.margin = ggplot2::unit(c(5.5, 1, 5.5, 5.5), # more space LHS
                             c("points", "cm", "points", "points"))) +
        ggplot2::labs(
          title = "Cost Effectiveness Acceptability Curve (CEAC)",
          x = "Willingness-to-pay (£)",
          y = "Probability cost-effective") +
        ggplot2::guides(
          # Increase the size of the points in the legend:
          color = ggplot2::guide_legend(
            override.aes = list(order = 1,
                                size = 1,
                                alpha = 1,
                                shape = NA)))

      # Show/hide WTP on the CEAC:
      if(.show_wtp) {
        ## CEAC plot willingness-to-pay (WTP) values:
        .wtp = .wtp_threshold %>%
          dplyr::as_tibble() %>%
          dplyr::mutate(
            x_cord = .wtp_threshold,
            y_cord = 1,
            angle_cord = 0,
            label_cord = paste0("£", format(.wtp_threshold,
                                            big.mark = ",")),
            lty_ = "Willingness-to-pay (£)")

        ## Plot:
        p <- p +
          ggplot2::geom_vline(
            data = .wtp,
            ggplot2::aes(xintercept = x_cord,
                linetype = lty_),
            colour = "dark gray") +
          ggplot2::scale_linetype_manual(
            breaks = .wtp$lty_[1], # keep one for the legend
            values = rep(3, nrow(.wtp))) +
          ggplot2::guides(
            # Remove the shapes from the line:
            linetype = ggplot2::guide_legend(
              override.aes = list(order = 3,
                                  shape = NA, # remove shape
                                  color = 'black')))
      }

      # Label WTP value(s) on the CEAC:
      if(.label_wtp) {
        p <- p +
          ggrepel::geom_text_repel(
            data = .wtp,
            ggplot2::aes(x = x_cord,
                y = y_cord,
                angle = angle_cord,
                label = label_cord),
            size = 1.5,
            show.legend = FALSE)
      }

      # Show/hide shapes on the CEAC:
      if(.show_shapes) {
        ## Data:
        n_points <- .PSA_data$WTPstar
        n_points <- c(0, n_points,
                      seq(from = 0,
                          to = .PSA_data$WTPs[length(.PSA_data$WTPs)],
                          length.out = 20),
                      .PSA_data$WTPs[length(.PSA_data$WTPs)],
                      .wtp_threshold)
        n_points <- sort(
          unique(
            plyr::round_any(n_points, 100, f = ceiling)))

        ## Plot:
        p <- p +
          ggplot2::geom_point(
            data = ceac_df %>%
              dplyr::filter(`WTP threshold` %in% n_points),
            ggplot2::aes(x = `WTP threshold`,
                y = `Probability cost-effective`,
                shape = Option, color = Option),
            size = 1,
            show.legend = TRUE)
      }

      # Show/hide CEAF on the CEAC:
      if(.add_CEAF & (length(.PSA_data$interventions) > 2)) {
        ## Data:
        ### Select a few points:
        n_points <- .PSA_data$WTPstar
        n_points <- c(0, n_points,
                      seq(from = 0,
                          to = .PSA_data$WTPs[length(.PSA_data$WTPs)],
                          length.out = 20),
                      .PSA_data$WTPs[length(.PSA_data$WTPs)],
                      .wtp_threshold)
        n_points <- sort(
          unique(
            plyr::round_any(n_points, 100, f = ceiling)))

        ### CEAF:
        ceaf_df = .PSA_data$CEAF %>%
          dplyr::mutate('Best option' = .PSA_data$best_name,
                 'WTP threshold' = .PSA_data$WTPs)

        ## Plot:
        p <- p +
          ggplot2::geom_point(
            data = ceaf_df %>%
              dplyr::filter(`WTP threshold` %in% n_points),
            ggplot2::aes(x = `WTP threshold`,
                y = ceaf),
            size = 2,
            stroke = 1,
            alpha = 0.8,
            shape = 21,
            color = "black",
            show.legend = TRUE) +
          ggplot2::scale_fill_manual(
            values = c("CEAF" = "black")) +
          ggplot2::guides(
            fill = ggplot2::guide_legend(
              override.aes = list(order = 2,
                                  shape = 21,
                                  size = 2.5,
                                  stroke = 1)))
      }

      return(p)
    },

    # Plot Cost Effectiveness Acceptability Frontier (CEAF)
    #
    # .PSA_data A list of class shinyPSA that contains summary PSA
    # results.
    # ... Additional arguments that include:
    # legend position \code{.legend_pos = c(0.8, 0.85)},
    # willingness-to-pay threshold(s)
    # \code{.wtp_threshold = c(20000, 30000)},
    # show WTP threshold(s) lines \code{.show_wtp = TRUE},
    # show WTP threshold(s) labels \code{.label_wtp' = TRUE},
    # zoom to min/max values \code{.zoom = FALSE},
    # zoom to supplied coordinates values \code{.zoom_cords = NULL}, and
    # show 20 points/shapes along the lines \code{.show_shapes = FALSE}.
    #
    # An object of class ggplot.
    #
    # \dontrun{
    # library(ShinyPSA)
    #
    # PSA_summary <- summarise_PSA_(
    #   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #   .interventions = ShinyPSA::Smoking_PSA$treats)
    #
    # p <- plot_CEAF_(PSA_summary,
    #                .legend_pos = NULL,
    #                .wtp_threshold = c(2000, 10000, 20000, 25000),
    #                .show_wtp = TRUE,
    #                .label_wtp = FALSE,
    #                .zoom = FALSE,
    #                .zoom_cords = NULL,
    #                .show_shapes = TRUE)
    #
    # p
    # }
    #
    plot_CEAF_ = function(.PSA_data = private$PSA_summary, ...) {
      # Grab the function's environment for correct assignment in assign():
      env_ = environment()
      # Define defaults:
      default_args <- list(
        '.legend_pos' = c(0.8, 0.85), # c(x, y) double between 0:1
        '.wtp_threshold' = c(20000, 30000),
        '.show_wtp' = TRUE, # TRUE/FALSE
        '.label_wtp' = FALSE, # TRUE/FALSE
        '.zoom' = FALSE, # TRUE/FALSE
        '.zoom_cords' = NULL, # c(x, x) double min and max x axis values
        '.show_shapes' = FALSE) # TRUE/FALSE
      # Grab additional arguments:
      args_ <- list(...)
      # Assign additional arguments:
      private$assign_extraArgs_(.default_args_ = default_args,
                                .args_ = args_,
                                .env_ = env_)

      # Plot data:
      ceaf_df = .PSA_data$CEAF %>%
        dplyr::mutate('Best option' = .PSA_data$best_name,
               'WTP threshold' = .PSA_data$WTPs)

      # Zoom:
      if(.zoom | (!is.null(.zoom_cords) & is.numeric(.zoom_cords))) {
        .zoom = TRUE
        if(is.null(.zoom_cords) |
           (!is.null(.zoom_cords) & length(.zoom_cords) != 2))
          .zoom_cords = c(0, 31000)
      }

      # CEAC main plot:
      p <- ggplot2::ggplot() +
        ggplot2::coord_cartesian(ylim = c(0, 1), xlim = .zoom_cords, expand = FALSE) +
        ggplot2::geom_hline(
          yintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_vline(
          xintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_line(
          data = ceaf_df,
          ggplot2::aes(x = `WTP threshold`,
              y = ceaf,
              group = 1,
              color = `Best option`),
          size = 0.4) +
        ggplot2::scale_x_continuous(labels = scales::dollar_format(prefix = "£")) +
        ggplot2::scale_y_continuous(labels = scales::percent_format()) +
        ggplot2::theme(
          plot.title.position = "plot", # Start title from near the margin
          legend.position = .legend_pos,
          legend.title = ggplot2::element_blank(),
          # Control legend text alignment:
          legend.text.align = 0, # 0 left (default), 1 right
          # Remove background and box around the legend:
          legend.background = ggplot2::element_rect(fill = NA, color = NA),
          legend.spacing = ggplot2::unit(0, "cm"), # spacing between legend items
          legend.spacing.y = ggplot2::unit(-0.195, "cm"), # bring legends closer
          # Add a box around the keys:
          legend.key = ggplot2::element_rect(fill = "white", colour = "grey"),
          legend.key.size = ggplot2::unit(0.35, "cm"),
          # Add a border and space around the plot:
          panel.border = ggplot2::element_rect(colour = 'black', fill = NA),
          plot.margin = ggplot2::unit(c(5.5, 1, 5.5, 5.5), # more space LHS
                             c("points", "cm", "points", "points"))) +
        ggplot2::labs(
          title = "Cost Effectiveness Acceptability Frontier (CEAF)",
          x = "Willingness-to-pay (£)",
          y = "Probability cost-effective") +
        ggplot2::guides(
          # Increase the size of the points in the legend:
          color = ggplot2::guide_legend(
            override.aes = list(order = 1,
                                size = 1,
                                alpha = 1,
                                shape = NA)))

      # Show/hide WTP on the CEAF:
      if(.show_wtp) {
        ## CEAF plot willingness-to-pay (WTP) values:
        .wtp = .wtp_threshold %>%
          dplyr::as_tibble() %>%
          dplyr::mutate(
            x_cord = .wtp_threshold,
            y_cord = 1,
            angle_cord = 0,
            label_cord = paste0("£", format(.wtp_threshold,
                                            big.mark = ",")),
            lty_ = "Willingness-to-pay (£)")

        ## Plot:
        p <- p +
          ggplot2::geom_vline(
            data = .wtp,
            ggplot2::aes(xintercept = x_cord,
                linetype = lty_),
            colour = "dark gray") +
          ggplot2::scale_linetype_manual(
            breaks = .wtp$lty_[1], # keep one for the legend
            values = rep(3, nrow(.wtp))) +
          ggplot2::guides(
            # Remove the shapes from the line:
            linetype = ggplot2::guide_legend(
              override.aes = list(order = 2,
                                  shape = NA, # remove shape
                                  color = 'black')))
      }

      # Label WTP value(s) on the CEAF:
      if(.label_wtp) {
        p <- p +
          ggrepel::geom_text_repel(
            data = .wtp,
            ggplot2::aes(x = x_cord,
                y = y_cord,
                angle = angle_cord,
                label = label_cord),
            size = 1.5,
            show.legend = FALSE)
      }

      # Show/hide shapes on the CEAF:
      if(.show_shapes) {
        ## Data:
        ### Select a few points:
        n_points <- .PSA_data$WTPstar
        n_points <- c(0, n_points,
                      seq(from = 0,
                          to = .PSA_data$WTPs[length(.PSA_data$WTPs)],
                          length.out = 20),
                      .PSA_data$WTPs[length(.PSA_data$WTPs)],
                      .wtp_threshold)
        n_points <- sort(
          unique(
            plyr::round_any(n_points, 100, f = ceiling)))

        ## Plot:
        p <- p +
          ggplot2::geom_point(
            data = ceaf_df %>%
              dplyr::filter(`WTP threshold` %in% n_points),
            ggplot2::aes(x = `WTP threshold`,
                y = ceaf,
                color = `Best option`,
                shape = `Best option`),
            size = 1.5,
            alpha = 0.8,
            show.legend = TRUE)
      }

      return(p)
    },

    # Plot Cost Effectiveness Plane (CEP).
    #
    # .PSA_data A list of class shinyPSA that contains summary PSA
    # results.
    # ... Additional arguments that include:
    # reference intervention \code{.ref = NULL} rescales interventions on
    # CEP, legend position \code{.legend_pos = c(0.8, 0.2)},
    # show ICER information \code{.show_ICER' = TRUE},
    # nudge ICER labels \code{.nudge_labels' = c(NULL, NULL)},
    # willingness-to-pay threshold(s)
    # \code{.wtp_threshold = c(20000, 30000)},
    # show WTP threshold(s) lines and labels \code{.show_wtp = TRUE}, and
    # seed number to set \code{.seed_no = 1}.
    #
    # An object of class ggplot.
    #
    # \dontrun{
    # library(ShinyPSA)
    #
    # PSA_summary <- summarise_PSA_(
    #   .effs = as_tibble(ShinyPSA::Vaccine_PSA$e),
    #   .costs = as_tibble(ShinyPSA::Vaccine_PSA$c),
    #   .interventions = ShinyPSA::Vaccine_PSA$treats)
    #
    # p <- plot_CEplane_(PSA_summary,
    #                  .ref = 1,
    #                  .show_ICER = TRUE,
    #                  .legend_pos = c(0.8, 0.2),
    #                  .show_wtp = FALSE,
    #                  .zoom = T,
    #                  .wtp_threshold = c(200),
    #                  tst = "PRINT", # this will be ignored
    #                  .nudge_labels = c(0.1, -0.1),
    #                  .zoom_cords = c(-0.001, 0.001, -5, 5)))
    #
    # p
    # }
    #
    plot_CEplane_ = function(.PSA_data = private$PSA_summary, ...) {
      # Get the function's environment for correct assignment in assign():
      env_ = environment()
      # Define defaults:
      default_args <- list(
        '.ref' = NULL, # Integer 1:length(interventions)
        '.legend_pos' = c(0.8, 0.2), # c(x, y) double between 0:1
        '.show_ICER' = TRUE, # TRUE/FALSE
        '.nudge_labels' = c(NULL, NULL), # c(x, y) double between 0:1
        '.wtp_threshold' = c(20000, 30000),
        '.show_wtp' = TRUE, # TRUE/FALSE
        '.zoom' = FALSE, # TRUE/FALSE
        '.zoom_cords' = NULL) # double c(min x, max x, min y, max y)
      # Grab additional arguments:
      args_ <- list(...)
      # Assign additional arguments:
      private$assign_extraArgs_(.default_args_ = default_args,
                                .args_ = args_,
                                .env_ = env_)

      # Plot data:
      ## CE plot points:
      if(is.null(.ref)) { # No rescaling of point data
        ce_plane_dt <- .PSA_data$e %>%
          dplyr::mutate(sims = dplyr::row_number()) %>%
          tidyr::pivot_longer(cols = -sims,
                       names_to = "interventions",
                       values_to = "Effects") %>%
          dplyr::left_join(x = .,
                    y = .PSA_data$c %>%
                      dplyr::mutate(sims = dplyr::row_number()) %>%
                      tidyr::pivot_longer(cols = -sims,
                                   names_to = "interventions",
                                   values_to = "Costs"),
                    by = c("sims", "interventions"))
        # Labels:
        .title_lab = "Cost Effectiveness Plane"
        .x_lab = "Effects"
        .y_lab = "Costs (£)"
      } else { # Rescale point data
        ce_plane_dt <- .PSA_data$e %>%
          private$calculate_differentials_(.ref = .ref) %>%
          dplyr::mutate(sims = dplyr::row_number()) %>%
          tidyr::pivot_longer(cols = -sims,
                       names_to = "interventions",
                       values_to = "Effects") %>%
          dplyr::left_join(x = .,
                    y = .PSA_data$c %>%
                      private$calculate_differentials_(.ref = .ref) %>%
                      dplyr::mutate(sims = dplyr::row_number()) %>%
                      tidyr::pivot_longer(cols = -sims,
                                   names_to = "interventions",
                                   values_to = "Costs"),
                    by = c("sims", "interventions"))
        # Labels:
        .title_lab = "Cost Effectiveness Plane"
        .x_lab = "Effectiveness differential"
        .y_lab = "Cost differential (£)"
      }

      ## CE plot mean values:
      ce_plane_mean_dt <- ce_plane_dt %>%
        dplyr::group_by(interventions) %>%
        dplyr::summarise(
          Effects = mean(Effects),
          Costs = mean(Costs))

      # Plot:
      p <- ggplot2::ggplot() +
        ggplot2::geom_hline(
          yintercept = 0, colour = "dark gray") +
        ggplot2::geom_vline(
          xintercept = 0, colour = "dark gray") +
        ggplot2::geom_point(
          data = ce_plane_dt,
          ggplot2::aes(x = Effects,
              y = Costs,
              color = interventions),
          size = 1, alpha = 0.5) +
        ggplot2::scale_y_continuous(
          labels = scales::dollar_format(prefix = "£",
                                         big.mark = ",")) +
        ggplot2::geom_point(
          data = ce_plane_mean_dt,
          ggplot2::aes(x = Effects,
              y = Costs,
              fill = interventions),
          shape = 21, colour = "black", show.legend = TRUE,
          size = 2, alpha = 1, stroke = 0.6) +
        ## Keep one value in the legend:
        ggplot2::scale_fill_discrete(
          breaks = ce_plane_mean_dt$interventions[1], # keep one
          labels = "Mean effects/costs") + # change its label
        ggplot2::theme(
          plot.title.position = "plot", # Start title from near the margin
          legend.position = .legend_pos,
          legend.title = ggplot2::element_blank(),
          # Control legend text alignment:
          legend.text.align = 0, # 0 left (default), 1 right
          # Remove background and box around the legend:
          legend.background = ggplot2::element_rect(fill = NA, color = NA),
          legend.spacing = ggplot2::unit(0, "cm"), # spacing between legend items
          legend.spacing.y = ggplot2::unit(-0.195, "cm"), # bring legends closer
          # Add a box around the keys:
          legend.key = ggplot2::element_rect(fill = "white", colour = "grey"),
          legend.key.size = ggplot2::unit(0.35, "cm"),
          # Add a border around the plot:
          panel.border = ggplot2::element_rect(colour = 'black', fill = NA),
          plot.margin = ggplot2::unit(c(5.5, 1, 5.5, 5.5), # more space LHS
                             c("points", "cm", "points", "points"))) +
        ggplot2::labs(
          title = .title_lab,
          x = .x_lab,
          y = .y_lab) +
        ggplot2::guides(
          # Increase the size of the points in the legend:
          color = ggplot2::guide_legend(
            override.aes = list(order = 1,
                                size = 1.5,
                                alpha = 1,
                                stroke = NA, # remove stroke
                                linetype = 0)), # remove line
          # Remove the fill colour in shape 21, generalising it to all
          # options:
          fill = ggplot2::guide_legend(
            override.aes = list(order = 2,
                                size = 2.5,
                                alpha = 1,
                                fill = NA, # remove fill
                                linetype = 0))) # remove line

      # Show/hide ICER label(s) on the CE plot:
      if(.show_ICER) {
        ## CE plot ICER labels nudging values:
        .nudge_labels[1] = max(ce_plane_dt$Effects) *
          .nudge_labels[1]
        .nudge_labels[2] = (max(ce_plane_dt$Costs) - min(ce_plane_dt$Costs)) *
          .nudge_labels[2]

        ## Plot:
        p <- p +
          ggrepel::geom_text_repel(
            data = ce_plane_mean_dt,
            ggplot2::aes(x = Effects,
                y = Costs,
                label = .PSA_data$ICER$icer_label),
            force_pull = 8,
            size = 2.5,
            point.padding = 0,
            nudge_x = .nudge_labels[1],
            nudge_y = .nudge_labels[2],
            segment.curvature = 1e-8,
            arrow = ggplot2::arrow(length = ggplot2::unit(0.015, "npc")),
            max.overlaps = Inf,
            min.segment.length = 0)
      }

      # Show/hide WTP label(s) on the CE plot:
      if(.show_wtp) {
        ## CE plot willingness-to-pay (WTP) values:
        if(max(ce_plane_dt$Effects) < max(abs(ce_plane_dt$Effects))){
          ### Get labels' coordinates dynamically:
          x_cord = ifelse(rep(min(ce_plane_dt$Costs), length(.wtp_threshold)) <
                            0,
                          ifelse((min(ce_plane_dt$Costs) / .wtp_threshold) <=
                                   min(ce_plane_dt$Effects),
                                 min(ce_plane_dt$Effects),
                                 (min(ce_plane_dt$Costs) / .wtp_threshold)),
                          ifelse(-(min(ce_plane_dt$Costs) / .wtp_threshold) <=
                                   min(ce_plane_dt$Effects),
                                 min(ce_plane_dt$Effects),
                                 -(min(ce_plane_dt$Costs) / .wtp_threshold)))
          y_cord = ifelse(rep(max(ce_plane_dt$Costs), length(.wtp_threshold)) <
                            0,
                          0,
                          ifelse((min(ce_plane_dt$Effects) * .wtp_threshold) <=
                                   min(ce_plane_dt$Costs),
                                 min(ce_plane_dt$Costs),
                                 (min(ce_plane_dt$Effects) * .wtp_threshold)))
        } else {
          ### Get labels' coordinates dynamically:
          x_cord = ifelse((max(ce_plane_dt$Costs) / .wtp_threshold) >=
                            max(ce_plane_dt$Effects),
                          max(ce_plane_dt$Effects),
                          (max(ce_plane_dt$Costs) / .wtp_threshold))
          y_cord = ifelse(rep(max(ce_plane_dt$Costs), length(.wtp_threshold)) <
                            0,
                          0,
                          ifelse((max(ce_plane_dt$Effects) * .wtp_threshold) >=
                                   max(ce_plane_dt$Costs),
                                 max(ce_plane_dt$Costs),
                                 (max(ce_plane_dt$Effects) * .wtp_threshold)))
        }

        ### Get axis scale to correctly set the labels:
        x_range <- ggplot2::layer_scales(p)$x$range$range
        y_range <- ggplot2::layer_scales(p)$y$range$range
        x_to_y <- (x_range[2] - x_range[1])/(y_range[2] - y_range[1])
        ### Calculate angles:
        angle_cord <- atan(.wtp_threshold * x_to_y) * 180/pi
        ### Put .wtp data on tibble:
        .wtp = .wtp_threshold %>%
          dplyr::as_tibble() %>%
          dplyr::mutate(
            x_cord = x_cord,
            y_cord = y_cord,
            angle_cord = angle_cord,
            label_cord = paste0("£", format(.wtp_threshold,
                                            big.mark = ",")),
            lty_ = "Willingness-to-pay (£)")

        ## Plot:
        p <- p +
          ggplot2::geom_abline(
            data = .wtp,
            ggplot2::aes(intercept = 0,
                slope = value,
                linetype = lty_),
            show.legend = TRUE) +
          ggplot2::scale_linetype_manual(
            breaks = .wtp$lty_[1], # keep one for the legend
            values = rep(3, nrow(.wtp))) +
          ggrepel::geom_text_repel(
            data = .wtp,
            ggplot2::aes(x = x_cord,
                y = y_cord,
                #angle = angle_cord,
                label = label_cord),
            size = 1.5,
            show.legend = FALSE) +
          ggplot2::guides(
            # Remove the stroke from the line:
            linetype = ggplot2::guide_legend(
              override.aes = list(order = 3,
                                  stroke = NA)) # remove stroke
          )
      }

      # Zoom to max x and y values:
      if(.zoom &
         (is.null(.zoom_cords) |
          if(!is.null(.zoom_cords)) length(.zoom_cords) < 4 else TRUE)) {

        ## CE plot x and y axis limits:
        x_lim = c(NA, max(ce_plane_dt$Effects))
        y_lim = c(NA, max(ce_plane_dt$Costs))

        # Plot:
        p <- p +
          ggplot2::coord_cartesian(xlim = x_lim, ylim = y_lim, expand = !.zoom,
                          default = .zoom)
      }

      if(.zoom & !is.null(.zoom_cords) &
         if(!is.null(.zoom_cords)) length(.zoom_cords) == 4 else FALSE) {
        ## CE plot x and y axis limits:
        x_lim = c(.zoom_cords[1], .zoom_cords[2])
        y_lim = c(.zoom_cords[3], .zoom_cords[4])

        # Plot:
        p <- p +
          ggplot2::coord_cartesian(xlim = x_lim, ylim = y_lim, expand = !.zoom,
                          default = .zoom)
      }

      return(p)
    },

    # Plot the Expected Value of Perfect Information (EVPI)
    #
    # .PSA_data A list of class shinyPSA that contains summary PSA
    # results.
    # ... Additional arguments that include:
    # legend position \code{.legend_pos = "bottom"},
    # willingness-to-pay threshold(s)
    # \code{.wtp_threshold = c(20000, 30000)},
    # show WTP threshold(s) lines \code{.show_wtp = TRUE},
    # show WTP threshold(s) labels \code{.label_wtp' = TRUE},
    # plot individual EVPI \code{.individual_evpi' = TRUE},
    # time horizon to estimate population EVPI \code{.time_horion = 5},
    # discount rate to estimate population EVPI
    # \code{.discount_rate = 0.035},
    # population size for population EVPI \code{.population' = 15000},
    # zoom to min/max values \code{.zoom = FALSE}, and
    # zoom to supplied coordinates values \code{.zoom_cords = NULL}.
    #
    # An object of class ggplot.
    #
    # \dontrun{
    # library(ShinyPSA)
    #
    # PSA_summary <- summarise_PSA_(
    #   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #   .interventions = ShinyPSA::Smoking_PSA$treats)
    #
    # p <- plot_EVPI_(PSA_summary,
    #                .legend_pos = NULL,
    #                .wtp_threshold = c(2000, 10000, 20000, 25000),
    #                .show_wtp = TRUE,
    #                .label_wtp = FALSE,
    #                .individual_evpi = FALSE,
    #                .time_horion = 1,
    #                .discount_rate = 0.035,
    #                .population = 15000,
    #                .zoom = FALSE,
    #                .zoom_cords = NULL)
    #
    # p
    # }
    #
    plot_EVPI_ = function(.PSA_data = private$PSA_summary, ...) {
      # Grab the function's environment for correct assignment in assign():
      env_ = environment()
      # Define defaults:
      default_args <- list(
        '.legend_pos' = "bottom", # c(x, y) double between 0:1 or character
        '.wtp_threshold' = c(20000, 30000),
        '.show_wtp' = TRUE, # TRUE/FALSE
        '.label_wtp' = FALSE, # TRUE/FALSE
        '.individual_evpi' = TRUE, # TRUE/FALSE
        '.time_horion' = 5, # Integer
        '.discount_rate' = 0.035, # double 0:1
        '.population' = 15000, # Integer
        '.zoom' = FALSE, # TRUE/FALSE
        '.zoom_cords' = NULL) # c(x, x) double min and max x axis values
      # Grab additional arguments:
      args_ <- list(...)
      # Assign additional arguments:
      private$assign_extraArgs_(.default_args_ = default_args,
                                .args_ = args_,
                                .env_ = env_)

      # Plot data:
      discounted_population = 1
      subtitle_lab = "    Individual EVPI"

      ## Population EVPI:
      if(!.individual_evpi){
        ## Population EVPI:
        discounted_population <- sum(
          .population / ((1 + .discount_rate)^(1:.time_horion)))
        subtitle_lab = paste0("    Population EVPI: [",
                              "Population size: ", .population, "; ",
                              "Time horizon: ", .time_horion, " year(s); ",
                              "Discount rate: ", .discount_rate, ".]")
      }
      ## EVPI data:
      evpi_df <- dplyr::tibble('EVPI' = .PSA_data$EVPI * discounted_population,
                        'WTP threshold' = .PSA_data$WTPs,
                        'Best option' = .PSA_data$best_name)

      # Zoom:
      y_cords <- NULL
      if(.zoom | (!is.null(.zoom_cords) & is.numeric(.zoom_cords))) {
        .zoom = TRUE
        if(is.null(.zoom_cords) |
           (!is.null(.zoom_cords) & length(.zoom_cords) != 2))
          .zoom_cords = c(0, 31000)
      }

      # CEAC main plot:
      p <- ggplot2::ggplot() +
        ggplot2::coord_cartesian(ylim = y_cords, xlim = .zoom_cords, expand = FALSE) +
        ggplot2::geom_hline(
          yintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_vline(
          xintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_line(
          data = evpi_df,
          ggplot2::aes(x = `WTP threshold`,
              y = EVPI),
          size = 0.4) +
        ggplot2::scale_x_continuous(labels = scales::dollar_format(prefix = "£")) +
        ggplot2::scale_y_continuous(labels = scales::dollar_format(prefix = "£")) +
        ggplot2::theme(
          # Adjust title size and position:
          plot.title.position = "plot", # Start title from near the margin
          plot.subtitle = ggplot2::element_text(size = 6, face = "italic"),
          legend.position = .legend_pos,
          legend.title = ggplot2::element_blank(),
          # Control legend text alignment:
          legend.text.align = 0, # 0 left (default), 1 right
          # Remove background and box around the legend:
          legend.background = ggplot2::element_rect(fill = NA, color = NA),
          legend.spacing = ggplot2::unit(0, "cm"), # spacing between legend items
          legend.spacing.y = ggplot2::unit(-0.195, "cm"), # bring legends closer
          legend.margin=ggplot2::margin(t = -8), # remove space between it x-axis
          # Add a box around the keys:
          legend.key = ggplot2::element_rect(fill = "white", colour = "grey"),
          legend.key.size = ggplot2::unit(0.35, "cm"),
          # Add a border and space around the plot:
          panel.border = ggplot2::element_rect(colour = 'black', fill = NA),
          plot.margin = ggplot2::unit(c(5.5, 1, 5.5, 5.5), # more space LHS
                             c("points", "cm", "points", "points"))) +
        ggplot2::labs(
          title = "Expected Value of Perfect Information (EVPI)",
          x = "Willingness-to-pay (£)",
          y = "Expected value of perfect information",
          subtitle = subtitle_lab)

      # Show/hide WTP on the CEAF:
      if(.show_wtp) {
        ## CEAF plot willingness-to-pay (WTP) values:
        .wtp = .wtp_threshold %>%
          dplyr::as_tibble() %>%
          dplyr::mutate(
            x_cord = .wtp_threshold,
            y_cord = max(evpi_df$EVPI),
            angle_cord = 0,
            label_cord = paste0("£", format(.wtp_threshold,
                                            big.mark = ",")),
            lty_ = "Willingness-to-pay (£)")

        ## Plot:
        p <- p +
          ggplot2::geom_vline(
            data = .wtp,
            ggplot2::aes(xintercept = x_cord,
                linetype = lty_),
            colour = "dark gray") +
          ggplot2::scale_linetype_manual(
            breaks = .wtp$lty_[1], # keep one for the legend
            values = rep(3, nrow(.wtp))) +
          ggplot2::guides(
            # Remove the shapes from the line:
            linetype = ggplot2::guide_legend(
              override.aes = list(order = 2,
                                  shape = NA, # remove shape
                                  color = 'black')))
      }

      # Label WTP value(s) on the CEAF:
      if(.label_wtp) {
        p <- p +
          ggrepel::geom_text_repel(
            data = .wtp,
            ggplot2::aes(x = x_cord,
                y = y_cord,
                angle = angle_cord,
                label = label_cord),
            size = 1.5,
            show.legend = FALSE)
      }

      return(p)
    },

    # Plot Expected Net Monetary Benefit (eNMB)
    #
    # .PSA_data A list of class shinyPSA that contains summary PSA
    # results.
    # ... Additional arguments that include:
    # legend position \code{.legend_pos = "bottom"},
    # willingness-to-pay threshold(s)
    # \code{.wtp_threshold = c(20000, 30000)},
    # show WTP threshold(s) lines \code{.show_wtp = TRUE},
    # show WTP threshold(s) labels \code{.label_wtp' = TRUE},
    # zoom to min/max values \code{.zoom = FALSE}, and
    # zoom to supplied coordinates values \code{.zoom_cords = NULL}.
    #
    # An object of class ggplot.
    #
    # \dontrun{
    # library(ShinyPSA)
    #
    # PSA_summary <- summarise_PSA_(
    #   .effs = as_tibble(ShinyPSA::Smoking_PSA$e),
    #   .costs = as_tibble(ShinyPSA::Smoking_PSA$c),
    #   .interventions = ShinyPSA::Smoking_PSA$treats)
    #
    # p <- plot_eNMB_(PSA_summary,
    #                .legend_pos = NULL,
    #                .wtp_threshold = c(2000, 10000, 20000, 25000),
    #                .show_wtp = TRUE,
    #                .label_wtp = FALSE,
    #                .zoom = FALSE,
    #                .zoom_cords = NULL)
    #
    # p
    # }
    #
    plot_eNMB_ = function(.PSA_data = private$PSA_summary, ...) {
      # Grab the function's environment for correct assignment in assign():
      env_ = environment()
      # Define defaults:
      default_args <- list(
        '.legend_pos' = c(0.22, 0.78), # c(x, y) between 0:1 or character
        '.wtp_threshold' = c(20000, 30000),
        '.show_wtp' = TRUE, # TRUE/FALSE
        '.label_wtp' = FALSE, # TRUE/FALSE
        '.zoom' = FALSE, # TRUE/FALSE
        '.zoom_cords' = NULL) # c(x, x)  min and max x axis values
      # Grab additional arguments:
      args_ <- list(...)
      # Assign additional arguments:
      private$assign_extraArgs_(.default_args_ = default_args,
                                .args_ = args_,
                                .env_ = env_)

      # Plot data:
      enmb_df <- .PSA_data$e.NMB %>%
        dplyr::as_tibble() %>%
        dplyr::mutate('WTP threshold' = .PSA_data$WTPs,
               'Best option' = .PSA_data$best_name) %>%
        tidyr::pivot_longer(cols = colnames(.PSA_data$e.NMB),
                     names_to = 'Option',
                     values_to = 'eNMB')

      # Zoom:
      y_cords <- NULL
      if(.zoom | (!is.null(.zoom_cords) & is.numeric(.zoom_cords))) {
        .zoom = TRUE
        if(is.null(.zoom_cords) |
           (!is.null(.zoom_cords) & length(.zoom_cords) != 2))
          .zoom_cords = c(0, 31000)
      }

      # CEAC main plot:
      p <- ggplot2::ggplot() +
        ggplot2::coord_cartesian(ylim = y_cords, xlim = .zoom_cords, expand = FALSE) +
        ggplot2::geom_hline(
          yintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_vline(
          xintercept = 0,
          color = 'grey',
          size = 0.1) +
        ggplot2::geom_line(
          data = enmb_df,
          ggplot2::aes(x = `WTP threshold`,
              y = eNMB,
              group = Option,
              linetype = Option,
              color = Option),
          size = 0.4) +
        ggplot2::scale_x_continuous(labels = scales::dollar_format(prefix = "£")) +
        ggplot2::scale_y_continuous(labels = scales::dollar_format(prefix = "£")) +
        ggplot2::theme(
          plot.title.position = "plot", # Start title from near the margin
          legend.position = .legend_pos,
          legend.title = ggplot2::element_blank(),
          # Control legend text alignment:
          legend.text.align = 0, # 0 left (default), 1 right
          # Remove background and box around the legend:
          legend.background = ggplot2::element_rect(fill = NA, color = NA),
          legend.spacing = ggplot2::unit(0, "cm"), # spacing between legend items
          legend.spacing.y = ggplot2::unit(-0.195, "cm"), # bring legends closer
          #legend.margin=margin(t = -8), # remove space between it x-axis
          # Add a box around the keys:
          legend.key = ggplot2::element_rect(fill = "white", colour = "grey"),
          legend.key.size = ggplot2::unit(0.35, "cm"),
          # Add a border and space around the plot:
          panel.border = ggplot2::element_rect(colour = 'black', fill = NA),
          plot.margin = ggplot2::unit(c(5.5, 1, 5.5, 5.5), # more space LHS
                             c("points", "cm", "points", "points"))) +
        ggplot2::labs(
          title = "Expected Net Monetary Benefit (eNMB)",
          x = "Willingness-to-pay (£)",
          y = "Expected Net Monetary Benefit (£)")


      # Show/hide WTP on the CEAF:
      if(.show_wtp) {
        ## CEAF plot willingness-to-pay (WTP) values:
        .wtp = .wtp_threshold %>%
          dplyr::as_tibble() %>%
          dplyr::mutate(
            x_cord = .wtp_threshold,
            y_cord = max(enmb_df$eNMB),
            angle_cord = 0,
            label_cord = paste0("£", format(.wtp_threshold,
                                            big.mark = ",")),
            lty_ = "Willingness-to-pay (£)")

        ## Plot:
        p <- p +
          ggplot2::geom_vline(
            data = .wtp,
            ggplot2::aes(xintercept = x_cord,
                alpha = lty_),
            color = 'dark gray',
            linetype = 3) +
          ggplot2::scale_alpha_manual(
            breaks = .wtp$lty_[1], # keep one for the legend
            values = rep(1, nrow(.wtp))) +
          # scale_colour_manual(
          #   breaks = .wtp$lty_[1], # keep one for the legend
          #   values = rep("dark gray", nrow(.wtp)))
          ggplot2::guides(
            # Remove the shapes from the line:
            alpha = ggplot2::guide_legend(
              override.aes = list(order = 2,
                                  shape = NA, # remove shape
                                  color = 'black')))
      }

      # Label WTP value(s) on the CEAF:
      if(.label_wtp) {
        p <- p +
          ggrepel::geom_text_repel(
            data = .wtp,
            ggplot2::aes(x = x_cord,
                y = y_cord,
                angle = angle_cord,
                label = label_cord),
            size = 1.5,
            show.legend = FALSE)
      }

      return(p)
    },

    # Check and add any missing columns expected by ICER computation
    # functions
    #
    # .characters the columns to ensure in returned data table that are
    # expected to contain characters.
    # .numerics the columns to ensure in returned data table that are
    # expected to contain numerics.
    # .x the data table to which the columns are to be added.
    #
    # A tibble with at least the set of variables/columns provided by the
    # user.
    #
    # \dontrun{}
    add_missing_columns_ = function(.x, .characters, .numerics) {
      # Check for missing columns:
      missing_nms <- dplyr::setdiff(c(.numerics, .characters), names(.x))

      # In case there were missing columns:
      if(!length(missing_nms) == 0) {
        # Create missing columns:
        .x <- .x %>%
          cbind(missing_nms %>%
                  `names<-`(missing_nms) %>%
                  purrr::map_dfc(.f = function(.x) {
                    .x = ifelse(.x %in% .numerics, NA_real_, NA_character_)
                  }))
      }
      .x <- .x %>%
        dplyr::select(".id", dplyr::everything()) %>%
        dplyr::mutate(.id = dplyr::row_number())

      return(.x)
    },

    # Calculate differential costs and QALYs
    #
    # .data_ A dataframe containing costs or QALYs data for which the
    # function is to estimate differential values
    # .ref An integer indicating the index of the reference intervention
    #
    # A tibble with differential costs and QALYs.
    #
    # \dontrun{}
    calculate_differentials_ = function(.data_, .ref) {
      differentials_data <- .data_ %>%
        dplyr::mutate(dplyr::across(.fns = function(.x) {
          .x - .data_ %>%
            dplyr::pull({{.ref}})
        }))

      return(differentials_data)
    },

    # Assign extra arguments/parameters in parent function
    #
    # .default_args_ # A list containing default arguments names and
    # their values.
    # .env_ # Environment object grabbed from the parent function's
    # environment to correctly assign arguments to that function.
    # .args_ # A list containing supplied/additional arguments names
    # and their values. Arguments in .default_args_ but existing in .args_
    # will be assigned values from .args_ and vice versa.
    #
    # This function assigns variables/objects in the parent's function
    # environment, hence it returns nothing.
    #
    # \dontrun{}
    assign_extraArgs_ = function(.default_args_, .env_, .args_) {
      # Grab default arguments' names:
      if(is.null(names(.default_args_)))
        stop(".default_args_ should contain named objects")
      if(length(names(.default_args_)) != length(.default_args_))
        stop("all arguments in .default_args_ should be named")
      expected_args_names <- names(.default_args_)
      # Grab additional arguments' names:
      supplied_args_names <- names(.args_)
      # Let the user know if any of the supplied arguments were unrecognised:
      if(any(!supplied_args_names %in% expected_args_names))
        message("Argument(s) [",
                paste(supplied_args_names[!supplied_args_names %in%
                                            expected_args_names]),
                "] is/are unknown, and therefore ignored")
      # Set additional arguments:
      purrr::walk(
        .x = expected_args_names,
        .f = function(.arg) {
          assign(.arg,
                 if(is.null(.args_[[.arg]])) {
                   .default_args_[[.arg]]
                 } else {
                   .args_[[.arg]]
                 }, envir = .env_)
        })
    }

  )
)
W-Mohammed/ShinyPSA documentation built on April 24, 2022, 6:57 p.m.