From bb77f9d0a610622cabcc3165f7fa796b19893bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:31:45 +0000 Subject: [PATCH 01/24] feat: support categorical variables in association and bivariate --- NEWS.md | 1 + R/custom_mosaic.R | 87 +++++++++++++++++++++++++++++++++++++ R/tm_g_bivariate.R | 5 ++- man/create_mosaic_layers.Rd | 24 ++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 R/custom_mosaic.R create mode 100644 man/create_mosaic_layers.Rd diff --git a/NEWS.md b/NEWS.md index 71f2ecf30..c95d96a59 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ ### Enhancements - Modules now return a `teal_report` object that contains the data, code and reporter. All the reporter buttons were removed from the modules' UI. +- Support case when both variables are categorical in association and bivariate plots. # teal.modules.general 0.5.1 diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R new file mode 100644 index 000000000..503f17fa2 --- /dev/null +++ b/R/custom_mosaic.R @@ -0,0 +1,87 @@ +#' Minimal mosaic plot +#' +#' Provides a minimal mosaic plot implementation using ggplot2. +#' @param data_name Name of the data frame to use. +#' @param x_var Name of the variable to use on the x-axis. +#' @param y_var Name of the variable to use for fill colors. +#' @param reduce_plot_call Function that takes multiple ggplot2 layers and combines them into a single plot call. +#' @return An expression that creates a mosaic plot when evaluated. +#' @keywords internal +create_mosaic_layers <- function(data_name, x_var, y_var, reduce_plot_call) { + data_call <- substitute( + mosaic_data <- data_name |> + # Count combinations of X and Y + dplyr::count(x_var, y_var) |> + # Compute total for each X group + dplyr::group_by(x_var) |> + dplyr::mutate( + x_total = sum(n), + prop = n / x_total + ) |> + dplyr::ungroup() |> + # Compute total sample size to turn counts into widths + dplyr::mutate(N_total = sum(x_total)) |> + # Convert counts to x widths + dplyr::group_by(x_var) |> + dplyr::mutate( + x_width = x_total / unique(N_total) + ) |> + # Compute x-min/x-max for each group + dplyr::group_by(x_var) |> + dplyr::mutate( + x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) + ) |> + dplyr::ungroup() |> + dplyr::mutate( + xmin = cumsum(dplyr::lag(x_width_last, default = 0)), + xmax = xmin + x_width + ) |> + # Compute y-min/y-max for stacked proportions + dplyr::group_by(x_var) |> + dplyr::arrange(x_var, y_var) |> + dplyr::mutate( + ymin = c(0, head(cumsum(prop), -1)), + ymax = cumsum(prop) + ) |> + dplyr::ungroup(), + env = list(x_var = as.name(x_var), y_var = as.name(y_var), data_name = as.name(data_name)) + ) + + layer_rect <- substitute( + ggplot2::geom_rect( + ggplot2::aes( + xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = y_var + ), + data = mosaic_data, + color = "white" + ), + env = list(y_var = as.name(y_var)) + ) + + layer_scale_x <- substitute( + ggplot2::scale_x_continuous( + breaks = mosaic_data |> + dplyr::distinct(x_var, xmin, xmax) |> + dplyr::rowwise() |> + dplyr::mutate(mid = (xmin + xmax) / 2) |> + dplyr::pull(mid), + labels = mosaic_data |> + dplyr::distinct(x_var) |> + dplyr::pull(x_var), + expand = c(0, 0) + ), + env = list(x_var = as.name(x_var)) + ) + + bquote( + local({ + .(data_call) + + list( + .(layer_rect), + .(layer_scale_x), + ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) + ) + }) + ) +} diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index 4bb60d406..c59516a18 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -927,7 +927,10 @@ bivariate_ggplot_call <- function(x_class, ) # Factor and character plots } else if (x_class == "factor" && y_class == "factor") { - stop("Categorical variables 'x' and 'y' are currently not supported.") + plot_call <- reduce_plot_call( + plot_call, + create_mosaic_layers(data_name, x_var = x, y_var = y, reduce_plot_call = reduce_plot_call) + ) } else { stop("x y type combination not allowed") } diff --git a/man/create_mosaic_layers.Rd b/man/create_mosaic_layers.Rd new file mode 100644 index 000000000..17ed7503c --- /dev/null +++ b/man/create_mosaic_layers.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/custom_mosaic.R +\name{create_mosaic_layers} +\alias{create_mosaic_layers} +\title{Minimal mosaic plot} +\usage{ +create_mosaic_layers(data_name, x_var, y_var, reduce_plot_call) +} +\arguments{ +\item{data_name}{Name of the data frame to use.} + +\item{x_var}{Name of the variable to use on the x-axis.} + +\item{y_var}{Name of the variable to use for fill colors.} + +\item{reduce_plot_call}{Function that takes multiple ggplot2 layers and combines them into a single plot call.} +} +\value{ +An expression that creates a mosaic plot when evaluated. +} +\description{ +Provides a minimal mosaic plot implementation using ggplot2. +} +\keyword{internal} From f3c2020fea1468eb8c75a9950b7663b6bb2e5367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:45:26 +0000 Subject: [PATCH 02/24] chore: prefix internal function with dot --- R/custom_mosaic.R | 2 +- R/tm_g_bivariate.R | 2 +- ...{create_mosaic_layers.Rd => dot-create_mosaic_layers.Rd} | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename man/{create_mosaic_layers.Rd => dot-create_mosaic_layers.Rd} (82%) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 503f17fa2..2487d0afd 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -7,7 +7,7 @@ #' @param reduce_plot_call Function that takes multiple ggplot2 layers and combines them into a single plot call. #' @return An expression that creates a mosaic plot when evaluated. #' @keywords internal -create_mosaic_layers <- function(data_name, x_var, y_var, reduce_plot_call) { +.create_mosaic_layers <- function(data_name, x_var, y_var, reduce_plot_call) { data_call <- substitute( mosaic_data <- data_name |> # Count combinations of X and Y diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index c59516a18..32b586048 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -929,7 +929,7 @@ bivariate_ggplot_call <- function(x_class, } else if (x_class == "factor" && y_class == "factor") { plot_call <- reduce_plot_call( plot_call, - create_mosaic_layers(data_name, x_var = x, y_var = y, reduce_plot_call = reduce_plot_call) + .create_mosaic_layers(data_name, x_var = x, y_var = y, reduce_plot_call = reduce_plot_call) ) } else { stop("x y type combination not allowed") diff --git a/man/create_mosaic_layers.Rd b/man/dot-create_mosaic_layers.Rd similarity index 82% rename from man/create_mosaic_layers.Rd rename to man/dot-create_mosaic_layers.Rd index 17ed7503c..5512ae9a3 100644 --- a/man/create_mosaic_layers.Rd +++ b/man/dot-create_mosaic_layers.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/custom_mosaic.R -\name{create_mosaic_layers} -\alias{create_mosaic_layers} +\name{.create_mosaic_layers} +\alias{.create_mosaic_layers} \title{Minimal mosaic plot} \usage{ -create_mosaic_layers(data_name, x_var, y_var, reduce_plot_call) +.create_mosaic_layers(data_name, x_var, y_var, reduce_plot_call) } \arguments{ \item{data_name}{Name of the data frame to use.} From 6fe6664c49bdf6293d3cbbb8f4ca7e1787c0ca85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:24:29 +0000 Subject: [PATCH 03/24] pr: feedback from @llrs-roche --- R/custom_mosaic.R | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 2487d0afd..02088f511 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -9,41 +9,35 @@ #' @keywords internal .create_mosaic_layers <- function(data_name, x_var, y_var, reduce_plot_call) { data_call <- substitute( - mosaic_data <- data_name |> + mosaic_data <- data_name %>% # Count combinations of X and Y - dplyr::count(x_var, y_var) |> + dplyr::count(x_var, y_var) %>% # Compute total for each X group - dplyr::group_by(x_var) |> dplyr::mutate( + .by = x_var, x_total = sum(n), prop = n / x_total - ) |> - dplyr::ungroup() |> + ) %>% # Compute total sample size to turn counts into widths - dplyr::mutate(N_total = sum(x_total)) |> + dplyr::mutate(N_total = sum(x_total)) %>% # Convert counts to x widths - dplyr::group_by(x_var) |> - dplyr::mutate( - x_width = x_total / unique(N_total) - ) |> - # Compute x-min/x-max for each group - dplyr::group_by(x_var) |> dplyr::mutate( + .by = x_var, + x_width = x_total / unique(N_total), x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) - ) |> - dplyr::ungroup() |> + ) %>% + # Compute x-min/x-max for each group dplyr::mutate( xmin = cumsum(dplyr::lag(x_width_last, default = 0)), xmax = xmin + x_width - ) |> + ) %>% # Compute y-min/y-max for stacked proportions - dplyr::group_by(x_var) |> dplyr::arrange(x_var, y_var) |> dplyr::mutate( + .by = x_var, ymin = c(0, head(cumsum(prop), -1)), ymax = cumsum(prop) - ) |> - dplyr::ungroup(), + ), env = list(x_var = as.name(x_var), y_var = as.name(y_var), data_name = as.name(data_name)) ) @@ -60,13 +54,12 @@ layer_scale_x <- substitute( ggplot2::scale_x_continuous( - breaks = mosaic_data |> - dplyr::distinct(x_var, xmin, xmax) |> - dplyr::rowwise() |> - dplyr::mutate(mid = (xmin + xmax) / 2) |> + breaks = mosaic_data %>% + dplyr::distinct(x_var, xmin, xmax) %>% + dplyr::mutate(mid = (xmin + xmax) / 2) %>% dplyr::pull(mid), - labels = mosaic_data |> - dplyr::distinct(x_var) |> + labels = mosaic_data %>% + dplyr::distinct(x_var) %>% dplyr::pull(x_var), expand = c(0, 0) ), From fc0a48214d0ce5849c59d185ef356701dc79aada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:34:51 +0000 Subject: [PATCH 04/24] pr: make mosaic call simpler --- R/custom_mosaic.R | 15 +++++---------- R/tm_g_bivariate.R | 8 +++++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 02088f511..d7b60a959 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -32,7 +32,7 @@ xmax = xmin + x_width ) %>% # Compute y-min/y-max for stacked proportions - dplyr::arrange(x_var, y_var) |> + dplyr::arrange(x_var, y_var) %>% dplyr::mutate( .by = x_var, ymin = c(0, head(cumsum(prop), -1)), @@ -46,7 +46,6 @@ ggplot2::aes( xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = y_var ), - data = mosaic_data, color = "white" ), env = list(y_var = as.name(y_var)) @@ -67,14 +66,10 @@ ) bquote( - local({ - .(data_call) - - list( - .(layer_rect), - .(layer_scale_x), + .(data_call) %>% + ggplot2::ggplot() + + .(layer_rect) + + .(layer_scale_x) + ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) - ) - }) ) } diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index 32b586048..4e9968c90 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -927,9 +927,11 @@ bivariate_ggplot_call <- function(x_class, ) # Factor and character plots } else if (x_class == "factor" && y_class == "factor") { - plot_call <- reduce_plot_call( - plot_call, - .create_mosaic_layers(data_name, x_var = x, y_var = y, reduce_plot_call = reduce_plot_call) + plot_call <- .create_mosaic_layers( + data_name, + x_var = x, + y_var = y, + reduce_plot_call = reduce_plot_call ) } else { stop("x y type combination not allowed") From 54914e67f72dab060d2d6e35f7776afbae9f3f24 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:36:55 +0000 Subject: [PATCH 05/24] [skip style] [skip vbump] Restyle files --- R/custom_mosaic.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index d7b60a959..901ff7c66 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -68,8 +68,8 @@ bquote( .(data_call) %>% ggplot2::ggplot() + - .(layer_rect) + - .(layer_scale_x) + - ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) + .(layer_rect) + + .(layer_scale_x) + + ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) ) } From e068c35afb94cf5d39281495afa9d52c95627500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:18:43 +0000 Subject: [PATCH 06/24] tests: re-adds tests that were removed --- tests/testthat/test_bivariate_ggplot_call.R | 47 ++++++++------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/tests/testthat/test_bivariate_ggplot_call.R b/tests/testthat/test_bivariate_ggplot_call.R index c85d4fd1b..d65d122f5 100644 --- a/tests/testthat/test_bivariate_ggplot_call.R +++ b/tests/testthat/test_bivariate_ggplot_call.R @@ -18,35 +18,24 @@ testthat::test_that("bivariate_ggplot_call with numerics", { ) }) -testthat::test_that("bivariate_ggplot_call with factor, char, logical", { - error_message <- "Categorical variables 'x' and 'y' are currently not supported." - testthat::expect_error( - bivariate_ggplot_call("factor", "factor") %>% deparse(width.cutoff = 300), - error_message - ) - testthat::expect_error( - bivariate_ggplot_call("logical", "factor") %>% deparse(width.cutoff = 300), - error_message - ) - testthat::expect_error( - bivariate_ggplot_call("character", "factor") %>% deparse(width.cutoff = 300), - error_message - ) - testthat::expect_error( - bivariate_ggplot_call("logical", "character") %>% deparse(width.cutoff = 300), - error_message - ) - testthat::expect_error( - bivariate_ggplot_call("character", "logical") %>% deparse(width.cutoff = 300), - error_message - ) - testthat::expect_error( - bivariate_ggplot_call("logical", "logical") %>% deparse(width.cutoff = 300), - error_message - ) - testthat::expect_error( - bivariate_ggplot_call("character", "character") %>% deparse(width.cutoff = 300), - error_message +testthat::describe("bivariate_ggplot_call with arguments:", { + possible_classes <- c("factor", "logical", "character") + comb <- expand.grid(a = possible_classes, b = possible_classes, stringsAsFactors = FALSE) + apply( + comb, + 1, + function(x) { + it(sprintf("%s and %s", x[[1]], x[[2]]), { + testthat::expect_match( + deparse( + bivariate_ggplot_call(x[[1]], x[[2]], data_name = "ANL", x = "x", y = "y"), + width.cutoff = 300 + ), + "mosaic_data <- ", + all = FALSE + ) + }) + } ) }) From 9102fba1754666af250d77f96ae259eab259cab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:52:05 +0000 Subject: [PATCH 07/24] feat: adds datacall outside and supports multiple plots in association --- R/custom_mosaic.R | 35 +++++++++++++++++++++++------------ R/tm_g_association.R | 11 +++++++++-- R/tm_g_bivariate.R | 17 ++++++++++++----- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 901ff7c66..a015afe72 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -7,9 +7,11 @@ #' @param reduce_plot_call Function that takes multiple ggplot2 layers and combines them into a single plot call. #' @return An expression that creates a mosaic plot when evaluated. #' @keywords internal -.create_mosaic_layers <- function(data_name, x_var, y_var, reduce_plot_call) { +.build_mosaic_plot <- function(data_name, x_var, y_var, reduce_plot_call) { + mosaic_data_name <- sprintf("%s_mosaic_%s_%s", data_name, as.character(x_var), as.character(y_var)) + browser() data_call <- substitute( - mosaic_data <- data_name %>% + mosaic_data_name <- data_name %>% # Count combinations of X and Y dplyr::count(x_var, y_var) %>% # Compute total for each X group @@ -38,7 +40,12 @@ ymin = c(0, head(cumsum(prop), -1)), ymax = cumsum(prop) ), - env = list(x_var = as.name(x_var), y_var = as.name(y_var), data_name = as.name(data_name)) + env = list( + x_var = as.name(x_var), + y_var = as.name(y_var), + data_name = as.name(data_name), + mosaic_data_name = as.name(mosaic_data_name) + ) ) layer_rect <- substitute( @@ -53,23 +60,27 @@ layer_scale_x <- substitute( ggplot2::scale_x_continuous( - breaks = mosaic_data %>% + breaks = mosaic_data_name %>% dplyr::distinct(x_var, xmin, xmax) %>% dplyr::mutate(mid = (xmin + xmax) / 2) %>% dplyr::pull(mid), - labels = mosaic_data %>% + labels = mosaic_data_name %>% dplyr::distinct(x_var) %>% dplyr::pull(x_var), expand = c(0, 0) ), - env = list(x_var = as.name(x_var)) + env = list(x_var = as.name(x_var), mosaic_data_name = as.name(mosaic_data_name)) ) - bquote( - .(data_call) %>% - ggplot2::ggplot() + - .(layer_rect) + - .(layer_scale_x) + - ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) + list( + data_call = data_call, + plot_call = bquote( + ggplot2::ggplot(.(as.name(mosaic_data_name))) + + .(layer_rect) + + .(layer_scale_x) + + ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) + ) ) } + + diff --git a/R/tm_g_association.R b/R/tm_g_association.R index 8516efc9b..f2e65aa08 100644 --- a/R/tm_g_association.R +++ b/R/tm_g_association.R @@ -495,6 +495,10 @@ srv_tm_g_association <- function(id, } obj <- merged$anl_q_r() teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Plot") + + data_calls <- Map(function(x) .subset2(x, "data_call"), var_calls) + plot_calls <- Map(function(x) .subset2(x, "plot_call"), var_calls) + teal.code::eval_code( obj, substitute( @@ -502,16 +506,19 @@ srv_tm_g_association <- function(id, env = list(new_title = new_title) ) ) %>% + teal.code::eval_code( + bquote({..(data_calls)}, splice = TRUE) + ) %>% teal.code::eval_code( substitute( expr = { plots <- plot_calls - plot <- gridExtra::arrangeGrob(plots[[1]], plots[[2]], ncol = 1) + plot <- gridExtra::arrangeGrob(grobs = plots, ncol = 1) }, env = list( plot_calls = do.call( "call", - c(list("list", ref_call), var_calls), + c(list("list", ref_call$plot_call), plot_calls), quote = TRUE ) ) diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index 4e9968c90..807eae44e 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -632,7 +632,7 @@ srv_g_bivariate <- function(id, teal::validate_has_data(ANL[, c(x_name, y_name), drop = FALSE], 3, complete = TRUE, allow_inf = FALSE) - cl <- bivariate_plot_call( + bivariate_cl <- bivariate_plot_call( data_name = "ANL", x = x_name, y = y_name, @@ -649,6 +649,8 @@ srv_g_bivariate <- function(id, ggplot2_args = ggplot2_args ) + cl <- bivariate_cl$plot_call + facetting <- (isTRUE(input$facetting) && (!is.null(row_facet_name) || !is.null(col_facet_name))) if (facetting) { @@ -688,7 +690,9 @@ srv_g_bivariate <- function(id, obj <- merged$anl_q_r() teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Plot") - teal.code::eval_code(obj, substitute(expr = plot <- cl, env = list(cl = cl))) + browser() + teal.code::eval_code(obj, bivariate_cl$data_call %||% "") |> + teal.code::eval_code(substitute(expr = plot <- cl, env = list(cl = cl))) }) decorated_output_q_facets <- srv_decorate_teal_data( @@ -768,7 +772,7 @@ bivariate_plot_call <- function(data_name, y <- if (is.call(y)) y else as.name(y) } - cl <- bivariate_ggplot_call( + bivariate_ggplot_call( x_class = x_class, y_class = y_class, freq = freq, @@ -832,6 +836,7 @@ bivariate_ggplot_call <- function(x_class, Reduce(function(x, y) call("+", x, y), args) } + data_call <- NULL plot_call <- substitute(ggplot2::ggplot(data_name), env = list(data_name = as.name(data_name))) # Single data plots @@ -927,12 +932,14 @@ bivariate_ggplot_call <- function(x_class, ) # Factor and character plots } else if (x_class == "factor" && y_class == "factor") { - plot_call <- .create_mosaic_layers( + mosaic_call <- .build_mosaic_plot( data_name, x_var = x, y_var = y, reduce_plot_call = reduce_plot_call ) + plot_call <- mosaic_call$plot_call + data_call <- mosaic_call$data_call } else { stop("x y type combination not allowed") } @@ -972,7 +979,7 @@ bivariate_ggplot_call <- function(x_class, plot_call <- reduce_plot_call(plot_call, quote(coord_flip())) } - plot_call + list(plot_call = plot_call, data_call = data_call) } # Create facet call From 432eb3822e228a4f2e8100911409cdf109b6a803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:24:10 +0000 Subject: [PATCH 08/24] feat: using custom geom --- NAMESPACE | 4 + R/custom_mosaic.R | 295 +++++++++++++++++++++++-------- R/tm_g_association.R | 9 +- R/tm_g_bivariate.R | 30 ++-- R/utils.R | 1 + man/dot-calculate_coordinates.Rd | 26 +++ man/dot-create_mosaic_layers.Rd | 24 --- man/geom_mosaic.Rd | 46 +++++ man/scale_type.mosaic.Rd | 17 ++ man/scale_x_mosaic.Rd | 66 +++++++ 10 files changed, 392 insertions(+), 126 deletions(-) create mode 100644 man/dot-calculate_coordinates.Rd delete mode 100644 man/dot-create_mosaic_layers.Rd create mode 100644 man/geom_mosaic.Rd create mode 100644 man/scale_type.mosaic.Rd create mode 100644 man/scale_x_mosaic.Rd diff --git a/NAMESPACE b/NAMESPACE index 302a2f68e..0392296ef 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,8 +8,11 @@ S3method(create_sparklines,default) S3method(create_sparklines,factor) S3method(create_sparklines,logical) S3method(create_sparklines,numeric) +S3method(scale_type,mosaic) export(add_facet_labels) +export(geom_mosaic) export(get_scatterplotmatrix_stats) +export(scale_x_mosaic) export(tm_a_pca) export(tm_a_regression) export(tm_data_table) @@ -30,4 +33,5 @@ import(shiny) import(teal) import(teal.transform) importFrom(dplyr,"%>%") +importFrom(ggplot2,scale_type) importFrom(lifecycle,deprecated) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index a015afe72..f78e37264 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -1,86 +1,227 @@ -#' Minimal mosaic plot +#' Mosaic Rectangles Layer for ggplot2 #' -#' Provides a minimal mosaic plot implementation using ggplot2. -#' @param data_name Name of the data frame to use. -#' @param x_var Name of the variable to use on the x-axis. -#' @param y_var Name of the variable to use for fill colors. -#' @param reduce_plot_call Function that takes multiple ggplot2 layers and combines them into a single plot call. -#' @return An expression that creates a mosaic plot when evaluated. -#' @keywords internal -.build_mosaic_plot <- function(data_name, x_var, y_var, reduce_plot_call) { - mosaic_data_name <- sprintf("%s_mosaic_%s_%s", data_name, as.character(x_var), as.character(y_var)) - browser() - data_call <- substitute( - mosaic_data_name <- data_name %>% - # Count combinations of X and Y - dplyr::count(x_var, y_var) %>% - # Compute total for each X group - dplyr::mutate( - .by = x_var, - x_total = sum(n), - prop = n / x_total - ) %>% - # Compute total sample size to turn counts into widths - dplyr::mutate(N_total = sum(x_total)) %>% - # Convert counts to x widths - dplyr::mutate( - .by = x_var, - x_width = x_total / unique(N_total), - x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) - ) %>% - # Compute x-min/x-max for each group - dplyr::mutate( - xmin = cumsum(dplyr::lag(x_width_last, default = 0)), - xmax = xmin + x_width - ) %>% - # Compute y-min/y-max for stacked proportions - dplyr::arrange(x_var, y_var) %>% - dplyr::mutate( - .by = x_var, - ymin = c(0, head(cumsum(prop), -1)), - ymax = cumsum(prop) - ), - env = list( - x_var = as.name(x_var), - y_var = as.name(y_var), - data_name = as.name(data_name), - mosaic_data_name = as.name(mosaic_data_name) - ) - ) +#' Adds a mosaic-style rectangles layer to a ggplot, visualizing the joint distribution of categorical variables. +#' Each rectangle's size reflects the proportion of observations for combinations of `x` and `fill`. +#' +#' @param mapping Set of aesthetic mappings created by `aes()`. Must specify `x` and `fill`. +#' @param data The data to be displayed in this layer. +#' @param stat The statistical transformation to use on the data. Defaults to `"rects"`. +#' @param position Position adjustment. Defaults to `"identity"`. +#' @param ... Other arguments passed to `layer()`. +#' @param na.rm Logical. Should missing values be removed? +#' @param show.legend Logical. Should this layer be included in the legends? +#' @param inherit.aes Logical. If `FALSE`, overrides default aesthetics. +#' +#' @return A ggplot2 layer that adds mosaic rectangles to the plot. +#' +#' @examples +#' df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) +#' library(ggplot2) +#' ggplot(df, aes(x = RACE, fill = SEX)) + geom_rects() +#' @export +geom_mosaic <- function(mapping = NULL, data = NULL, + stat = "mosaic", position = "identity", + ..., + na.rm = FALSE, + show.legend = TRUE, + inherit.aes = TRUE) { + aes_x <- list(rlang::quo_get_expr(mapping$x)) + var_x <- sprintf("x__%s", as.character(aes_x)) + aes_fill <- rlang::quo_text(mapping$fill) + var_fill <- sprintf("x__fill__%s", aes_fill) - layer_rect <- substitute( - ggplot2::geom_rect( - ggplot2::aes( - xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = y_var - ), - color = "white" - ), - env = list(y_var = as.name(y_var)) - ) + mapping[[var_x]] <- mapping$x + mapping$x <- structure(1L, class = "mosaic") + # mapping[[var_fill]] <- mapping$fill - layer_scale_x <- substitute( - ggplot2::scale_x_continuous( - breaks = mosaic_data_name %>% - dplyr::distinct(x_var, xmin, xmax) %>% - dplyr::mutate(mid = (xmin + xmax) / 2) %>% - dplyr::pull(mid), - labels = mosaic_data_name %>% - dplyr::distinct(x_var) %>% - dplyr::pull(x_var), - expand = c(0, 0) - ), - env = list(x_var = as.name(x_var), mosaic_data_name = as.name(mosaic_data_name)) + layer <- ggplot2::layer( + geom = GeomMosaic, + stat = "mosaic", + data = data, + mapping = mapping, + position = position, + show.legend = show.legend, + inherit.aes = inherit.aes, + check.aes = FALSE, + params = list(na.rm = na.rm, ...) ) + #list(layer, .scale_x_mosaic()) + layer +} + +GeomMosaic <- ggplot2::ggproto( + "GeomMosaic", ggplot2::GeomRect, + default_aes = ggplot2::aes( + colour = NA, linewidth = 0.5, linetype = 1, alpha = 1, fill = "grey30" + ), + draw_panel = function(data, panel_params, coord) { + if (all(is.na(data$colour))) data$colour <- scales::alpha(data$fill, data$alpha) + ggplot2::GeomRect$draw_panel(data, panel_params, coord) + }, + required_aes = c("xmin", "xmax", "ymin", "ymax") +) + +#' Calculate Rectangle Coordinates for Mosaic Plot +#' +#' Computes the coordinates for rectangles in a mosaic plot based on combinations of `x` and `fill` variables. +#' For each unique `x` and `fill`, calculates the proportional widths and heights, stacking rectangles within each `x` group. +#' +#' @param data A data frame containing at least `x` and `fill` columns. +#' +#' @return A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, representing the position and size of each rectangle. +#' +#' @details +#' - Counts occurrences of each `x`/`fill` combination. +#' - Calculates proportions within each `x` group. +#' - Determines horizontal (`xmin`, `xmax`) and vertical (`ymin`, `ymax`) boundaries for each rectangle. +#' - Adds small padding to each boundary for visual separation. +.calculate_coordinates <- function(data) { + # Example: compute rectangles from x and y + result <- data |> + # Count combinations of X and Y + dplyr::count(x, fill) |> + # Compute total for each X group + dplyr::mutate( + .by = x, + x_total = sum(n), + prop = n / x_total + ) |> + # Change order from biggest group to smaller + dplyr::arrange(dplyr::desc(x_total), x, fill) |> + # Compute total sample size to turn counts into widths + dplyr::mutate( + N_total = dplyr::n(), + x_width = x_total / N_total + ) |> + # Convert counts to x widths + dplyr::mutate( + .by = x, + x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) + ) |> + # Compute x-min/x-max for each group + dplyr::mutate( + xmin = cumsum(dplyr::lag(x_width_last, default = 0)), + xmax = xmin + x_width + ) |> + # Compute y-min/y-max for stacked proportions + dplyr::mutate( + .by = x, + ymin = c(0, head(cumsum(prop), -1)), + ymax = cumsum(prop) + ) |> + dplyr::mutate( + xmin = xmin / max(xmax), + xmax = xmax / max(xmax), + xmin = xmin + 0.005, + xmax = xmax - 0.005, + ymin = ymin + 0.005, + ymax = ymax - 0.005 + ) |> + dplyr::select(x, fill, xmin, xmax, ymin, ymax) + result +} - list( - data_call = data_call, - plot_call = bquote( - ggplot2::ggplot(.(as.name(mosaic_data_name))) + - .(layer_rect) + - .(layer_scale_x) + - ggplot2::scale_y_continuous(expand = c(0, 0), labels = scales::percent_format(scale = 100)) - ) +StatMosaic <- ggplot2::ggproto( + "StatMosaic", ggplot2::Stat, + + required_aes = c("x", "fill"), + + compute_group = function(data, scales) { + data + }, + compute_panel = function(data, scales) { + data$x <- data[, grepl("x__", colnames(data))] + result <- .calculate_coordinates(data) + + scale_x <- ggplot2::ScaleContinuous + scale_x[["breaks"]] <- result |> + dplyr::distinct(x, xmin, xmax) |> + dplyr::mutate(mid = (xmin + xmax) / 2) |> + dplyr::pull(mid) + + scale_x[["labels"]] <- result |> + dplyr::distinct(x) |> + dplyr::pull(x) + + result$x <- list(scale = scale_x) + result$group <- 1 + result$PANEL <- unique(data$PANEL) + result + } +) + +#' Helper function for determining scales +#' +#' Used internally to determine class of variable x +#' @param x variable +#' @return character string "productlist" +#' @importFrom ggplot2 scale_type +#' @export +scale_type.mosaic <- function(x) { + # cat("checking for type productlist\n") + #browser() + "mosaic" +} + + +#' Determining scales for mosaics +#' +#' @param name set to pseudo waiver function `product_names` by default. +#' @param ... other arguments passed to `continuous_scale()`. +#' @inheritParams ggplot2::continuous_scale +#' @export +scale_x_mosaic <- function(breaks = function() function(x) unique(x), + minor_breaks = NULL, + labels = function() function(x) unique(x), + na.value = NA_real_, + position = "bottom", + ...) { + ggplot2::continuous_scale( + aesthetics = c( + "x", "xmin", "xmax", "xend", "xintercept", "xmin_final", "xmax_final", + "xlower", "xmiddle", "xupper" + ), palette = identity, breaks = breaks, minor_breaks = minor_breaks, + labels = labels, na.value = na.value, position = position, + super = ScaleContinuousProduct,, guide = ggplot2::waiver(), ... ) } +#' @rdname dot-scale_x_mosaic +ScaleContinuousProduct <- ggplot2::ggproto( + "ScaleContinuousProduct", ggplot2::ScaleContinuousPosition, + train =function(self, x) { + if (is.list(x)) { + x <- x[[1]] + if ("Scale" %in% class(x)) { + # re-assign the scale values now that we have the information - but only if necessary + if (is.function(self$breaks)) self$breaks <- x$breaks + if (is.function(self$labels)) self$labels <- x$labels + return(NULL) + } + } + if (self$is.discrete(x)) { + self$range$train(x=c(0,1)) + return(NULL) + } + self$range$train(x) + }, + map = function(self, x, limits = self$get_limits()) { + if (self$is.discrete(x)) return(x) + if (is.list(x)) return(0) # need a number + scaled <- as.numeric(self$oob(x, limits)) + ifelse(!is.na(scaled), scaled, self$na.value) + }, + dimension = function(self, expand = c(0, 0)) { + c(-0.05,1.05) + }, + make_title = function(..., self) { + title <- ggplot2::ggproto_parent(ggplot2::ScaleContinuousPosition, self)$make_title(...) + if (isTRUE(title %in% self$aesthetics)) { + title <- self$product_name + } + else title + }, + is.discrete = function(self, x) is.factor(x) || is.character(x) || is.logical(x) +) + diff --git a/R/tm_g_association.R b/R/tm_g_association.R index f2e65aa08..3fbc19d2c 100644 --- a/R/tm_g_association.R +++ b/R/tm_g_association.R @@ -495,10 +495,6 @@ srv_tm_g_association <- function(id, } obj <- merged$anl_q_r() teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Plot") - - data_calls <- Map(function(x) .subset2(x, "data_call"), var_calls) - plot_calls <- Map(function(x) .subset2(x, "plot_call"), var_calls) - teal.code::eval_code( obj, substitute( @@ -506,9 +502,6 @@ srv_tm_g_association <- function(id, env = list(new_title = new_title) ) ) %>% - teal.code::eval_code( - bquote({..(data_calls)}, splice = TRUE) - ) %>% teal.code::eval_code( substitute( expr = { @@ -518,7 +511,7 @@ srv_tm_g_association <- function(id, env = list( plot_calls = do.call( "call", - c(list("list", ref_call$plot_call), plot_calls), + c(list("list", ref_call), var_calls), quote = TRUE ) ) diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index 807eae44e..96a4725aa 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -632,7 +632,7 @@ srv_g_bivariate <- function(id, teal::validate_has_data(ANL[, c(x_name, y_name), drop = FALSE], 3, complete = TRUE, allow_inf = FALSE) - bivariate_cl <- bivariate_plot_call( + cl <- bivariate_plot_call( data_name = "ANL", x = x_name, y = y_name, @@ -649,8 +649,6 @@ srv_g_bivariate <- function(id, ggplot2_args = ggplot2_args ) - cl <- bivariate_cl$plot_call - facetting <- (isTRUE(input$facetting) && (!is.null(row_facet_name) || !is.null(col_facet_name))) if (facetting) { @@ -690,9 +688,7 @@ srv_g_bivariate <- function(id, obj <- merged$anl_q_r() teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Plot") - browser() - teal.code::eval_code(obj, bivariate_cl$data_call %||% "") |> - teal.code::eval_code(substitute(expr = plot <- cl, env = list(cl = cl))) + teal.code::eval_code(obj, substitute(expr = plot <- cl, env = list(cl = cl))) }) decorated_output_q_facets <- srv_decorate_teal_data( @@ -728,7 +724,9 @@ srv_g_bivariate <- function(id, }) ) - plot_r <- reactive(req(decorated_output_q_facets())[["plot"]]) + plot_r <- reactive({ + req(decorated_output_q_facets())[["plot"]] + }) pws <- teal.widgets::plot_with_settings_srv( id = "myplot", @@ -738,7 +736,7 @@ srv_g_bivariate <- function(id, ) set_chunk_dims(pws, decorated_output_q_facets) - }) + }) } # Get Substituted ggplot call @@ -836,7 +834,6 @@ bivariate_ggplot_call <- function(x_class, Reduce(function(x, y) call("+", x, y), args) } - data_call <- NULL plot_call <- substitute(ggplot2::ggplot(data_name), env = list(data_name = as.name(data_name))) # Single data plots @@ -932,14 +929,13 @@ bivariate_ggplot_call <- function(x_class, ) # Factor and character plots } else if (x_class == "factor" && y_class == "factor") { - mosaic_call <- .build_mosaic_plot( - data_name, - x_var = x, - y_var = y, - reduce_plot_call = reduce_plot_call + plot_call <- reduce_plot_call( + plot_call, + substitute( + teal.modules.general::geom_mosaic(ggplot2::aes(x = xval, fill = yval)), + env = list(xval = x, yval = y) + ) ) - plot_call <- mosaic_call$plot_call - data_call <- mosaic_call$data_call } else { stop("x y type combination not allowed") } @@ -979,7 +975,7 @@ bivariate_ggplot_call <- function(x_class, plot_call <- reduce_plot_call(plot_call, quote(coord_flip())) } - list(plot_call = plot_call, data_call = data_call) + plot_call } # Create facet call diff --git a/R/utils.R b/R/utils.R index 5f462ba0d..d625746a5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -287,6 +287,7 @@ srv_decorate_teal_data <- function(id, data, decorators, expr) { reactive({ req(decorated_output()) + browser() if (no_expr) { decorated_output() } else { diff --git a/man/dot-calculate_coordinates.Rd b/man/dot-calculate_coordinates.Rd new file mode 100644 index 000000000..f17830c9c --- /dev/null +++ b/man/dot-calculate_coordinates.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/custom_mosaic.R +\name{.calculate_coordinates} +\alias{.calculate_coordinates} +\title{Calculate Rectangle Coordinates for Mosaic Plot} +\usage{ +.calculate_coordinates(data) +} +\arguments{ +\item{data}{A data frame containing at least \code{x} and \code{fill} columns.} +} +\value{ +A data frame with columns: \code{x}, \code{fill}, \code{xmin}, \code{xmax}, \code{ymin}, \code{ymax}, representing the position and size of each rectangle. +} +\description{ +Computes the coordinates for rectangles in a mosaic plot based on combinations of \code{x} and \code{fill} variables. +For each unique \code{x} and \code{fill}, calculates the proportional widths and heights, stacking rectangles within each \code{x} group. +} +\details{ +\itemize{ +\item Counts occurrences of each \code{x}/\code{fill} combination. +\item Calculates proportions within each \code{x} group. +\item Determines horizontal (\code{xmin}, \code{xmax}) and vertical (\code{ymin}, \code{ymax}) boundaries for each rectangle. +\item Adds small padding to each boundary for visual separation. +} +} diff --git a/man/dot-create_mosaic_layers.Rd b/man/dot-create_mosaic_layers.Rd deleted file mode 100644 index 5512ae9a3..000000000 --- a/man/dot-create_mosaic_layers.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/custom_mosaic.R -\name{.create_mosaic_layers} -\alias{.create_mosaic_layers} -\title{Minimal mosaic plot} -\usage{ -.create_mosaic_layers(data_name, x_var, y_var, reduce_plot_call) -} -\arguments{ -\item{data_name}{Name of the data frame to use.} - -\item{x_var}{Name of the variable to use on the x-axis.} - -\item{y_var}{Name of the variable to use for fill colors.} - -\item{reduce_plot_call}{Function that takes multiple ggplot2 layers and combines them into a single plot call.} -} -\value{ -An expression that creates a mosaic plot when evaluated. -} -\description{ -Provides a minimal mosaic plot implementation using ggplot2. -} -\keyword{internal} diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd new file mode 100644 index 000000000..679c6b4b3 --- /dev/null +++ b/man/geom_mosaic.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/custom_mosaic.R +\name{geom_mosaic} +\alias{geom_mosaic} +\title{Mosaic Rectangles Layer for ggplot2} +\usage{ +geom_mosaic( + mapping = NULL, + data = NULL, + stat = "mosaic", + position = "identity", + ..., + na.rm = FALSE, + show.legend = TRUE, + inherit.aes = TRUE +) +} +\arguments{ +\item{mapping}{Set of aesthetic mappings created by \code{aes()}. Must specify \code{x} and \code{fill}.} + +\item{data}{The data to be displayed in this layer.} + +\item{stat}{The statistical transformation to use on the data. Defaults to \code{"rects"}.} + +\item{position}{Position adjustment. Defaults to \code{"identity"}.} + +\item{...}{Other arguments passed to \code{layer()}.} + +\item{na.rm}{Logical. Should missing values be removed?} + +\item{show.legend}{Logical. Should this layer be included in the legends?} + +\item{inherit.aes}{Logical. If \code{FALSE}, overrides default aesthetics.} +} +\value{ +A ggplot2 layer that adds mosaic rectangles to the plot. +} +\description{ +Adds a mosaic-style rectangles layer to a ggplot, visualizing the joint distribution of categorical variables. +Each rectangle's size reflects the proportion of observations for combinations of \code{x} and \code{fill}. +} +\examples{ +df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) +library(ggplot2) +ggplot(df, aes(x = RACE, fill = SEX)) + geom_rects() +} diff --git a/man/scale_type.mosaic.Rd b/man/scale_type.mosaic.Rd new file mode 100644 index 000000000..9b98318e3 --- /dev/null +++ b/man/scale_type.mosaic.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/custom_mosaic.R +\name{scale_type.mosaic} +\alias{scale_type.mosaic} +\title{Helper function for determining scales} +\usage{ +\method{scale_type}{mosaic}(x) +} +\arguments{ +\item{x}{variable} +} +\value{ +character string "productlist" +} +\description{ +Used internally to determine class of variable x +} diff --git a/man/scale_x_mosaic.Rd b/man/scale_x_mosaic.Rd new file mode 100644 index 000000000..27c62a872 --- /dev/null +++ b/man/scale_x_mosaic.Rd @@ -0,0 +1,66 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/custom_mosaic.R +\name{scale_x_mosaic} +\alias{scale_x_mosaic} +\title{Determining scales for mosaics} +\usage{ +scale_x_mosaic( + breaks = function() function(x) unique(x), + minor_breaks = NULL, + labels = function() function(x) unique(x), + na.value = NA_real_, + position = "bottom", + ... +) +} +\arguments{ +\item{breaks}{One of: +\itemize{ +\item \code{NULL} for no breaks +\item \code{waiver()} for the default breaks computed by the +\link[scales:new_transform]{transformation object} +\item A numeric vector of positions +\item A function that takes the limits as input and returns breaks +as output (e.g., a function returned by \code{\link[scales:breaks_extended]{scales::extended_breaks()}}). +Note that for position scales, limits are provided after scale expansion. +Also accepts rlang \link[rlang:as_function]{lambda} function notation. +}} + +\item{minor_breaks}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} + +\item{labels}{One of the options below. Please note that when \code{labels} is a +vector, it is highly recommended to also set the \code{breaks} argument as a +vector to protect against unintended mismatches. +\itemize{ +\item \code{NULL} for no labels +\item \code{waiver()} for the default labels computed by the +transformation object +\item A character vector giving labels (must be same length as \code{breaks}) +\item An expression vector (must be the same length as breaks). See ?plotmath for details. +\item A function that takes the breaks as input and returns labels +as output. Also accepts rlang \link[rlang:as_function]{lambda} function +notation. +}} + +\item{na.value}{Missing values will be replaced with this value.} + +\item{position}{For position scales, The position of the axis. +\code{left} or \code{right} for y axes, \code{top} or \code{bottom} for x axes.} + +\item{...}{other arguments passed to \code{continuous_scale()}.} + +\item{name}{set to pseudo waiver function \code{product_names} by default.} +} +\description{ +Determining scales for mosaics +} From 56149078c2a69976f09c170a5966fdee5b715c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:45:25 +0000 Subject: [PATCH 09/24] feat: update mosaic to working version --- R/custom_mosaic.R | 208 +++++++++++++++++++++++----------------------- R/utils.R | 1 - 2 files changed, 105 insertions(+), 104 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index f78e37264..97be4a314 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -31,8 +31,7 @@ geom_mosaic <- function(mapping = NULL, data = NULL, var_fill <- sprintf("x__fill__%s", aes_fill) mapping[[var_x]] <- mapping$x - mapping$x <- structure(1L, class = "mosaic") - # mapping[[var_fill]] <- mapping$fill + mapping$x <- structure(1L) layer <- ggplot2::layer( geom = GeomMosaic, @@ -45,10 +44,10 @@ geom_mosaic <- function(mapping = NULL, data = NULL, check.aes = FALSE, params = list(na.rm = na.rm, ...) ) - #list(layer, .scale_x_mosaic()) - layer + list(layer, .scale_x_mosaic()) } +#' @keywords internal GeomMosaic <- ggplot2::ggproto( "GeomMosaic", ggplot2::GeomRect, default_aes = ggplot2::aes( @@ -61,66 +60,16 @@ GeomMosaic <- ggplot2::ggproto( required_aes = c("xmin", "xmax", "ymin", "ymax") ) -#' Calculate Rectangle Coordinates for Mosaic Plot -#' -#' Computes the coordinates for rectangles in a mosaic plot based on combinations of `x` and `fill` variables. -#' For each unique `x` and `fill`, calculates the proportional widths and heights, stacking rectangles within each `x` group. +#' Mosaic Statistic for ggplot2 #' -#' @param data A data frame containing at least `x` and `fill` columns. -#' -#' @return A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, representing the position and size of each rectangle. +#' Implements a custom statistic for mosaic plots in ggplot2, +#' calculating coordinates and axis breaks/labels for categorical +#' variables. #' -#' @details -#' - Counts occurrences of each `x`/`fill` combination. -#' - Calculates proportions within each `x` group. -#' - Determines horizontal (`xmin`, `xmax`) and vertical (`ymin`, `ymax`) boundaries for each rectangle. -#' - Adds small padding to each boundary for visual separation. -.calculate_coordinates <- function(data) { - # Example: compute rectangles from x and y - result <- data |> - # Count combinations of X and Y - dplyr::count(x, fill) |> - # Compute total for each X group - dplyr::mutate( - .by = x, - x_total = sum(n), - prop = n / x_total - ) |> - # Change order from biggest group to smaller - dplyr::arrange(dplyr::desc(x_total), x, fill) |> - # Compute total sample size to turn counts into widths - dplyr::mutate( - N_total = dplyr::n(), - x_width = x_total / N_total - ) |> - # Convert counts to x widths - dplyr::mutate( - .by = x, - x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) - ) |> - # Compute x-min/x-max for each group - dplyr::mutate( - xmin = cumsum(dplyr::lag(x_width_last, default = 0)), - xmax = xmin + x_width - ) |> - # Compute y-min/y-max for stacked proportions - dplyr::mutate( - .by = x, - ymin = c(0, head(cumsum(prop), -1)), - ymax = cumsum(prop) - ) |> - dplyr::mutate( - xmin = xmin / max(xmax), - xmax = xmax / max(xmax), - xmin = xmin + 0.005, - xmax = xmax - 0.005, - ymin = ymin + 0.005, - ymax = ymax - 0.005 - ) |> - dplyr::select(x, fill, xmin, xmax, ymin, ymax) - result -} - +#' This statistic processes input data to compute the positions +#' and sizes of mosaic rectangles, as well as the appropriate +#' axis breaks and labels for the plot. +#' @keywords internal StatMosaic <- ggplot2::ggproto( "StatMosaic", ggplot2::Stat, @@ -130,49 +79,33 @@ StatMosaic <- ggplot2::ggproto( data }, compute_panel = function(data, scales) { + # old_x <- data$x data$x <- data[, grepl("x__", colnames(data))] result <- .calculate_coordinates(data) - scale_x <- ggplot2::ScaleContinuous - scale_x[["breaks"]] <- result |> + breaks <- result |> dplyr::distinct(x, xmin, xmax) |> dplyr::mutate(mid = (xmin + xmax) / 2) |> dplyr::pull(mid) - scale_x[["labels"]] <- result |> - dplyr::distinct(x) |> - dplyr::pull(x) + labels <- dplyr::pull(dplyr::distinct(result, x)) + result$x <- list(list2env(list(breaks = breaks[breaks != 0], labels = labels[breaks != 0]))) - result$x <- list(scale = scale_x) result$group <- 1 result$PANEL <- unique(data$PANEL) result } ) -#' Helper function for determining scales -#' -#' Used internally to determine class of variable x -#' @param x variable -#' @return character string "productlist" -#' @importFrom ggplot2 scale_type -#' @export -scale_type.mosaic <- function(x) { - # cat("checking for type productlist\n") - #browser() - "mosaic" -} - - #' Determining scales for mosaics #' #' @param name set to pseudo waiver function `product_names` by default. #' @param ... other arguments passed to `continuous_scale()`. #' @inheritParams ggplot2::continuous_scale -#' @export -scale_x_mosaic <- function(breaks = function() function(x) unique(x), +#' @keywords internal +.scale_x_mosaic <- function(breaks = function(x) unique(x), minor_breaks = NULL, - labels = function() function(x) unique(x), + labels = function(x) unique(x), na.value = NA_real_, position = "bottom", ...) { @@ -180,33 +113,43 @@ scale_x_mosaic <- function(breaks = function() function(x) unique(x), aesthetics = c( "x", "xmin", "xmax", "xend", "xintercept", "xmin_final", "xmax_final", "xlower", "xmiddle", "xupper" - ), palette = identity, breaks = breaks, minor_breaks = minor_breaks, - labels = labels, na.value = na.value, position = position, - super = ScaleContinuousProduct,, guide = ggplot2::waiver(), ... + ), + palette = identity, + breaks = breaks, + minor_breaks = minor_breaks, + labels = labels, + na.value = na.value, + position = position, + super = ScaleContinuousMosaic,, + guide = ggplot2::waiver(), + ... ) } #' @rdname dot-scale_x_mosaic -ScaleContinuousProduct <- ggplot2::ggproto( - "ScaleContinuousProduct", ggplot2::ScaleContinuousPosition, +#' @importFrom ggplot2 ggproto ScaleContinuousPosition +#' @keywords internal +ScaleContinuousMosaic <- ggproto( + "ScaleContinuousMosaic", ScaleContinuousPosition, train =function(self, x) { + if (length(x) == 0) { + return() + } if (is.list(x)) { - x <- x[[1]] - if ("Scale" %in% class(x)) { - # re-assign the scale values now that we have the information - but only if necessary - if (is.function(self$breaks)) self$breaks <- x$breaks - if (is.function(self$labels)) self$labels <- x$labels - return(NULL) - } + scale_x <- x[[1]] + # re-assign the scale values now that we have the information - but only if necessary + if (is.function(self$breaks)) self$breaks <- scale_x$breaks + if (is.function(self$labels)) self$labels <- as.vector(scale_x$labels) + return(NULL) } - if (self$is.discrete(x)) { + if (is.discrete(x)) { self$range$train(x=c(0,1)) return(NULL) } - self$range$train(x) + self$range$train(x, call = self$call) }, map = function(self, x, limits = self$get_limits()) { - if (self$is.discrete(x)) return(x) + if (is.discrete(x)) return(x) if (is.list(x)) return(0) # need a number scaled <- as.numeric(self$oob(x, limits)) ifelse(!is.na(scaled), scaled, self$na.value) @@ -220,8 +163,67 @@ ScaleContinuousProduct <- ggplot2::ggproto( title <- self$product_name } else title - }, - is.discrete = function(self, x) is.factor(x) || is.character(x) || is.logical(x) + } ) +is.discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) - +#' Calculate Rectangle Coordinates for Mosaic Plot +#' +#' Computes the coordinates for rectangles in a mosaic plot based on combinations of `x` and `fill` variables. +#' For each unique `x` and `fill`, calculates the proportional widths and heights, stacking rectangles within each `x` group. +#' +#' @param data A data frame containing at least `x` and `fill` columns. +#' +#' @return A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, representing the position and size of each rectangle. +#' +#' @details +#' - Counts occurrences of each `x`/`fill` combination. +#' - Calculates proportions within each `x` group. +#' - Determines horizontal (`xmin`, `xmax`) and vertical (`ymin`, `ymax`) boundaries for each rectangle. +#' - Adds small padding to each boundary for visual separation. +#' @keywords internal +.calculate_coordinates <- function(data) { + # Example: compute rectangles from x and y + result <- data |> + # Count combinations of X and Y + dplyr::count(x, fill, .drop = FALSE) |> + # Compute total for each X group + dplyr::mutate( + .by = x, + x_total = sum(n), + prop = n / x_total, + prop = dplyr::if_else(is.nan(prop), 0, prop) + ) |> + dplyr::arrange(dplyr::desc(x_total), x, fill) |> + # Compute total sample size to turn counts into widths + dplyr::mutate( + N_total = dplyr::n(), + x_width = x_total / N_total + ) |> + # Convert counts to x widths + dplyr::mutate( + .by = x, + x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) + ) |> + # Compute x-min/x-max for each group + dplyr::mutate( + xmin = cumsum(dplyr::lag(x_width_last, default = 0)), + xmax = xmin + x_width + ) |> + # Compute y-min/y-max for stacked proportions + dplyr::mutate( + .by = x, + ymin = c(0, head(cumsum(prop), -1)), + ymax = cumsum(prop) + ) |> + dplyr::mutate( + xmin = xmin / max(xmax), + xmax = xmax / max(xmax), + xmin = dplyr::if_else(n == 0, 0, xmin + 0.005), + xmax = dplyr::if_else(n == 0, 0, xmax - 0.005), + ymin = dplyr::if_else(n == 0, 0, ymin + 0.005), + ymax = dplyr::if_else(n == 0, 0, ymax - 0.005) + ) |> + dplyr::select(x, fill, xmin, xmax, ymin, ymax, .n = n) + result +} \ No newline at end of file diff --git a/R/utils.R b/R/utils.R index d625746a5..5f462ba0d 100644 --- a/R/utils.R +++ b/R/utils.R @@ -287,7 +287,6 @@ srv_decorate_teal_data <- function(id, data, decorators, expr) { reactive({ req(decorated_output()) - browser() if (no_expr) { decorated_output() } else { From ff1519f5a258641328c7700d2b5484f6ef005741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:51:30 +0000 Subject: [PATCH 10/24] docs: update docs and remove import --- NAMESPACE | 3 --- R/custom_mosaic.R | 5 ++-- man/StatMosaic.Rd | 23 +++++++++++++++++++ man/dot-calculate_coordinates.Rd | 1 + ...cale_x_mosaic.Rd => dot-scale_x_mosaic.Rd} | 18 +++++++++++---- man/scale_type.mosaic.Rd | 17 -------------- 6 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 man/StatMosaic.Rd rename man/{scale_x_mosaic.Rd => dot-scale_x_mosaic.Rd} (85%) delete mode 100644 man/scale_type.mosaic.Rd diff --git a/NAMESPACE b/NAMESPACE index 0392296ef..e86cf0f54 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,11 +8,9 @@ S3method(create_sparklines,default) S3method(create_sparklines,factor) S3method(create_sparklines,logical) S3method(create_sparklines,numeric) -S3method(scale_type,mosaic) export(add_facet_labels) export(geom_mosaic) export(get_scatterplotmatrix_stats) -export(scale_x_mosaic) export(tm_a_pca) export(tm_a_regression) export(tm_data_table) @@ -33,5 +31,4 @@ import(shiny) import(teal) import(teal.transform) importFrom(dplyr,"%>%") -importFrom(ggplot2,scale_type) importFrom(lifecycle,deprecated) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 97be4a314..80691ce9d 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -127,10 +127,9 @@ StatMosaic <- ggplot2::ggproto( } #' @rdname dot-scale_x_mosaic -#' @importFrom ggplot2 ggproto ScaleContinuousPosition #' @keywords internal -ScaleContinuousMosaic <- ggproto( - "ScaleContinuousMosaic", ScaleContinuousPosition, +ScaleContinuousMosaic <- ggplot2::ggproto( + "ScaleContinuousMosaic", ggplot2::ScaleContinuousPosition, train =function(self, x) { if (length(x) == 0) { return() diff --git a/man/StatMosaic.Rd b/man/StatMosaic.Rd new file mode 100644 index 000000000..fd8d091de --- /dev/null +++ b/man/StatMosaic.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/custom_mosaic.R +\docType{data} +\name{StatMosaic} +\alias{StatMosaic} +\title{Mosaic Statistic for ggplot2} +\format{ +An object of class \code{StatMosaic} (inherits from \code{Stat}, \code{ggproto}, \code{gg}) of length 4. +} +\usage{ +StatMosaic +} +\description{ +Implements a custom statistic for mosaic plots in ggplot2, +calculating coordinates and axis breaks/labels for categorical +variables. +} +\details{ +This statistic processes input data to compute the positions +and sizes of mosaic rectangles, as well as the appropriate +axis breaks and labels for the plot. +} +\keyword{internal} diff --git a/man/dot-calculate_coordinates.Rd b/man/dot-calculate_coordinates.Rd index f17830c9c..0118bc49f 100644 --- a/man/dot-calculate_coordinates.Rd +++ b/man/dot-calculate_coordinates.Rd @@ -24,3 +24,4 @@ For each unique \code{x} and \code{fill}, calculates the proportional widths and \item Adds small padding to each boundary for visual separation. } } +\keyword{internal} diff --git a/man/scale_x_mosaic.Rd b/man/dot-scale_x_mosaic.Rd similarity index 85% rename from man/scale_x_mosaic.Rd rename to man/dot-scale_x_mosaic.Rd index 27c62a872..4006a6281 100644 --- a/man/scale_x_mosaic.Rd +++ b/man/dot-scale_x_mosaic.Rd @@ -1,17 +1,24 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/custom_mosaic.R -\name{scale_x_mosaic} -\alias{scale_x_mosaic} +\docType{data} +\name{.scale_x_mosaic} +\alias{.scale_x_mosaic} +\alias{ScaleContinuousMosaic} \title{Determining scales for mosaics} +\format{ +An object of class \code{ScaleContinuousMosaic} (inherits from \code{ScaleContinuousPosition}, \code{ScaleContinuous}, \code{Scale}, \code{ggproto}, \code{gg}) of length 5. +} \usage{ -scale_x_mosaic( - breaks = function() function(x) unique(x), +.scale_x_mosaic( + breaks = function(x) unique(x), minor_breaks = NULL, - labels = function() function(x) unique(x), + labels = function(x) unique(x), na.value = NA_real_, position = "bottom", ... ) + +ScaleContinuousMosaic } \arguments{ \item{breaks}{One of: @@ -64,3 +71,4 @@ notation. \description{ Determining scales for mosaics } +\keyword{internal} diff --git a/man/scale_type.mosaic.Rd b/man/scale_type.mosaic.Rd deleted file mode 100644 index 9b98318e3..000000000 --- a/man/scale_type.mosaic.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/custom_mosaic.R -\name{scale_type.mosaic} -\alias{scale_type.mosaic} -\title{Helper function for determining scales} -\usage{ -\method{scale_type}{mosaic}(x) -} -\arguments{ -\item{x}{variable} -} -\value{ -character string "productlist" -} -\description{ -Used internally to determine class of variable x -} From 2fd5f4c72d244108355f86ec79d8370ba4f746ab Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:53:41 +0000 Subject: [PATCH 11/24] [skip style] [skip vbump] Restyle files --- R/custom_mosaic.R | 48 +++++++++++++++++++++++++--------------------- R/tm_g_bivariate.R | 2 +- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 80691ce9d..8a2bb22ba 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -17,14 +17,15 @@ #' @examples #' df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) #' library(ggplot2) -#' ggplot(df, aes(x = RACE, fill = SEX)) + geom_rects() +#' ggplot(df, aes(x = RACE, fill = SEX)) + +#' geom_rects() #' @export geom_mosaic <- function(mapping = NULL, data = NULL, - stat = "mosaic", position = "identity", - ..., - na.rm = FALSE, - show.legend = TRUE, - inherit.aes = TRUE) { + stat = "mosaic", position = "identity", + ..., + na.rm = FALSE, + show.legend = TRUE, + inherit.aes = TRUE) { aes_x <- list(rlang::quo_get_expr(mapping$x)) var_x <- sprintf("x__%s", as.character(aes_x)) aes_fill <- rlang::quo_text(mapping$fill) @@ -72,9 +73,7 @@ GeomMosaic <- ggplot2::ggproto( #' @keywords internal StatMosaic <- ggplot2::ggproto( "StatMosaic", ggplot2::Stat, - required_aes = c("x", "fill"), - compute_group = function(data, scales) { data }, @@ -104,11 +103,11 @@ StatMosaic <- ggplot2::ggproto( #' @inheritParams ggplot2::continuous_scale #' @keywords internal .scale_x_mosaic <- function(breaks = function(x) unique(x), - minor_breaks = NULL, - labels = function(x) unique(x), - na.value = NA_real_, - position = "bottom", - ...) { + minor_breaks = NULL, + labels = function(x) unique(x), + na.value = NA_real_, + position = "bottom", + ...) { ggplot2::continuous_scale( aesthetics = c( "x", "xmin", "xmax", "xend", "xintercept", "xmin_final", "xmax_final", @@ -120,7 +119,7 @@ StatMosaic <- ggplot2::ggproto( labels = labels, na.value = na.value, position = position, - super = ScaleContinuousMosaic,, + super = ScaleContinuousMosaic, , guide = ggplot2::waiver(), ... ) @@ -130,9 +129,9 @@ StatMosaic <- ggplot2::ggproto( #' @keywords internal ScaleContinuousMosaic <- ggplot2::ggproto( "ScaleContinuousMosaic", ggplot2::ScaleContinuousPosition, - train =function(self, x) { + train = function(self, x) { if (length(x) == 0) { - return() + return() } if (is.list(x)) { scale_x <- x[[1]] @@ -142,26 +141,31 @@ ScaleContinuousMosaic <- ggplot2::ggproto( return(NULL) } if (is.discrete(x)) { - self$range$train(x=c(0,1)) + self$range$train(x = c(0, 1)) return(NULL) } self$range$train(x, call = self$call) }, map = function(self, x, limits = self$get_limits()) { - if (is.discrete(x)) return(x) - if (is.list(x)) return(0) # need a number + if (is.discrete(x)) { + return(x) + } + if (is.list(x)) { + return(0) + } # need a number scaled <- as.numeric(self$oob(x, limits)) ifelse(!is.na(scaled), scaled, self$na.value) }, dimension = function(self, expand = c(0, 0)) { - c(-0.05,1.05) + c(-0.05, 1.05) }, make_title = function(..., self) { title <- ggplot2::ggproto_parent(ggplot2::ScaleContinuousPosition, self)$make_title(...) if (isTRUE(title %in% self$aesthetics)) { title <- self$product_name + } else { + title } - else title } ) is.discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) @@ -225,4 +229,4 @@ is.discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) ) |> dplyr::select(x, fill, xmin, xmax, ymin, ymax, .n = n) result -} \ No newline at end of file +} diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index 96a4725aa..20de5387c 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -736,7 +736,7 @@ srv_g_bivariate <- function(id, ) set_chunk_dims(pws, decorated_output_q_facets) - }) + }) } # Get Substituted ggplot call From 9300a7a6a30a12c2ba0b6929e4737634d4f031f1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:00:35 +0000 Subject: [PATCH 12/24] [skip roxygen] [skip vbump] Roxygen Man Pages Auto Update --- man/geom_mosaic.Rd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd index 679c6b4b3..f122dcc8d 100644 --- a/man/geom_mosaic.Rd +++ b/man/geom_mosaic.Rd @@ -42,5 +42,6 @@ Each rectangle's size reflects the proportion of observations for combinations o \examples{ df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) library(ggplot2) -ggplot(df, aes(x = RACE, fill = SEX)) + geom_rects() +ggplot(df, aes(x = RACE, fill = SEX)) + + geom_rects() } From 3dae9e16360f6b75593a57c80e81d419f9c6f178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:58:13 +0000 Subject: [PATCH 13/24] docs: clean up --- R/custom_mosaic.R | 27 ++++----------------------- man/StatMosaic.Rd | 23 ----------------------- man/dot-calculate_coordinates.Rd | 27 --------------------------- man/dot-scale_x_mosaic.Rd | 7 ------- man/geom_mosaic.Rd | 14 ++++++++++++++ 5 files changed, 18 insertions(+), 80 deletions(-) delete mode 100644 man/StatMosaic.Rd delete mode 100644 man/dot-calculate_coordinates.Rd diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 8a2bb22ba..78ff21774 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -61,24 +61,12 @@ GeomMosaic <- ggplot2::ggproto( required_aes = c("xmin", "xmax", "ymin", "ymax") ) -#' Mosaic Statistic for ggplot2 -#' -#' Implements a custom statistic for mosaic plots in ggplot2, -#' calculating coordinates and axis breaks/labels for categorical -#' variables. -#' -#' This statistic processes input data to compute the positions -#' and sizes of mosaic rectangles, as well as the appropriate -#' axis breaks and labels for the plot. #' @keywords internal StatMosaic <- ggplot2::ggproto( "StatMosaic", ggplot2::Stat, required_aes = c("x", "fill"), - compute_group = function(data, scales) { - data - }, + compute_group = function(data, scales) data, compute_panel = function(data, scales) { - # old_x <- data$x data$x <- data[, grepl("x__", colnames(data))] result <- .calculate_coordinates(data) @@ -125,7 +113,6 @@ StatMosaic <- ggplot2::ggproto( ) } -#' @rdname dot-scale_x_mosaic #' @keywords internal ScaleContinuousMosaic <- ggplot2::ggproto( "ScaleContinuousMosaic", ggplot2::ScaleContinuousPosition, @@ -170,20 +157,14 @@ ScaleContinuousMosaic <- ggplot2::ggproto( ) is.discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) -#' Calculate Rectangle Coordinates for Mosaic Plot -#' +#' @describeIn geom_mosaic #' Computes the coordinates for rectangles in a mosaic plot based on combinations of `x` and `fill` variables. #' For each unique `x` and `fill`, calculates the proportional widths and heights, stacking rectangles within each `x` group. #' -#' @param data A data frame containing at least `x` and `fill` columns. +#' ### Value #' -#' @return A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, representing the position and size of each rectangle. +#' A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, representing the position and size of each rectangle. #' -#' @details -#' - Counts occurrences of each `x`/`fill` combination. -#' - Calculates proportions within each `x` group. -#' - Determines horizontal (`xmin`, `xmax`) and vertical (`ymin`, `ymax`) boundaries for each rectangle. -#' - Adds small padding to each boundary for visual separation. #' @keywords internal .calculate_coordinates <- function(data) { # Example: compute rectangles from x and y diff --git a/man/StatMosaic.Rd b/man/StatMosaic.Rd deleted file mode 100644 index fd8d091de..000000000 --- a/man/StatMosaic.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/custom_mosaic.R -\docType{data} -\name{StatMosaic} -\alias{StatMosaic} -\title{Mosaic Statistic for ggplot2} -\format{ -An object of class \code{StatMosaic} (inherits from \code{Stat}, \code{ggproto}, \code{gg}) of length 4. -} -\usage{ -StatMosaic -} -\description{ -Implements a custom statistic for mosaic plots in ggplot2, -calculating coordinates and axis breaks/labels for categorical -variables. -} -\details{ -This statistic processes input data to compute the positions -and sizes of mosaic rectangles, as well as the appropriate -axis breaks and labels for the plot. -} -\keyword{internal} diff --git a/man/dot-calculate_coordinates.Rd b/man/dot-calculate_coordinates.Rd deleted file mode 100644 index 0118bc49f..000000000 --- a/man/dot-calculate_coordinates.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/custom_mosaic.R -\name{.calculate_coordinates} -\alias{.calculate_coordinates} -\title{Calculate Rectangle Coordinates for Mosaic Plot} -\usage{ -.calculate_coordinates(data) -} -\arguments{ -\item{data}{A data frame containing at least \code{x} and \code{fill} columns.} -} -\value{ -A data frame with columns: \code{x}, \code{fill}, \code{xmin}, \code{xmax}, \code{ymin}, \code{ymax}, representing the position and size of each rectangle. -} -\description{ -Computes the coordinates for rectangles in a mosaic plot based on combinations of \code{x} and \code{fill} variables. -For each unique \code{x} and \code{fill}, calculates the proportional widths and heights, stacking rectangles within each \code{x} group. -} -\details{ -\itemize{ -\item Counts occurrences of each \code{x}/\code{fill} combination. -\item Calculates proportions within each \code{x} group. -\item Determines horizontal (\code{xmin}, \code{xmax}) and vertical (\code{ymin}, \code{ymax}) boundaries for each rectangle. -\item Adds small padding to each boundary for visual separation. -} -} -\keyword{internal} diff --git a/man/dot-scale_x_mosaic.Rd b/man/dot-scale_x_mosaic.Rd index 4006a6281..d4a5397a6 100644 --- a/man/dot-scale_x_mosaic.Rd +++ b/man/dot-scale_x_mosaic.Rd @@ -1,13 +1,8 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/custom_mosaic.R -\docType{data} \name{.scale_x_mosaic} \alias{.scale_x_mosaic} -\alias{ScaleContinuousMosaic} \title{Determining scales for mosaics} -\format{ -An object of class \code{ScaleContinuousMosaic} (inherits from \code{ScaleContinuousPosition}, \code{ScaleContinuous}, \code{Scale}, \code{ggproto}, \code{gg}) of length 5. -} \usage{ .scale_x_mosaic( breaks = function(x) unique(x), @@ -17,8 +12,6 @@ An object of class \code{ScaleContinuousMosaic} (inherits from \code{ScaleContin position = "bottom", ... ) - -ScaleContinuousMosaic } \arguments{ \item{breaks}{One of: diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd index f122dcc8d..fdad53e46 100644 --- a/man/geom_mosaic.Rd +++ b/man/geom_mosaic.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/custom_mosaic.R \name{geom_mosaic} \alias{geom_mosaic} +\alias{.calculate_coordinates} \title{Mosaic Rectangles Layer for ggplot2} \usage{ geom_mosaic( @@ -14,6 +15,8 @@ geom_mosaic( show.legend = TRUE, inherit.aes = TRUE ) + +.calculate_coordinates(data) } \arguments{ \item{mapping}{Set of aesthetic mappings created by \code{aes()}. Must specify \code{x} and \code{fill}.} @@ -39,9 +42,20 @@ A ggplot2 layer that adds mosaic rectangles to the plot. Adds a mosaic-style rectangles layer to a ggplot, visualizing the joint distribution of categorical variables. Each rectangle's size reflects the proportion of observations for combinations of \code{x} and \code{fill}. } +\section{Functions}{ +\itemize{ +\item \code{.calculate_coordinates()}: Computes the coordinates for rectangles in a mosaic plot based on combinations of \code{x} and \code{fill} variables. +For each unique \code{x} and \code{fill}, calculates the proportional widths and heights, stacking rectangles within each \code{x} group. +\subsection{Value}{ + +A data frame with columns: \code{x}, \code{fill}, \code{xmin}, \code{xmax}, \code{ymin}, \code{ymax}, representing the position and size of each rectangle. +} + +}} \examples{ df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) library(ggplot2) ggplot(df, aes(x = RACE, fill = SEX)) + geom_rects() } +\keyword{internal} From c06f9f99cce36f0bec2c3a0b425f3a2fc86d4407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:04:23 +0000 Subject: [PATCH 14/24] chore: lint code --- R/custom_mosaic.R | 37 ++++++++++++++++++++++--------------- man/geom_mosaic.Rd | 15 ++++++++++----- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 78ff21774..b2f02039f 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -1,7 +1,9 @@ #' Mosaic Rectangles Layer for ggplot2 #' -#' Adds a mosaic-style rectangles layer to a ggplot, visualizing the joint distribution of categorical variables. -#' Each rectangle's size reflects the proportion of observations for combinations of `x` and `fill`. +#' Adds a mosaic-style rectangles layer to a ggplot, visualizing the +#' joint distribution of categorical variables. +#' Each rectangle's size reflects the proportion of observations for +#' combinations of `x` and `fill`. #' #' @param mapping Set of aesthetic mappings created by `aes()`. Must specify `x` and `fill`. #' @param data The data to be displayed in this layer. @@ -23,9 +25,9 @@ geom_mosaic <- function(mapping = NULL, data = NULL, stat = "mosaic", position = "identity", ..., - na.rm = FALSE, - show.legend = TRUE, - inherit.aes = TRUE) { + na.rm = FALSE, # nolint: object_name_linter. + show.legend = TRUE, # nolint: object_name_linter. + inherit.aes = TRUE) { # nolint: object_name_linter. aes_x <- list(rlang::quo_get_expr(mapping$x)) var_x <- sprintf("x__%s", as.character(aes_x)) aes_fill <- rlang::quo_text(mapping$fill) @@ -49,7 +51,7 @@ geom_mosaic <- function(mapping = NULL, data = NULL, } #' @keywords internal -GeomMosaic <- ggplot2::ggproto( +GeomMosaic <- ggplot2::ggproto( # nolint: object_name_linter. "GeomMosaic", ggplot2::GeomRect, default_aes = ggplot2::aes( colour = NA, linewidth = 0.5, linetype = 1, alpha = 1, fill = "grey30" @@ -62,7 +64,7 @@ GeomMosaic <- ggplot2::ggproto( ) #' @keywords internal -StatMosaic <- ggplot2::ggproto( +StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter. "StatMosaic", ggplot2::Stat, required_aes = c("x", "fill"), compute_group = function(data, scales) data, @@ -93,7 +95,7 @@ StatMosaic <- ggplot2::ggproto( .scale_x_mosaic <- function(breaks = function(x) unique(x), minor_breaks = NULL, labels = function(x) unique(x), - na.value = NA_real_, + na.value = NA_real_, # nolint: object_name_linter. position = "bottom", ...) { ggplot2::continuous_scale( @@ -114,7 +116,7 @@ StatMosaic <- ggplot2::ggproto( } #' @keywords internal -ScaleContinuousMosaic <- ggplot2::ggproto( +ScaleContinuousMosaic <- ggplot2::ggproto( # nolint: object_name_linter. "ScaleContinuousMosaic", ggplot2::ScaleContinuousPosition, train = function(self, x) { if (length(x) == 0) { @@ -127,14 +129,14 @@ ScaleContinuousMosaic <- ggplot2::ggproto( if (is.function(self$labels)) self$labels <- as.vector(scale_x$labels) return(NULL) } - if (is.discrete(x)) { + if (is_discrete(x)) { self$range$train(x = c(0, 1)) return(NULL) } self$range$train(x, call = self$call) }, map = function(self, x, limits = self$get_limits()) { - if (is.discrete(x)) { + if (is_discrete(x)) { return(x) } if (is.list(x)) { @@ -155,15 +157,20 @@ ScaleContinuousMosaic <- ggplot2::ggproto( } } ) -is.discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) + +#' @noRd +is_discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) #' @describeIn geom_mosaic -#' Computes the coordinates for rectangles in a mosaic plot based on combinations of `x` and `fill` variables. -#' For each unique `x` and `fill`, calculates the proportional widths and heights, stacking rectangles within each `x` group. +#' Computes the coordinates for rectangles in a mosaic plot based +#' on combinations of `x` and `fill` variables. +#' For each unique `x` and `fill`, calculates the proportional +#' widths and heights, stacking rectangles within each `x` group. #' #' ### Value #' -#' A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, representing the position and size of each rectangle. +#' A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`, +#' representing the position and size of each rectangle. #' #' @keywords internal .calculate_coordinates <- function(data) { diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd index fdad53e46..63d720cbe 100644 --- a/man/geom_mosaic.Rd +++ b/man/geom_mosaic.Rd @@ -39,16 +39,21 @@ geom_mosaic( A ggplot2 layer that adds mosaic rectangles to the plot. } \description{ -Adds a mosaic-style rectangles layer to a ggplot, visualizing the joint distribution of categorical variables. -Each rectangle's size reflects the proportion of observations for combinations of \code{x} and \code{fill}. +Adds a mosaic-style rectangles layer to a ggplot, visualizing the +joint distribution of categorical variables. +Each rectangle's size reflects the proportion of observations for +combinations of \code{x} and \code{fill}. } \section{Functions}{ \itemize{ -\item \code{.calculate_coordinates()}: Computes the coordinates for rectangles in a mosaic plot based on combinations of \code{x} and \code{fill} variables. -For each unique \code{x} and \code{fill}, calculates the proportional widths and heights, stacking rectangles within each \code{x} group. +\item \code{.calculate_coordinates()}: Computes the coordinates for rectangles in a mosaic plot based +on combinations of \code{x} and \code{fill} variables. +For each unique \code{x} and \code{fill}, calculates the proportional +widths and heights, stacking rectangles within each \code{x} group. \subsection{Value}{ -A data frame with columns: \code{x}, \code{fill}, \code{xmin}, \code{xmax}, \code{ymin}, \code{ymax}, representing the position and size of each rectangle. +A data frame with columns: \code{x}, \code{fill}, \code{xmin}, \code{xmax}, \code{ymin}, \code{ymax}, +representing the position and size of each rectangle. } }} From eacc78d66f916aca34b654fb54e700528b526506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:09:01 +0000 Subject: [PATCH 15/24] chore: cleanup and adds disclaimer on top --- R/custom_mosaic.R | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index b2f02039f..0f7b96e21 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -1,3 +1,8 @@ +# minimal implementation of ggplot2 mosaic after ggmosaic was archived in CRAN +# +# This was heavily inspired by github.com/haleyjeppson/ggmosaic package but +# simplified to only support 2 categorical variables + #' Mosaic Rectangles Layer for ggplot2 #' #' Adds a mosaic-style rectangles layer to a ggplot, visualizing the @@ -145,17 +150,7 @@ ScaleContinuousMosaic <- ggplot2::ggproto( # nolint: object_name_linter. scaled <- as.numeric(self$oob(x, limits)) ifelse(!is.na(scaled), scaled, self$na.value) }, - dimension = function(self, expand = c(0, 0)) { - c(-0.05, 1.05) - }, - make_title = function(..., self) { - title <- ggplot2::ggproto_parent(ggplot2::ScaleContinuousPosition, self)$make_title(...) - if (isTRUE(title %in% self$aesthetics)) { - title <- self$product_name - } else { - title - } - } + dimension = function(self, expand = c(0, 0)) c(-0.05, 1.05) ) #' @noRd From 296ab1505f8b48b5c5e3ae7697a63abf17b536c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:54:07 +0000 Subject: [PATCH 16/24] fix: update tests with bivariate geom plot call --- tests/testthat/test_bivariate_ggplot_call.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test_bivariate_ggplot_call.R b/tests/testthat/test_bivariate_ggplot_call.R index d65d122f5..618ada9e2 100644 --- a/tests/testthat/test_bivariate_ggplot_call.R +++ b/tests/testthat/test_bivariate_ggplot_call.R @@ -31,7 +31,7 @@ testthat::describe("bivariate_ggplot_call with arguments:", { bivariate_ggplot_call(x[[1]], x[[2]], data_name = "ANL", x = "x", y = "y"), width.cutoff = 300 ), - "mosaic_data <- ", + "teal.modules.general::geom_mosaic", all = FALSE ) }) From f0917e5d2dd9c30ea876ff76553569746f947276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:17:39 +0100 Subject: [PATCH 17/24] Update R/custom_mosaic.R MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lluís Revilla <185338939+llrs-roche@users.noreply.github.com> Signed-off-by: André Veríssimo <211358+averissimo@users.noreply.github.com> --- R/custom_mosaic.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index 0f7b96e21..c1b14bacb 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -25,7 +25,7 @@ #' df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) #' library(ggplot2) #' ggplot(df, aes(x = RACE, fill = SEX)) + -#' geom_rects() +#' geom_mosaic() #' @export geom_mosaic <- function(mapping = NULL, data = NULL, stat = "mosaic", position = "identity", From a05f6ddfa400563a93d8ab55fb6bd43ec416e11d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:24:18 +0000 Subject: [PATCH 18/24] [skip roxygen] [skip vbump] Roxygen Man Pages Auto Update --- man/geom_mosaic.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd index 63d720cbe..f0eb14173 100644 --- a/man/geom_mosaic.Rd +++ b/man/geom_mosaic.Rd @@ -61,6 +61,6 @@ representing the position and size of each rectangle. df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) library(ggplot2) ggplot(df, aes(x = RACE, fill = SEX)) + - geom_rects() + geom_mosaic() } \keyword{internal} From dcb15072c58e61395f04f36df8186877bfed1dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:29:26 +0000 Subject: [PATCH 19/24] fix: examples --- R/custom_mosaic.R | 21 ++++++++++++++------- man/geom_mosaic.Rd | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index c1b14bacb..f2e737ce4 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -24,8 +24,8 @@ #' @examples #' df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) #' library(ggplot2) -#' ggplot(df, aes(x = RACE, fill = SEX)) + -#' geom_mosaic() +#' ggplot(df) + +#' geom_mosaic(aes(x = RACE, fill = SEX)) #' @export geom_mosaic <- function(mapping = NULL, data = NULL, stat = "mosaic", position = "identity", @@ -33,12 +33,19 @@ geom_mosaic <- function(mapping = NULL, data = NULL, na.rm = FALSE, # nolint: object_name_linter. show.legend = TRUE, # nolint: object_name_linter. inherit.aes = TRUE) { # nolint: object_name_linter. - aes_x <- list(rlang::quo_get_expr(mapping$x)) - var_x <- sprintf("x__%s", as.character(aes_x)) - aes_fill <- rlang::quo_text(mapping$fill) - var_fill <- sprintf("x__fill__%s", aes_fill) - mapping[[var_x]] <- mapping$x + aes_x <- mapping$x + if (!is.null(aes_x)) { + aes_x <- list(rlang::quo_get_expr(mapping$x)) + var_x <- paste0("x__", as.character(aes_x)) + mapping[[var_x]] <- mapping$x + } + + aes_fill <- mapping$fill + if (!is.null(aes_fill)) { + aes_fill <- rlang::quo_text(mapping$fill) + } + mapping$x <- structure(1L) layer <- ggplot2::layer( diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd index f0eb14173..01524681f 100644 --- a/man/geom_mosaic.Rd +++ b/man/geom_mosaic.Rd @@ -60,7 +60,7 @@ representing the position and size of each rectangle. \examples{ df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F")) library(ggplot2) -ggplot(df, aes(x = RACE, fill = SEX)) + - geom_mosaic() +ggplot(df) + + geom_mosaic(aes(x = RACE, fill = SEX)) } \keyword{internal} From 07e3ff48a5abb071ec71449566a83991645f19fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:47:05 +0000 Subject: [PATCH 20/24] feat: use simpler expressions --- R/custom_mosaic.R | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/R/custom_mosaic.R b/R/custom_mosaic.R index f2e737ce4..b4b65f34f 100644 --- a/R/custom_mosaic.R +++ b/R/custom_mosaic.R @@ -84,12 +84,8 @@ StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter. data$x <- data[, grepl("x__", colnames(data))] result <- .calculate_coordinates(data) - breaks <- result |> - dplyr::distinct(x, xmin, xmax) |> - dplyr::mutate(mid = (xmin + xmax) / 2) |> - dplyr::pull(mid) - - labels <- dplyr::pull(dplyr::distinct(result, x)) + breaks <- breaks <- unique(with(result, (xmin + xmax) / 2)) + labels <- unique(result$x) result$x <- list(list2env(list(breaks = breaks[breaks != 0], labels = labels[breaks != 0]))) result$group <- 1 From 60ee4e2b4da85e175f92da06759414629d00894c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:10:49 +0000 Subject: [PATCH 21/24] chore: rename of file to better describe contents --- R/{custom_mosaic.R => geom_mosaic.R} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename R/{custom_mosaic.R => geom_mosaic.R} (100%) diff --git a/R/custom_mosaic.R b/R/geom_mosaic.R similarity index 100% rename from R/custom_mosaic.R rename to R/geom_mosaic.R From 3eb774c71222e1b952f6b0ee0de13d1d56491517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:16:53 +0000 Subject: [PATCH 22/24] chore: update ggplot2 and dplyr versions --- DESCRIPTION | 4 ++-- R/geom_mosaic.R | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9ba78ad5d..e0cee387d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,7 +23,7 @@ URL: https://insightsengineering.github.io/teal.modules.general/, BugReports: https://github.com/insightsengineering/teal.modules.general/issues Depends: - ggplot2 (>= 3.4.0), + ggplot2 (>= 3.5.0), R (>= 4.1), shiny (>= 1.8.1), teal (>= 1.0.0.9003), @@ -32,7 +32,7 @@ Imports: bslib (>= 0.8.0), checkmate (>= 2.1.0), colourpicker (>= 1.3.0), - dplyr (>= 1.0.5), + dplyr (>= 1.1.0), DT (>= 0.13), forcats (>= 1.0.0), generics (>= 0.1.3), diff --git a/R/geom_mosaic.R b/R/geom_mosaic.R index b4b65f34f..a672e500a 100644 --- a/R/geom_mosaic.R +++ b/R/geom_mosaic.R @@ -84,7 +84,7 @@ StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter. data$x <- data[, grepl("x__", colnames(data))] result <- .calculate_coordinates(data) - breaks <- breaks <- unique(with(result, (xmin + xmax) / 2)) + breaks <- unique(with(result, (xmin + xmax) / 2)) labels <- unique(result$x) result$x <- list(list2env(list(breaks = breaks[breaks != 0], labels = labels[breaks != 0]))) From e88600b730d56e4a0230102bf6c782b50260d812 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:25:00 +0000 Subject: [PATCH 23/24] [skip roxygen] [skip vbump] Roxygen Man Pages Auto Update --- man/dot-scale_x_mosaic.Rd | 2 +- man/geom_mosaic.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/man/dot-scale_x_mosaic.Rd b/man/dot-scale_x_mosaic.Rd index d4a5397a6..6e1c17c5c 100644 --- a/man/dot-scale_x_mosaic.Rd +++ b/man/dot-scale_x_mosaic.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/custom_mosaic.R +% Please edit documentation in R/geom_mosaic.R \name{.scale_x_mosaic} \alias{.scale_x_mosaic} \title{Determining scales for mosaics} diff --git a/man/geom_mosaic.Rd b/man/geom_mosaic.Rd index 01524681f..dadbb4902 100644 --- a/man/geom_mosaic.Rd +++ b/man/geom_mosaic.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/custom_mosaic.R +% Please edit documentation in R/geom_mosaic.R \name{geom_mosaic} \alias{geom_mosaic} \alias{.calculate_coordinates} From 5ca3ffdb840eb97def1e1fef2d460931692a4352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= <211358+averissimo@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:02:12 +0000 Subject: [PATCH 24/24] fix: .data pronoun and bug with breaks/labels --- NAMESPACE | 1 + R/geom_mosaic.R | 62 ++++++++++++++++++++++----------------- R/teal.modules.general.R | 3 +- R/tm_g_association.R | 2 +- man/dot-scale_x_mosaic.Rd | 51 +++++++------------------------- 5 files changed, 48 insertions(+), 71 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index b48bd9068..9a8bb56e3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -34,4 +34,5 @@ import(shiny) import(teal) import(teal.transform) importFrom(dplyr,"%>%") +importFrom(dplyr,.data) importFrom(lifecycle,deprecated) diff --git a/R/geom_mosaic.R b/R/geom_mosaic.R index a672e500a..6ecfddb4b 100644 --- a/R/geom_mosaic.R +++ b/R/geom_mosaic.R @@ -84,8 +84,9 @@ StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter. data$x <- data[, grepl("x__", colnames(data))] result <- .calculate_coordinates(data) - breaks <- unique(with(result, (xmin + xmax) / 2)) - labels <- unique(result$x) + results_non_zero <- result[result$.n != 0, ] + breaks <- unique(with(results_non_zero, (xmin + xmax) / 2)) + labels <- unique(results_non_zero$x) result$x <- list(list2env(list(breaks = breaks[breaks != 0], labels = labels[breaks != 0]))) result$group <- 1 @@ -96,13 +97,20 @@ StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter. #' Determining scales for mosaics #' -#' @param name set to pseudo waiver function `product_names` by default. +#' @param breaks,labels,minor_breaks One of: +#' - `NULL` for no breaks / labels. +#' - [ggplot2::waiver()] for the default breaks / labels computed by the scale. +#' - A numeric / character vector giving the positions of the breaks / labels. +#' - A function. +#' See [ggplot2::scale_x_continuous()] for more details. +#' @param na.value The value to be used for `NA` values. +#' @param position For position scales, The position of the axis. +#' left or right for y axes, top or bottom for x axes. #' @param ... other arguments passed to `continuous_scale()`. -#' @inheritParams ggplot2::continuous_scale #' @keywords internal -.scale_x_mosaic <- function(breaks = function(x) unique(x), +.scale_x_mosaic <- function(breaks = unique, minor_breaks = NULL, - labels = function(x) unique(x), + labels = unique, na.value = NA_real_, # nolint: object_name_linter. position = "bottom", ...) { @@ -175,44 +183,44 @@ is_discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x) # Example: compute rectangles from x and y result <- data |> # Count combinations of X and Y - dplyr::count(x, fill, .drop = FALSE) |> + dplyr::count(.data$x, .data$fill, .drop = FALSE) |> # Compute total for each X group dplyr::mutate( - .by = x, - x_total = sum(n), - prop = n / x_total, - prop = dplyr::if_else(is.nan(prop), 0, prop) + .by = .data$x, + x_total = sum(.data$n), + prop = .data$n / .data$x_total, + prop = dplyr::if_else(is.nan(.data$prop), 0, .data$prop) ) |> - dplyr::arrange(dplyr::desc(x_total), x, fill) |> + dplyr::arrange(dplyr::desc(.data$x_total), .data$x, .data$fill) |> # Compute total sample size to turn counts into widths dplyr::mutate( N_total = dplyr::n(), - x_width = x_total / N_total + x_width = .data$x_total / .data$N_total ) |> # Convert counts to x widths dplyr::mutate( - .by = x, - x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), x_width, 0) + .by = .data$x, + x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), .data$x_width, 0) ) |> # Compute x-min/x-max for each group dplyr::mutate( - xmin = cumsum(dplyr::lag(x_width_last, default = 0)), - xmax = xmin + x_width + xmin = cumsum(dplyr::lag(.data$x_width_last, default = 0)), + xmax = .data$xmin + .data$x_width ) |> # Compute y-min/y-max for stacked proportions dplyr::mutate( - .by = x, - ymin = c(0, head(cumsum(prop), -1)), - ymax = cumsum(prop) + .by = .data$x, + ymin = c(0, utils::head(cumsum(.data$prop), -1)), + ymax = cumsum(.data$prop) ) |> dplyr::mutate( - xmin = xmin / max(xmax), - xmax = xmax / max(xmax), - xmin = dplyr::if_else(n == 0, 0, xmin + 0.005), - xmax = dplyr::if_else(n == 0, 0, xmax - 0.005), - ymin = dplyr::if_else(n == 0, 0, ymin + 0.005), - ymax = dplyr::if_else(n == 0, 0, ymax - 0.005) + xmin = .data$xmin / max(.data$xmax), + xmax = .data$xmax / max(.data$xmax), + xmin = dplyr::if_else(.data$n == 0, 0, .data$xmin + 0.005), + xmax = dplyr::if_else(.data$n == 0, 0, .data$xmax - 0.005), + ymin = dplyr::if_else(.data$n == 0, 0, .data$ymin + 0.005), + ymax = dplyr::if_else(.data$n == 0, 0, .data$ymax - 0.005) ) |> - dplyr::select(x, fill, xmin, xmax, ymin, ymax, .n = n) + dplyr::select(.data$x, .data$fill, .data$xmin, .data$xmax, .data$ymin, .data$ymax, .n = .data$n) result } diff --git a/R/teal.modules.general.R b/R/teal.modules.general.R index 07e7cf03c..d733f4fdc 100644 --- a/R/teal.modules.general.R +++ b/R/teal.modules.general.R @@ -7,8 +7,7 @@ #' @import shiny #' @import teal #' @import teal.transform -#' @importFrom dplyr %>% -#' +#' @importFrom dplyr %>% .data #' #' @name teal.modules.general #' @keywords internal diff --git a/R/tm_g_association.R b/R/tm_g_association.R index 3fbc19d2c..d4bda6fd9 100644 --- a/R/tm_g_association.R +++ b/R/tm_g_association.R @@ -511,7 +511,7 @@ srv_tm_g_association <- function(id, env = list( plot_calls = do.call( "call", - c(list("list", ref_call), var_calls), + c(list("list", ref_call), unname(var_calls)), quote = TRUE ) ) diff --git a/man/dot-scale_x_mosaic.Rd b/man/dot-scale_x_mosaic.Rd index 6e1c17c5c..610143b36 100644 --- a/man/dot-scale_x_mosaic.Rd +++ b/man/dot-scale_x_mosaic.Rd @@ -5,61 +5,30 @@ \title{Determining scales for mosaics} \usage{ .scale_x_mosaic( - breaks = function(x) unique(x), + breaks = unique, minor_breaks = NULL, - labels = function(x) unique(x), + labels = unique, na.value = NA_real_, position = "bottom", ... ) } \arguments{ -\item{breaks}{One of: +\item{breaks, labels, minor_breaks}{One of: \itemize{ -\item \code{NULL} for no breaks -\item \code{waiver()} for the default breaks computed by the -\link[scales:new_transform]{transformation object} -\item A numeric vector of positions -\item A function that takes the limits as input and returns breaks -as output (e.g., a function returned by \code{\link[scales:breaks_extended]{scales::extended_breaks()}}). -Note that for position scales, limits are provided after scale expansion. -Also accepts rlang \link[rlang:as_function]{lambda} function notation. +\item \code{NULL} for no breaks / labels. +\item \code{\link[ggplot2:waiver]{ggplot2::waiver()}} for the default breaks / labels computed by the scale. +\item A numeric / character vector giving the positions of the breaks / labels. +\item A function. +See \code{\link[ggplot2:scale_continuous]{ggplot2::scale_x_continuous()}} for more details. }} -\item{minor_breaks}{One of: -\itemize{ -\item \code{NULL} for no minor breaks -\item \code{waiver()} for the default breaks (none for discrete, one minor break -between each major break for continuous) -\item A numeric vector of positions -\item A function that given the limits returns a vector of minor breaks. Also -accepts rlang \link[rlang:as_function]{lambda} function notation. When -the function has two arguments, it will be given the limits and major -break positions. -}} - -\item{labels}{One of the options below. Please note that when \code{labels} is a -vector, it is highly recommended to also set the \code{breaks} argument as a -vector to protect against unintended mismatches. -\itemize{ -\item \code{NULL} for no labels -\item \code{waiver()} for the default labels computed by the -transformation object -\item A character vector giving labels (must be same length as \code{breaks}) -\item An expression vector (must be the same length as breaks). See ?plotmath for details. -\item A function that takes the breaks as input and returns labels -as output. Also accepts rlang \link[rlang:as_function]{lambda} function -notation. -}} - -\item{na.value}{Missing values will be replaced with this value.} +\item{na.value}{The value to be used for \code{NA} values.} \item{position}{For position scales, The position of the axis. -\code{left} or \code{right} for y axes, \code{top} or \code{bottom} for x axes.} +left or right for y axes, top or bottom for x axes.} \item{...}{other arguments passed to \code{continuous_scale()}.} - -\item{name}{set to pseudo waiver function \code{product_names} by default.} } \description{ Determining scales for mosaics