diff --git a/DESCRIPTION b/DESCRIPTION index b6395b2..3bd7bb0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: thisplot Title: Utility Functions for Plotting -Version: 0.3.8 -Date: 2026-04-23 +Version: 0.3.10 +Date: 2026-05-28 Authors@R: c( person("Meng", "Xu", email = "mengxu98@qq.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-8300-1054")), @@ -50,7 +50,8 @@ Suggests: scales, stringr, testthat (>= 3.0.0), - tidyr + tidyr, + venny Config/Needs/website: mengxu98/thistemplate Config/testthat/edition: 3 Encoding: UTF-8 diff --git a/NEWS.md b/NEWS.md index 728dbf8..a8d0590 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,12 +1,14 @@ -# thisplot 0.3.9 +# thisplot 0.3.10 * **feat**: * `DimDataPlot()`: New lightweight helper for plotting supplied 2D coordinates from data frames, with grouping, splitting, shape mapping, labels, palettes, theme controls, and optional group marks. * `StatPlot()`: Added configurable major panel grid lines via `grid_major`, `grid_major_colour`, `grid_major_linetype`, and `grid_major_linewidth`. + * `StatPlot()`: Added optional `venn_engine = "venny"` support for more flexible 2-4 set Venn diagrams while keeping `ggVennDiagram` as the default. * **docs**: * Added documentation and examples for `DimDataPlot()`. * Updated `StatPlot()` documentation for the new major grid controls. + * Added `StatPlot()` documentation and examples for the optional `venny` Venn backend. # thisplot 0.3.8 diff --git a/R/StatPlot.R b/R/StatPlot.R index 44aa11d..2a65ed5 100644 --- a/R/StatPlot.R +++ b/R/StatPlot.R @@ -24,6 +24,14 @@ #' Can be one of `"bar"`, `"rose"`, `"ring"`, `"pie"`, `"trend"`, #' `"trend_alluvial"`, `"area"`, `"dot"`, `"sankey"`, `"chord"`, #' `"venn"`, or `"upset"`. +#' @param venn_engine The engine used when `plot_type = "venn"`. +#' Can be one of `"ggVennDiagram"` or `"venny"`. +#' Default is `"ggVennDiagram"`. +#' The `"venny"` engine supports 2 to 4 sets. +#' @param venn_args A list of additional arguments passed to +#' [venny::venny()] when `plot_type = "venn"` and +#' `venn_engine = "venny"`. +#' The `data` argument is generated internally and cannot be supplied here. #' @param stat_type The type of statistic to compute for the plot. #' Can be one of `"percent"` or `"count"`. #' @param position The position adjustment for the plot. @@ -164,6 +172,18 @@ #' group.by = "Group", #' plot_type = "trend_alluvial" #' ) +#' +#' meta_data$A <- meta_data$Type == "A" +#' meta_data$B <- meta_data$Group == "X" +#' meta_data$C <- meta_data$Batch == "B1" +#' StatPlot( +#' meta_data, +#' stat.by = c("A", "B", "C"), +#' stat_level = TRUE, +#' plot_type = "venn", +#' venn_engine = "venny", +#' venn_args = list(detail = TRUE) +#' ) StatPlot <- function( meta.data, stat.by, @@ -190,6 +210,8 @@ StatPlot <- function( "venn", "upset" ), + venn_engine = c("ggVennDiagram", "venny"), + venn_args = list(), stat_type = c("percent", "count"), position = c("stack", "dodge"), palette = "Chinese", @@ -227,7 +249,9 @@ StatPlot <- function( stat_type <- match.arg(stat_type) plot_type <- match.arg(plot_type) + venn_engine <- match.arg(venn_engine) position <- match.arg(position) + venn_args <- venn_args %||% list() if (identical(plot_type, "trend_alluvial")) { check_r("ggalluvial", verbose = FALSE) @@ -343,6 +367,27 @@ StatPlot <- function( ) } } + if (identical(plot_type, "venn") && identical(venn_engine, "venny")) { + if (!is.list(venn_args)) { + log_message( + "{.arg venn_args} must be a list", + message_type = "error" + ) + } + if ("data" %in% names(venn_args)) { + log_message( + "{.arg venn_args} cannot contain {.arg data}; it is generated internally", + message_type = "error" + ) + } + if (length(stat.by) < 2 || length(stat.by) > 4) { + log_message( + "{.arg stat.by} must contain 2 to 4 sets when {.arg venn_engine = 'venny'}", + message_type = "error" + ) + } + check_r("venny", verbose = FALSE) + } levels <- unique( unlist( @@ -982,6 +1027,7 @@ StatPlot <- function( colors <- palette_colors(stat.by, palette = palette, palcolor = palcolor) nlev <- nlevels(dat_all[[split.by]]) chord_use_temp <- plot_type == "chord" && isTRUE(combine) && nlev > 1L + venny_details <- list() if (plot_type == "chord" && isTRUE(combine)) { if (chord_use_temp) { temp <- tempfile(fileext = "png") @@ -1019,101 +1065,136 @@ StatPlot <- function( cellkeep } ) - venn <- ggVennDiagram::Venn(dat_list) - data <- ggVennDiagram::process_data(venn) - dat_venn_region <- ggVennDiagram::venn_region(data) - idname <- dat_venn_region[["name"]][ - dat_venn_region[["name"]] %in% stat.by - ] - names(idname) <- dat_venn_region[["id"]][ - dat_venn_region[["name"]] %in% stat.by - ] - idcomb <- strsplit(dat_venn_region[["id"]], split = "") - colorcomb <- lapply(idcomb, function(x) colors[idname[as.character(x)]]) - dat_venn_region[["colors"]] <- sapply( - colorcomb, - function(x) blendcolors(x, mode = "blend") - ) - dat_venn_region[["label"]] <- paste0( - dat_venn_region[["count"]], - "\n", - round( - dat_venn_region[["count"]] / sum(dat_venn_region[["count"]]) * 100, - 1 - ), - "%" - ) - dat_venn_setedge <- ggVennDiagram::venn_setedge(data) - dat_venn_setedge[["colors"]] <- colors[stat.by[as.numeric( - dat_venn_setedge[["id"]] - )]] - - venn_regionedge_data <- ggVennDiagram::venn_regionedge(data) - venn_regionedge_data[["colors"]] <- dat_venn_region[["colors"]][match( - venn_regionedge_data[["id"]], - dat_venn_region[["id"]] - )] - - p <- ggplot() + - geom_polygon( - data = venn_regionedge_data, - aes(X, Y, fill = colors, group = id), - alpha = alpha - ) + - geom_path( - data = dat_venn_setedge, - aes(X, Y, group = id), - color = "black", - linewidth = 1, - show.legend = FALSE - ) + - ggrepel::geom_text_repel( - data = ggVennDiagram::venn_setlabel(data), - aes( - X, - Y, - label = paste0( - name, - "\n(", - count, - ")" - ) + if (identical(venn_engine, "venny")) { + venny_result <- do.call( + venny::venny, + c(list(data = dat_list), venn_args) + ) + venny_detail <- NULL + if ( + is.list(venny_result) && + !inherits(venny_result, c("gg", "ggplot")) && + !is.null(venny_result[["venn"]]) + ) { + venny_detail <- venny_result + p <- venny_result[["venn"]] + } else { + p <- venny_result + } + if (!inherits(p, c("gg", "ggplot"))) { + log_message( + "{.fun venny::venny} must return a ggplot object or a list containing {.arg venn}", + message_type = "error" + ) + } + p <- p + labs(x = sp, title = title, subtitle = subtitle) + if (!is.null(venny_detail)) { + attr(p, "venny_detail") <- venny_detail + venny_details[[sp]] <- venny_detail + } + } else { + venn <- ggVennDiagram::Venn(dat_list) + data <- ggVennDiagram::process_data(venn) + dat_venn_region <- ggVennDiagram::venn_region(data) + idname <- dat_venn_region[["name"]][ + dat_venn_region[["name"]] %in% stat.by + ] + names(idname) <- dat_venn_region[["id"]][ + dat_venn_region[["name"]] %in% stat.by + ] + idcomb <- strsplit(dat_venn_region[["id"]], split = "") + colorcomb <- lapply( + idcomb, + function(x) colors[idname[as.character(x)]] + ) + dat_venn_region[["colors"]] <- sapply( + colorcomb, + function(x) blendcolors(x, mode = "blend") + ) + dat_venn_region[["label"]] <- paste0( + dat_venn_region[["count"]], + "\n", + round( + dat_venn_region[["count"]] / + sum(dat_venn_region[["count"]]) * 100, + 1 ), - fontface = "bold", - colour = label.fg, - size = label.size + 0.5, - bg.color = label.bg, - bg.r = label.bg.r, - point.size = NA, - max.overlaps = 100, - force = 0, - min.segment.length = 0, - segment.colour = "black" - ) + - ggrepel::geom_text_repel( - data = ggVennDiagram::venn_regionlabel(data), - aes(X, Y, label = count), - colour = label.fg, - size = label.size, - bg.color = label.bg, - bg.r = label.bg.r, - point.size = NA, - max.overlaps = 100, - force = 0, - min.segment.length = 0, - segment.colour = "black" - ) + - scale_fill_identity() + - coord_fixed(ratio = 1, clip = "off") + - theme( - plot.title = element_text(hjust = 0.5), - plot.background = element_blank(), - panel.background = element_blank(), - axis.title.y = element_blank(), - axis.text = element_blank(), - axis.ticks = element_blank() + "%" ) - p <- p + labs(x = sp, title = title, subtitle = subtitle) + dat_venn_setedge <- ggVennDiagram::venn_setedge(data) + dat_venn_setedge[["colors"]] <- colors[stat.by[as.numeric( + dat_venn_setedge[["id"]] + )]] + + venn_regionedge_data <- ggVennDiagram::venn_regionedge(data) + venn_regionedge_data[["colors"]] <- dat_venn_region[["colors"]][ + match( + venn_regionedge_data[["id"]], + dat_venn_region[["id"]] + ) + ] + + p <- ggplot() + + geom_polygon( + data = venn_regionedge_data, + aes(X, Y, fill = colors, group = id), + alpha = alpha + ) + + geom_path( + data = dat_venn_setedge, + aes(X, Y, group = id), + color = "black", + linewidth = 1, + show.legend = FALSE + ) + + ggrepel::geom_text_repel( + data = ggVennDiagram::venn_setlabel(data), + aes( + X, + Y, + label = paste0( + name, + "\n(", + count, + ")" + ) + ), + fontface = "bold", + colour = label.fg, + size = label.size + 0.5, + bg.color = label.bg, + bg.r = label.bg.r, + point.size = NA, + max.overlaps = 100, + force = 0, + min.segment.length = 0, + segment.colour = "black" + ) + + ggrepel::geom_text_repel( + data = ggVennDiagram::venn_regionlabel(data), + aes(X, Y, label = count), + colour = label.fg, + size = label.size, + bg.color = label.bg, + bg.r = label.bg.r, + point.size = NA, + max.overlaps = 100, + force = 0, + min.segment.length = 0, + segment.colour = "black" + ) + + scale_fill_identity() + + coord_fixed(ratio = 1, clip = "off") + + theme( + plot.title = element_text(hjust = 0.5), + plot.background = element_blank(), + panel.background = element_blank(), + axis.title.y = element_blank(), + axis.text = element_blank(), + axis.ticks = element_blank() + ) + p <- p + labs(x = sp, title = title, subtitle = subtitle) + } } if (plot_type == "upset") { @@ -1329,6 +1410,13 @@ StatPlot <- function( ncol = ncol, byrow = byrow ) + if ( + identical(plot_type, "venn") && + identical(venn_engine, "venny") && + length(venny_details) > 0 + ) { + attr(plot, "venny_detail") <- venny_details + } } else { plot <- plist[[1]] } diff --git a/man/StatPlot.Rd b/man/StatPlot.Rd index f68a2d4..428dd25 100644 --- a/man/StatPlot.Rd +++ b/man/StatPlot.Rd @@ -18,6 +18,8 @@ StatPlot( stat_level = NULL, plot_type = c("bar", "rose", "ring", "pie", "trend", "trend_alluvial", "area", "dot", "sankey", "chord", "venn", "upset"), + venn_engine = c("ggVennDiagram", "venny"), + venn_args = list(), stat_type = c("percent", "count"), position = c("stack", "dodge"), palette = "Chinese", @@ -87,6 +89,16 @@ Can be one of \code{"bar"}, \code{"rose"}, \code{"ring"}, \code{"pie"}, \code{"t \code{"trend_alluvial"}, \code{"area"}, \code{"dot"}, \code{"sankey"}, \code{"chord"}, \code{"venn"}, or \code{"upset"}.} +\item{venn_engine}{The engine used when \code{plot_type = "venn"}. +Can be one of \code{"ggVennDiagram"} or \code{"venny"}. +Default is \code{"ggVennDiagram"}. +The \code{"venny"} engine supports 2 to 4 sets.} + +\item{venn_args}{A list of additional arguments passed to +\link[venny:venny]{venny::venny()} when \code{plot_type = "venn"} and +\code{venn_engine = "venny"}. +The \code{data} argument is generated internally and cannot be supplied here.} + \item{stat_type}{The type of statistic to compute for the plot. Can be one of \code{"percent"} or \code{"count"}.} @@ -257,4 +269,16 @@ StatPlot( group.by = "Group", plot_type = "trend_alluvial" ) + +meta_data$A <- meta_data$Type == "A" +meta_data$B <- meta_data$Group == "X" +meta_data$C <- meta_data$Batch == "B1" +StatPlot( + meta_data, + stat.by = c("A", "B", "C"), + stat_level = TRUE, + plot_type = "venn", + venn_engine = "venny", + venn_args = list(detail = TRUE) +) } diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..818ccf8 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(thisplot) + +test_check("thisplot") diff --git a/tests/testthat/test-statplot-venny.R b/tests/testthat/test-statplot-venny.R new file mode 100644 index 0000000..447ca04 --- /dev/null +++ b/tests/testthat/test-statplot-venny.R @@ -0,0 +1,91 @@ +statplot_venn_data <- function() { + data.frame( + A = c(TRUE, TRUE, FALSE, FALSE, TRUE, FALSE), + B = c(TRUE, FALSE, TRUE, FALSE, TRUE, FALSE), + C = c(FALSE, TRUE, TRUE, FALSE, TRUE, FALSE), + D = c(FALSE, FALSE, TRUE, TRUE, TRUE, FALSE), + E = c(TRUE, FALSE, FALSE, TRUE, FALSE, TRUE), + Split = factor(c("S1", "S1", "S1", "S2", "S2", "S2")) + ) +} + +test_that("StatPlot venn keeps ggVennDiagram as the default engine", { + skip_if_not_installed("ggVennDiagram") + + p <- StatPlot( + meta.data = statplot_venn_data(), + stat.by = c("A", "B", "C"), + stat_level = TRUE, + plot_type = "venn" + ) + + expect_s3_class(p, "ggplot") + expect_null(attr(p, "venny_detail", exact = TRUE)) +}) + +test_that("StatPlot venn can use the venny engine", { + skip_if_not_installed("venny") + + p <- StatPlot( + meta.data = statplot_venn_data(), + stat.by = c("A", "B", "C"), + stat_level = TRUE, + plot_type = "venn", + venn_engine = "venny" + ) + + expect_s3_class(p, "ggplot") +}) + +test_that("StatPlot preserves venny detail results", { + skip_if_not_installed("venny") + + p <- StatPlot( + meta.data = statplot_venn_data(), + stat.by = c("A", "B", "C"), + stat_level = TRUE, + plot_type = "venn", + venn_engine = "venny", + venn_args = list(detail = TRUE) + ) + + detail <- attr(p, "venny_detail", exact = TRUE) + expect_type(detail, "list") + expect_true(all(c("venn", "ellipse_path", "table", "subset_elements") %in% names(detail))) +}) + +test_that("StatPlot preserves split venny detail results on combined plots", { + skip_if_not_installed("venny") + + p <- StatPlot( + meta.data = statplot_venn_data(), + stat.by = c("A", "B", "C"), + split.by = "Split", + stat_level = TRUE, + plot_type = "venn", + venn_engine = "venny", + venn_args = list(detail = TRUE) + ) + + detail <- attr(p, "venny_detail", exact = TRUE) + expect_type(detail, "list") + expect_named(detail, c("S1", "S2")) + expect_true(all(vapply( + detail, + function(x) all(c("venn", "ellipse_path", "table", "subset_elements") %in% names(x)), + logical(1) + ))) +}) + +test_that("StatPlot rejects venny venns with more than four sets", { + expect_error( + StatPlot( + meta.data = statplot_venn_data(), + stat.by = c("A", "B", "C", "D", "E"), + stat_level = TRUE, + plot_type = "venn", + venn_engine = "venny" + ), + "2 to 4" + ) +})