Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plotLoadings modifications #153

Merged
merged 8 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ importFrom(ape,rotateConstr)
importFrom(dplyr,"%>%")
importFrom(dplyr,all_of)
importFrom(dplyr,bind_cols)
importFrom(dplyr,case_when)
importFrom(dplyr,filter)
importFrom(dplyr,group_by)
importFrom(dplyr,last_col)
Expand All @@ -93,6 +94,8 @@ importFrom(ggplot2,geom_bar)
importFrom(ggplot2,geom_line)
importFrom(ggplot2,geom_point)
importFrom(ggplot2,geom_raster)
importFrom(ggplot2,geom_segment)
importFrom(ggplot2,geom_text)
importFrom(ggplot2,geom_tile)
importFrom(ggplot2,ggplot)
importFrom(ggplot2,labs)
Expand Down
203 changes: 144 additions & 59 deletions R/plotLoadings.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
#' plot.
#'
#' @param layout \code{Character scalar}. Determines the layout of plot. Must be
#' either \code{"barplot"} or \code{"heatmap"}. (Default: \code{"barplot"})
#' either \code{"barplot"}, \code{"heatmap"}, or \code{"lollipop"}.
#' (Default: \code{"barplot"})
#'
#' @param ncomponents \code{Numeric scalar}. Number of components must be lower
#' or equal to the number of components chosen in the reduction method.
Expand All @@ -29,9 +30,9 @@
#' \item \code{n}: \code{Integer scalar}. Number of features to be plotted.
#' Applicable when \code{layout="barplot"}. (Default: \code{10}))
#'
#' \item \code{absolute.scale}: ("barplot") \code{Logical scalar}. Specifies
#' whether a barplot should be visualized in absoltue scale.
#' (Default: \code{TRUE})
#' \item \code{absolute.scale}: ("barplot", "lollipop") \code{Logical scalar}.
#' Specifies whether a barplot or a lollipop plot should be visualized in
#' absolute scale. (Default: \code{TRUE})
#' }
#'
#' @details
Expand Down Expand Up @@ -203,7 +204,8 @@ setMethod("plotLoadings", signature = c(x = "matrix"),
# This functions checks that loadings matrix is correct
.check_loadings_matrix <- function(mat, layout, ncomponents, n = 10, ...) {
# Check layout
if( !(.is_a_string(layout) && layout %in% c("barplot", "heatmap")) ){
if( !(.is_a_string(layout) && layout %in%
c("barplot", "heatmap", "lollipop")) ){
stop("'layout' must be 'barplot' or 'heatmap',", call. = FALSE)
}
# Check n
Expand Down Expand Up @@ -232,7 +234,7 @@ setMethod("plotLoadings", signature = c(x = "matrix"),
# Keep only the number of components needed
df <- df[ , seq_len(ncomponents), drop = FALSE]
# If the layout is barplot, choose top features for each component
if( layout %in% c("barplot") ){
if( layout %in% c("barplot", "lollipop") ){
res <- lapply(seq_len(ncomponents), .process_component, df = df, n = n)
# Combine to single data.frame
res <- do.call(rbind, res)
Expand All @@ -248,11 +250,13 @@ setMethod("plotLoadings", signature = c(x = "matrix"),
}
# Convert into data.frame
res <- as.data.frame(res)
# Add column that shows the values in absolute scale, and another column
# showing sign
res[["Value_abs"]] <- abs(res[["Value"]])
res[["Sign"]] <- ifelse(
res[["Value"]] > 0, "+", ifelse(res[["Value"]] < 0, "-", ""))
# Check that values are numeric. This is the first time we test that the
# columns were numeric. Now all the values from columns are in this column.
if( !is.numeric(res[["Value"]]) ){
stop("Values must be numeric.", call. = FALSE)
}
# Calculate max and min values along with maximum absolute value and sign
res <- .calculate_max_and_min_for_loadings(res)
return(res)
}

Expand All @@ -275,18 +279,43 @@ setMethod("plotLoadings", signature = c(x = "matrix"),
return(df)
}

# This function calculates place for +/- sign in barplot/lollipop plot
#' @importFrom dplyr %>% group_by mutate case_when ungroup
.calculate_max_and_min_for_loadings <- function(df){
# Add column that shows the values in absolute scale, and another column
# showing sign
df[["Value_abs"]] <- abs(df[["Value"]])
df[["Sign"]] <- ifelse(
df[["Value"]] > 0, "+", ifelse(df[["Value"]] < 0, "-", ""))
# Add maximum values. This is used in scaling and placement of +/- sign
# in barplot and lollipop plot. In absolute scale, we use the maximum
# absolute value. In original scale, negative values gets minimum value
# and positive values maximum. These values are for each PC.
df <- df %>%
group_by(PC) %>%
mutate(
# Calculate max of abs(Value) and add 10%
max_scale_abs = max(abs(Value), na.rm = TRUE) +
0.1 * max(abs(Value), na.rm = TRUE),
# Calculate max_scale based on the sign of the Value
max_scale = case_when(
Value < 0 ~ min(Value, na.rm = TRUE) +
0.1 * min(Value, na.rm = TRUE),
Value > 0 ~ max(Value, na.rm = TRUE) +
0.1 * max(Value, na.rm = TRUE),
TRUE ~ NA_real_
)
) %>%
ungroup()
return(df)
}

# This functions plots a data.frame in barplot or heatmap layout.
#' @importFrom tidytext scale_y_reordered reorder_within
#' @importFrom ggplot2 geom_tile scale_fill_gradient2 geom_bar
.plot_loadings <- function(df, layout, absolute.scale = TRUE, ...) {
#
if( !.is_a_bool(absolute.scale) ){
stop("'absolute.scale' must be TRUE or FALSE.", call. = FALSE)
}
#
#' @importFrom ggplot2 geom_tile scale_fill_gradient2
.plot_loadings <- function(df, layout, ...) {
# Initialize a plot
plot_out <- ggplot(df)
# Either create a heatmap or barplt
# Either create a heatmap or barplot/lollipop
if( layout == "heatmap" ){
plot_out <- plot_out +
# Create a heatmap
Expand All @@ -296,54 +325,110 @@ setMethod("plotLoadings", signature = c(x = "matrix"),
) +
# Adjust color scale
scale_fill_gradient2(
limits = c(-1, 1),
low = "darkslateblue", mid = "white", high = "darkred"
limits = c(-max(abs(df$Value)), max(abs(df$Value))),
low = "darkblue", mid = "white", high = "darkred"
)

} else if( layout == "barplot" && !absolute.scale ){
# This creates a barplot where values can be negative or positive
# (bars can be in negative and positive side)
plot_out <- plot_out +
# Create a bar plot. Create unique facets for each PC. Each PC can
# have unique set of features. To reorder features by each facet,
# we use reorder_within() and scale_y_reordered().
geom_bar(
mapping = aes(
x = Value, y = reorder_within(Feature, Value, PC)),
stat = "identity"
) +
scale_y_reordered() +
facet_wrap(~ PC, scales = "free") +
labs(x = "Value", y = "Feature")

} else if( layout == "barplot" && absolute.scale ){
# This creates a barplot where bars are in absolute scale and the sing
# is denoted with +/-
plot_out <- plot_out +
# Create bars with absolute scale
geom_bar(
mapping = aes(
x = Value_abs, y = reorder_within(Feature, -Value_abs, PC)),
stat = "identity"
) +
# Add sign that tells whether the value is + or -
geom_text(aes(
x = max(Value_abs) + max(Value_abs)*0.1,
y = reorder_within(Feature, Value_abs, PC),
label = Sign,
fontface = "bold"
)) +
scale_y_reordered() +
facet_wrap(~ PC, scales = "free") +
labs(x = "Value", y = "Feature")

} else if( layout %in% c("barplot", "lollipop") ){
plot_out <- .plot_bar_or_lollipop(plot_out, df, layout, ...)
}
# Adjust theme
plot_out <- plot_out +
theme_minimal()
return(plot_out)
}

# This functions creates a barplot or lollipop plot.
#' @importFrom tidytext scale_y_reordered reorder_within
#' @importFrom ggplot2 geom_bar geom_segment geom_point geom_text
.plot_bar_or_lollipop <- function(
plot_out, df, layout, absolute.scale = TRUE, show.color = TRUE,
show.sign = FALSE, ...){
#
if( !.is_a_bool(absolute.scale) ){
stop("'absolute.scale' must be TRUE or FALSE.", call. = FALSE)
}
#
if( !.is_a_bool(show.color) ){
stop("'show.color' must be TRUE or FALSE.", call. = FALSE)
}
#
if( !.is_a_bool(show.sign) ){
stop("'show.sign' must be TRUE or FALSE.", call. = FALSE)
}
#
# Set the variables to use for aesthetics
value_var <- if (absolute.scale) "Value_abs" else "Value"
# Set the y aesthetics with reorder_within, making sure 'df' is referenced
y_aes <- reorder_within(
df$Feature,
# Either get values in absolute scale or not
if(absolute.scale) -df$Value_abs else df$Value,
df$PC
)

# Plot barplot or lollipop
if (layout == "barplot") {
# This creates a barplot
aesthetic <- aes(
x = !!sym(value_var),
y = y_aes,
# User can decide whether the bars are colored based on +/-
fill = if(show.color) Sign else NULL
)
plot_out <- plot_out + geom_bar(mapping = aesthetic, stat = "identity")
} else if (layout == "lollipop") {
# This creates a lollipop plot
plot_out <- plot_out +
# Add line
geom_segment(mapping = aes(
x = 0, xend = !!sym(value_var),
y = y_aes, yend = y_aes
)) +
# Add point at the end of the line to create "lollipop"
geom_point(mapping = aes(
x = !!sym(value_var),
y = y_aes,
# User can choose whether the point is colored based on sign
color = if (show.color) Sign else NULL
))
}

# Add sign labels if needed
if( show.sign ){
plot_out <- plot_out + geom_text(aes(
# This determines where the sign is placed, absolute scale or not
x = if (absolute.scale) max_scale_abs else max_scale,
y = y_aes,
label = Sign,
fontface = "bold"
))
}

# Customize the legend for Sign as "Effect"
if( show.color ) {
# Get correct function, barplot uses fill, lollipop color
scale_FUN <- if( layout == "barplot" ) scale_fill_manual else
scale_color_manual
# Currently the legend has title that shows the function call and the
# values shows + or -. Make the legend nicer.
plot_out <- plot_out +
scale_FUN(
name = "Effect",
values = c("+" = "blue", "-" = "red"),
labels = c("+" = "positive", "-" = "negative")
)
}

# Final wrangle, set facets and order the data
plot_out <- plot_out +
scale_y_reordered() +
facet_wrap(~PC, scales = "free") +
labs(x = "Value", y = "Feature")

return(plot_out)
}

# This function retrieves the data for tree + heatmap plotting. The output
# is a list that includes tree and data.frame in wide format.
#' @importFrom ggtree ggtree
Expand Down
9 changes: 5 additions & 4 deletions man/plotLoadings.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions tests/testthat/test-plotLoadings.R
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,52 @@ test_that("plot Loadings", {
expect_s3_class(p, "ggplot")
p <- plotLoadings(tse, dimred = "PCA", layout = "heatmap", add.tree = TRUE)
expect_s3_class(p, "ggplot")

# Create a mock dataset
df <- data.frame(
Feature = rep(c("Feature1", "Feature2", "Feature3"), times = 2),
Value = c(2, 4, -1, -3, 5, 7),
Value_abs = abs(c(2, 4, -1, -3, 5, 7)),
Sign = c("+", "+", "-", "-", "+", "+"),
PC = rep(c("PC1", "PC2"), each = 3)
)

# Create an empty ggplot object for plot_out
plot_out <- ggplot(df)
### 1). TEST: barplot with absolute scale and color
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = TRUE, show.color = TRUE)
expect_s3_class(plot, "ggplot")
### 2). TEST: barplot without absolute scale but with color
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = FALSE, show.color = TRUE)
expect_s3_class(plot, "ggplot")
### 3). TEST: barplot with absolute scale but no color
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = TRUE, show.color = FALSE)
expect_s3_class(plot, "ggplot")
### 4). TEST: lollipop plot with absolute scale and color
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "lollipop", absolute.scale = TRUE, show.color = TRUE)
expect_s3_class(plot, "ggplot")
### 5). TEST: lollipop plot without absolute scale but with color
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "lollipop", absolute.scale = FALSE, show.color = TRUE)
expect_s3_class(plot, "ggplot")
### 6). TEST: lollipop plot with absolute scale but no color
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "lollipop", absolute.scale = TRUE, show.color = FALSE)
expect_s3_class(plot, "ggplot")
### 7). TEST: error when `absolute.scale` is not a boolean
expect_error(
.plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = "not boolean", show.color = TRUE),
"'absolute.scale' must be TRUE or FALSE."
)
### 8). TEST: error when `show.color` is not a boolean
expect_error(
.plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = TRUE, show.color = "not boolean"),
"'show.color' must be TRUE or FALSE."
)
### 9). TEST: correct labels in the legend
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = TRUE, show.color = TRUE)
expect_true("Effect" %in% ggplot_build(plot)$plot$scales$scales[[1]]$name)
expect_equal(ggplot_build(plot)$plot$scales$scales[[1]]$labels, c("+" = "positive", "-" = "negative"))
### 10). TEST: adding sign labels
plot <- .plot_bar_or_lollipop(plot_out, df, layout = "barplot", absolute.scale = TRUE, show.color = TRUE, show.sign = TRUE)
expect_s3_class(plot, "ggplot")
})

Loading