Nothing
#' @title plot_gwas_power
#' @description
#' Generates a statistical power analysis plot for GWAS studies.
#' Supports binary (case-control) traits over a range of odds ratios and minor allele frequencies,
#' and quantitative traits over a range of effect sizes and minor allele frequencies.
#' This function uses the 'genpwr' package for calculations and creates a highly customized ggplot.
#'
#' @author Zhen Lu <luzh29@mail2.sysu.edu.cn>
#' @param trait_type Character string specifying trait type: "bt" for binary (case-control) or "qt" for quantitative traits. Default: `"bt"`.
#' @param n_cases Number of cases in the study (required if `trait_type = "bt"`).
#' @param n_controls Number of controls in the study (required if `trait_type = "bt"`).
#' @param sd_trait Numeric, standard deviation of the quantitative trait (required if `trait_type = "qt"`).
#' @param N Numeric, total sample size for quantitative traits (required if `trait_type = "qt"`).
#' @param maf_levels A numeric vector of Minor Allele Frequencies (MAFs) to test.
#' Default: `c(0.01, 0.02, 0.05, 0.10, 0.20, 0.50)`.
#' @param or_range A numeric vector specifying the sequence of Odds Ratios (ORs) to test.
#' Default: `seq(1.01, 2.00, 0.001)`. Used when `trait_type = "bt"`.
#' @param effect_size A numeric vector specifying the sequence of effect sizes (beta) to test for quantitative traits.
#' Default: `seq(0.01, 0.30, 0.001)`. Used when `trait_type = "qt"`.
#' @param alpha The significance level (alpha) for the power calculation.
#' Default: `5e-8`.
#' @param plot_title A string for the plot title.
#' Default: `Cases / Controls` for binary traits or `Sample Size` for quantitative traits.
#' @param save_plot Logical, whether to save the plot to a file. If `FALSE`, the
#' plot object is only returned. Default: `TRUE`.
#' @param output_graphics The file format for saving the plot. Currently supports
#' "png" and "pdf". Default: "png".
#' @param width The width of the saved plot in inches. Default: 17.
#' @param height The height of the saved plot in inches. Default: 9.
#' @param dpi The resolution of the saved plot in dots per inch. Default: 600.
#'
#' @return A list containing two elements:
#' \item{plot}{The ggplot object for the power plot.}
#' \item{power_data}{A data.table containing the full results from the power analysis.}
#'
#' @details
#' This function automates the process of calculating and visualizing GWAS power for both
#' binary (case-control) and quantitative traits. For binary traits, it analyzes power across
#' odds ratios, while for quantitative traits, it analyzes power across effect sizes.
#' It highlights the minimum OR/effect size required to achieve 80% power for the lowest and
#' third-lowest MAF levels, adding dashed lines and color-coded labels for clarity.
#'
#' @examples
#' \donttest{
#' # Binary trait example (case-control)
#' power_results_bt <- plot_gwas_power(
#' trait_type = "bt",
#' n_cases = 4324,
#' n_controls = 93945,
#' save_plot = FALSE
#' )
#'
#' # Quantitative trait example
#' power_results_qt <- plot_gwas_power(
#' trait_type = "qt",
#' sd_trait = 0.09365788681305078,
#' N = 10000,
#' maf_levels = c(0.01, 0.02, 0.05, 0.10, 0.20, 0.50),
#' effect_size = seq(0.01, 0.10, 0.001),
#' save_plot = FALSE
#' )
#'
#' # Access the ggplot object and data
#' # print(power_results_bt$plot)
#' # print(power_results_bt$power_data)
#' }
#'
#' @rdname plot_gwas_power
#' @export
#' @importFrom data.table as.data.table :=
#' @importFrom genpwr genpwr.calc
#' @importFrom ggplot2 ggplot aes geom_line scale_color_manual expand_limits
#' scale_x_continuous scale_y_continuous labs theme_classic geom_hline
#' geom_segment theme ggsave margin expansion element_text
#' @importFrom ggtext element_markdown
#' @importFrom ggsci pal_npg
#' @importFrom showtext showtext_auto showtext_opts
#' @importFrom sysfonts font_add
#' @importFrom scales label_number
#' @importFrom dplyr filter arrange case_when if_else
#' @importFrom magrittr %>% %<>%
#' @importFrom utils head
#' @importFrom stats setNames
#' @section Font Information:
#' The MetroSans font included in this package is intended for academic research and non-commercial use only. For commercial use, please contact the font copyright holder.
#'
#' The font files are included in the package's inst/extdata directory and are automatically loaded for plotting.
#'
plot_gwas_power <- function(
trait_type = "bt",
n_cases= NULL,
n_controls= NULL,
sd_trait= NULL,
N= NULL,
maf_levels = c(0.01, 0.02, 0.05, 0.10, 0.20, 0.50),
or_range = seq(1.01, 2.00, 0.001),
effect_size= seq(0.01, 0.30, 0.001),
alpha = 5e-8,
plot_title = NULL,
save_plot = TRUE,
output_graphics = "png",
width = 17,
height = 9,
dpi = 600
) {
if (trait_type == "bt" & (is.null(n_cases) | is.null(n_controls))) {
stop("For binary traits, please provide n_cases and n_controls.")
} else if (trait_type == "qt" & (is.null(sd_trait) | is.null(N))) {
stop("For quantitative traits, please provide sd_trait and N.")
}
# --- 1. Setup: Fonts and Colors ---
font_dir <- system.file("extdata", package = "omixVizR")
if (!"MetroSans" %in% sysfonts::font_families()) {
sysfonts::font_add(
family = "MetroSans",
regular = file.path(font_dir, "MetroSans-Regular.ttf"),
bold = file.path(font_dir, "MetroSans-Bold.ttf"),
bolditalic = file.path(font_dir, "MetroSans-BoldItalic.ttf")
)
}
showtext::showtext_auto(enable = TRUE)
showtext::showtext_opts(dpi = 600)
output_dir = tempdir()
output_file <- file.path(output_dir, paste0("gwas_power_plot.", output_graphics))
plot_colors <- ggsci::pal_npg("nrc")(length(maf_levels))
# --- 2. Power Calculation ---
if (trait_type == "bt") {
total_n <- n_cases + n_controls
case_rate <- n_cases / total_n
power_data <- genpwr::genpwr.calc(
calc = "power", model = "logistic", ge.interaction = NULL,
N = total_n, Case.Rate = case_rate, k=NULL,
MAF = maf_levels, OR = or_range,
Alpha = alpha,
True.Model = "Additive", Test.Model = "Additive"
)
} else if (trait_type == "qt") {
power_data <- genpwr::genpwr.calc(
calc = "power", model = "linear", ge.interaction = NULL,
N = N, sd_y = sd_trait, k=NULL,
MAF = maf_levels, ES= effect_size,
Alpha = alpha,
True.Model = "Additive", Test.Model = "Additive"
)
}
# --- 3. Data Preparation for Plotting ---
maf_labels <- paste("MAF =", format(maf_levels))
power_data$MAF %<>% factor(levels = maf_levels, labels = maf_labels)
# --- 4. Dynamic Calculation for Annotations ---
# Find minimum effect size for 80% power at specific MAFs for annotations
data.table::setDT(power_data)
if (trait_type == "bt") {
power_data[, effect_size_value := OR]
} else if (trait_type == "qt") {
power_data[, effect_size_value := ES]
}
min_or_maf1 <- power_data %>%
dplyr::filter(`Power_at_Alpha_5e-08` >= 0.80) %>%
dplyr::arrange(MAF, effect_size_value) %>%
utils::head(1)
min_or_maf3 <- power_data %>%
dplyr::filter(`Power_at_Alpha_5e-08` >= 0.80, MAF == maf_labels[3]) %>%
dplyr::arrange(effect_size_value) %>%
utils::head(1)
# --- 5. Create Plot ---
# Set default title if not provided
if (is.null(plot_title)) {
if (trait_type == "bt") {
plot_title <- sprintf(
"Cases / Controls = %s / %s\nGWAS Significance Level = %s",
format(n_cases, big.mark = ","),
format(n_controls, big.mark = ","),
format(alpha, scientific = TRUE)
)
} else if (trait_type == "qt") {
plot_title <- sprintf(
"Sample Size = %s\nGWAS Significance Level = %s",
format(N, big.mark = ","),
format(alpha, scientific = TRUE)
)
}
} else {
if (trait_type == "bt") {
plot_title= sprintf(
paste(
plot_title, "= %s / %s\nGWAS Significance Level = %s"
),
format(n_cases, big.mark = ","),
format(n_controls, big.mark = ","),
format(alpha, scientific = TRUE)
)
} else if( trait_type == "qt") {
plot_title= sprintf(
paste(
plot_title, "= %s\nGWAS Significance Level = %s"
),
format(N, big.mark = ","),
format(alpha, scientific = TRUE)
)
}
}
if (trait_type == "qt") {
x_axis_label <- expression("Effect Size (" * beta * ")")
} else {
x_axis_label <- "Odds Ratio (OR)"
}
get_axis_step <- function(value) {
thresholds <- c(0.5, 1, 2, 3)
steps <- c(0.1, 0.2, 0.5, 1.0, 1.5)
idx <- findInterval(value, thresholds) + 1
return(steps[idx])
}
detect_decimal_places <- function(x) {
x_char <- format(x, scientific = FALSE)
decimal_places <- sapply(x_char, function(num) {
if (grepl("\\.", num)) {
decimal_part <- sub(".*\\.", "", num)
decimal_part <- sub("0+$", "", decimal_part)
nchar(decimal_part)
} else {
0
}
})
return(max(decimal_places))
}
# Define dynamic breaks and labels for the x-axis
if (trait_type == "bt") {
min_eff <- floor(min(as.numeric(or_range), na.rm = TRUE))
max_eff <- ceiling(max(as.numeric(or_range), na.rm = TRUE))
eff_range <- max(or_range) - min(or_range)
by_eff= get_axis_step(eff_range)
x_breaks <- sort(unique(c(seq(min_eff, max_eff, ifelse(by_eff==0.01, 0.1, by_eff)), min_or_maf3$effect_size_value, min_or_maf1$effect_size_value)))
decimal_places= detect_decimal_places(max(or_range))
multiplier <- 10^decimal_places
max_limit= ceiling(max(or_range) * multiplier) / multiplier
xaxis_limit= c(min_eff, max_limit)
} else if (trait_type == "qt") {
min_eff <- floor(min(as.numeric(effect_size), na.rm = TRUE))
max_eff <- ceiling(max(as.numeric(effect_size), na.rm = TRUE))
eff_range <- max(effect_size) - min(effect_size)
by_eff= get_axis_step(eff_range)
x_breaks <- sort(unique(c(seq(min_eff, max_eff, ifelse(by_eff==0.01, 0.1, by_eff)), min_or_maf3$effect_size_value, min_or_maf1$effect_size_value)))
decimal_places= detect_decimal_places(max(effect_size))
multiplier <- 10^decimal_places
max_limit= ceiling(max(effect_size) * multiplier) / multiplier
xaxis_limit= c(min_eff, max_limit)
}
x_labels <- function(x) {
dplyr::case_when(
x == min_or_maf3$effect_size_value ~ paste0("<span style='color:", plot_colors[3], "'>", sprintf("%.2f", x), "</span>"),
x == min_or_maf1$effect_size_value ~ paste0("<span style='color:", plot_colors[1], "'>", sprintf("%.2f", x), "</span>"),
TRUE ~ paste0("<span style='color:black'>", scales::label_number(accuracy = 0.01)(x), "</span>")
)
}
p <- ggplot2::ggplot(power_data, ggplot2::aes(x = effect_size_value, y = `Power_at_Alpha_5e-08`)) +
ggplot2::geom_line(ggplot2::aes(color = MAF), linewidth = 1.28) +
ggplot2::scale_color_manual(
values = stats::setNames(plot_colors, levels(power_data$MAF)),
name = "Minor Allele Frequency (MAF)",
guide = ggplot2::guide_legend(override.aes = list(linewidth = 2.8))
) +
ggplot2::expand_limits(x = xaxis_limit, y = c(0, 1)) +
ggplot2::scale_x_continuous(breaks = x_breaks, labels = x_labels) +
ggplot2::scale_y_continuous(
breaks = seq(0, 1.0, 0.2),
expand = ggplot2::expansion(mult = c(0, 0.08), add = c(0, 0)),
labels = function(x) dplyr::if_else(x == 0, "0", sprintf("%.2f", x))
) +
ggplot2::labs(
x = x_axis_label,
y = "GWAS Statistical Power",
title = plot_title
) +
ggplot2::theme_classic() +
ggplot2::geom_hline(yintercept = 0.80, linewidth = 0.88, linetype = "dashed") +
# Dashed lines for specific ORs
ggplot2::geom_segment(
data = min_or_maf3,
ggplot2::aes(x = effect_size_value, xend = effect_size_value, y = 0, yend = 0.80),
linewidth = 0.88, linetype = "dashed", color = plot_colors[3]
) +
ggplot2::geom_segment(
data = min_or_maf1,
ggplot2::aes(x = effect_size_value, xend = effect_size_value, y = 0, yend = 0.80),
linewidth = 0.88, linetype = "dashed", color = plot_colors[1]
) +
ggplot2::theme(
text = ggplot2::element_text(family = "MetroSans", size = 21),
axis.title = ggplot2::element_text(face = "bold"),
axis.text.y = ggplot2::element_text(size = 18, colour = "black"),
axis.text.x = ggtext::element_markdown(size = 18),
legend.position = "inside",
legend.position.inside = c(0.88, 0.40),
plot.title = ggplot2::element_text(hjust = 0.5, face = "bold", size = 24, vjust = -0.8),
plot.title.position = "plot",
plot.margin = ggplot2::margin(t = 15, r = 10, b = 10, l = 10, unit = "pt")
)
if (trait_type == "qt"){
p= p + ggplot2::theme(axis.title.x = ggplot2::element_text(family = "sans", face = "bold"))
}
# --- 6. Save Plot (if requested) ---
if (save_plot) {
ggplot2::ggsave(
filename = output_file,
plot = p,
width = width, height = height, dpi = dpi
)
message("GWAS power plot saved to: ", output_file)
}
# --- 7. Return Results ---
return(invisible(list(plot = p, power_data = data.table::as.data.table(power_data))))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.