diff --git a/NEWS.md b/NEWS.md index 4b561b7..de33d3d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,13 @@ ## New features +- New argument `scale.square` to size the squares by the absolute correlation + when `method = "square"`: `ggcorrplot(corr, scale.square = TRUE)` scales each + square (larger = stronger correlation) on top of the fill color, the classic + corrplot size-scaled square look. Defaults to `FALSE` (constant full-cell + squares, unchanged); has no effect for `method = "circle"` (circles are always + sized). + - New argument `preset` for publication-grade output in one token: `ggcorrplot(corr, preset = "publication")` sets white cell outlines and the colorblind-safe `"RdBu"` palette. It only fills arguments you did not supply, so diff --git a/R/ggcorrplot.R b/R/ggcorrplot.R index cd3e5b5..7c4bc14 100644 --- a/R/ggcorrplot.R +++ b/R/ggcorrplot.R @@ -6,6 +6,14 @@ #' @param corr the correlation matrix to visualize #' @param method character, the visualization method of correlation matrix to be #' used. Allowed values are "square" (default), "circle". +#' @param scale.square logical. If \code{TRUE} and \code{method = "square"}, the +#' squares are sized by the absolute correlation (larger square = stronger +#' correlation), in addition to the fill color -- the classic corrplot +#' size-scaled square look. Defaults to \code{FALSE} (constant full-cell +#' squares, the current behavior). Has no effect for \code{method = "circle"} +#' (circles are always sized). Uses \code{circle.scale} to tune the size range. +#' As with \code{method = "circle"}, coefficient labels (\code{lab = TRUE}) are +#' drawn at full size and may overflow the smallest squares. #' @param lower.method,upper.method character, an optional per-triangle glyph for #' a mixed layout: one of "square", "circle" or "number" (the coefficient drawn #' as text, colored by its value on the same scale as the fill, so coefficients @@ -258,7 +266,8 @@ ggcorrplot <- function(corr, hc.rect = NULL, palette = NULL, preset = NULL, - hc.rect.col = "gray30") { + hc.rect.col = "gray30", + scale.square = FALSE) { type <- match.arg(type) method <- match.arg(method) # Resolve on insig[1] (not match.arg(insig)) so a caller passing the old @@ -396,7 +405,8 @@ ggcorrplot <- function(corr, corr, lower.method, upper.method, outline.color = outline.color, circle.scale = circle.scale, lab_size = lab_size, tl.cex = tl.cex, tl.col = tl.col, - digits = digits, nsmall = nsmall, leading.zero = leading.zero + digits = digits, nsmall = nsmall, leading.zero = leading.zero, + scale.square = scale.square ) } else { p <- @@ -407,7 +417,10 @@ ggcorrplot <- function(corr, # modification based on method (extracted so the mixed-method path can request # a different glyph per triangle from the same builder, P0.0) - p <- p + .method_layer(method, outline.color = outline.color, circle.scale = circle.scale) + p <- p + .method_layer(method, + outline.color = outline.color, circle.scale = circle.scale, + scale.square = scale.square + ) } # adding colors @@ -798,9 +811,22 @@ cor_pmat <- function(x, ..., use = c("pairwise.complete.obs", "everything")) { # Build the glyph layer(s) for a given method. Returns a list of ggplot # components so the caller can add them with a single `+` (and so the # mixed-method path can request a different glyph per triangle, P0.0). -.method_layer <- function(method, outline.color, circle.scale) { - if (method == "square") { +.method_layer <- function(method, outline.color, circle.scale, scale.square = FALSE) { + if (method == "square" && !scale.square) { list(ggplot2::geom_tile(color = outline.color)) + } else if (method == "square" && scale.square) { + # size-scaled squares (corrplot-style): a filled square (shape 22) whose size + # encodes |r|, mirroring the sized-circle glyph. Only reached when the caller + # opts in with scale.square = TRUE, so the default square heatmap is unchanged. + list( + ggplot2::geom_point( + color = outline.color, + shape = 22, + ggplot2::aes(size = .data[["abs_corr"]]) + ), + ggplot2::scale_size(range = c(4, 10) * circle.scale), + ggplot2::guides(size = "none") + ) } else if (method == "circle") { list( ggplot2::geom_point( @@ -850,7 +876,7 @@ cor_pmat <- function(x, ..., use = c("pairwise.complete.obs", "everything")) { # variable name. Returns a list of ggplot components. .mixed_layers <- function(df, lower.method, upper.method, outline.color, circle.scale, lab_size, tl.cex, tl.col, - digits, nsmall, leading.zero) { + digits, nsmall, leading.zero, scale.square = FALSE) { xi <- as.integer(df$Var1) yi <- as.integer(df$Var2) regions <- list( @@ -860,19 +886,26 @@ cor_pmat <- function(x, ..., use = c("pairwise.complete.obs", "everything")) { ) layers <- list() - has_circle <- FALSE + has_sized <- FALSE for (r in regions) { d <- r$data if (nrow(d) == 0) next m <- r$method - if (m == "square") { + if (m == "square" && !scale.square) { layers <- c(layers, list(ggplot2::geom_tile( data = d, mapping = ggplot2::aes(fill = .data[["value"]]), color = outline.color ))) + } else if (m == "square" && scale.square) { + has_sized <- TRUE + layers <- c(layers, list(ggplot2::geom_point( + data = d, + mapping = ggplot2::aes(fill = .data[["value"]], size = .data[["abs_corr"]]), + color = outline.color, shape = 22 + ))) } else if (m == "circle") { - has_circle <- TRUE + has_sized <- TRUE layers <- c(layers, list(ggplot2::geom_point( data = d, mapping = ggplot2::aes(fill = .data[["value"]], size = .data[["abs_corr"]]), @@ -895,7 +928,7 @@ cor_pmat <- function(x, ..., use = c("pairwise.complete.obs", "everything")) { } } - if (has_circle) { + if (has_sized) { layers <- c(layers, list( ggplot2::scale_size(range = c(4, 10) * circle.scale), ggplot2::guides(size = "none") diff --git a/man/ggcorrplot.Rd b/man/ggcorrplot.Rd index 836c2c3..ae8c58f 100644 --- a/man/ggcorrplot.Rd +++ b/man/ggcorrplot.Rd @@ -46,7 +46,8 @@ ggcorrplot( hc.rect = NULL, palette = NULL, preset = NULL, - hc.rect.col = "gray30" + hc.rect.col = "gray30", + scale.square = FALSE ) cor_pmat(x, ..., use = c("pairwise.complete.obs", "everything")) @@ -210,6 +211,15 @@ Defaults to \code{"gray30"}; set it to any color that suits your palette (e.g. \code{"black"} for a bolder box, or \code{"white"}). Only used when \code{hc.rect} is set.} +\item{scale.square}{logical. If \code{TRUE} and \code{method = "square"}, the +squares are sized by the absolute correlation (larger square = stronger +correlation), in addition to the fill color -- the classic corrplot +size-scaled square look. Defaults to \code{FALSE} (constant full-cell +squares, the current behavior). Has no effect for \code{method = "circle"} +(circles are always sized). Uses \code{circle.scale} to tune the size range. +As with \code{method = "circle"}, coefficient labels (\code{lab = TRUE}) are +drawn at full size and may overflow the smallest squares.} + \item{x}{numeric matrix or data frame} \item{...}{other arguments to be passed to the function cor.test.} diff --git a/tests/testthat/_snaps/vdiffr/ggcorrplot-works-scale-square.svg b/tests/testthat/_snaps/vdiffr/ggcorrplot-works-scale-square.svg new file mode 100644 index 0000000..4252d6e --- /dev/null +++ b/tests/testthat/_snaps/vdiffr/ggcorrplot-works-scale-square.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +mpg +cyl +disp +hp +drat +wt +qsec +vs +am +gear +carb +Corr + + + + + + + + + + + +-1.0 +-0.5 +0.0 +0.5 +1.0 +ggcorrplot works - scale square + + diff --git a/tests/testthat/test-scale-square.R b/tests/testthat/test-scale-square.R new file mode 100644 index 0000000..e1b7d5a --- /dev/null +++ b/tests/testthat/test-scale-square.R @@ -0,0 +1,49 @@ +# scale.square: size-scaled squares (corrplot-style) for method = "square". +# Default FALSE keeps method = "square" a constant full-cell geom_tile. + +corr <- round(cor(mtcars), 1) +geom1 <- function(p) class(p$layers[[1]]$geom)[1] +shape1 <- function(p) p$layers[[1]]$aes_params$shape + +test_that("scale.square defaults to FALSE: method = 'square' stays a full tile", { + p <- ggcorrplot(corr) + expect_identical(geom1(p), "GeomTile") + # explicit FALSE == implicit default (byte-identical built data) + expect_equal( + ggplot2::ggplot_build(ggcorrplot(corr))$data, + ggplot2::ggplot_build(ggcorrplot(corr, scale.square = FALSE))$data + ) +}) + +test_that("scale.square = TRUE draws size-scaled squares (shape 22, sized by |r|)", { + p <- ggcorrplot(corr, scale.square = TRUE) + expect_identical(geom1(p), "GeomPoint") + expect_identical(shape1(p), 22) + # the point size maps to abs_corr (magnitude), and there is a size scale + b <- ggplot2::ggplot_build(p) + expect_true("size" %in% names(b$data[[1]])) + # stronger correlations -> larger size than weak ones + pd <- b$plot$data + szmap <- b$data[[1]]$size + expect_gt(max(szmap), min(szmap)) # sizes actually vary +}) + +test_that("scale.square has no effect for method = 'circle'", { + a <- ggcorrplot(corr, method = "circle") + b <- ggcorrplot(corr, method = "circle", scale.square = TRUE) + expect_equal(shape1(a), 21) + expect_equal(shape1(b), 21) # still circles, unchanged + expect_equal( + ggplot2::ggplot_build(a)$data, + ggplot2::ggplot_build(b)$data + ) +}) + +test_that("scale.square sizes the square region in a mixed layout", { + # a mixed layout with a 'square' triangle -> that triangle becomes sized squares + p <- ggcorrplot(corr, lower.method = "square", upper.method = "number", scale.square = TRUE) + geoms <- vapply(p$layers, function(l) class(l$geom)[1], "") + expect_true("GeomPoint" %in% geoms) # the square triangle is now a sized point layer + sq <- p$layers[[which(geoms == "GeomPoint")[1]]] + expect_identical(sq$aes_params$shape, 22) +}) diff --git a/tests/testthat/test-vdiffr.R b/tests/testthat/test-vdiffr.R index 209a902..dfab78b 100644 --- a/tests/testthat/test-vdiffr.R +++ b/tests/testthat/test-vdiffr.R @@ -72,5 +72,11 @@ if (getRversion() >= "4.1") { title = "ggcorrplot works - preset publication", fig = ggcorrplot(corr, preset = "publication", lab = TRUE) ) + + set.seed(123) + vdiffr::expect_doppelganger( + title = "ggcorrplot works - scale square", + fig = ggcorrplot(corr, scale.square = TRUE, outline.color = "white") + ) }) } diff --git a/vignettes/publication-ready-correlation-plots.Rmd b/vignettes/publication-ready-correlation-plots.Rmd index 3544ab0..8f4d584 100644 --- a/vignettes/publication-ready-correlation-plots.Rmd +++ b/vignettes/publication-ready-correlation-plots.Rmd @@ -84,6 +84,16 @@ familiar corrplot look, drawn in ggplot2. ggcorrplot(corr, method = "circle", hc.order = TRUE, type = "upper", outline.color = "white") ``` +# Size-scaled squares + +`scale.square = TRUE` sizes the squares by the absolute correlation, so magnitude is encoded by both area +and color at once — the classic corrplot square look. Near-zero cells shrink to small squares while the +strong correlations dominate, which reads well for large matrices and for colorblind viewers. + +```{r scale-square, fig.width = 6, fig.height = 5.4} +ggcorrplot(corr, scale.square = TRUE, hc.order = TRUE, outline.color = "white") +``` + # A colorblind-safe palette `preset = "publication"` is a one-token beautiful default (white separators + the colorblind-safe RdBu