-
-
Notifications
You must be signed in to change notification settings - Fork 15
Adds back support where 2 categorical variables are used in association and bivariate plots #949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
bb77f9d
feat: support categorical variables in association and bivariate
averissimo f3c2020
chore: prefix internal function with dot
averissimo 6fe6664
pr: feedback from @llrs-roche
averissimo fc0a482
pr: make mosaic call simpler
averissimo 54914e6
[skip style] [skip vbump] Restyle files
github-actions[bot] e068c35
tests: re-adds tests that were removed
averissimo 9102fba
feat: adds datacall outside and supports multiple plots in association
averissimo 66a1720
Merge remote-tracking branch 'origin/main' into 948-minimal_mosaic
averissimo 432eb38
feat: using custom geom
averissimo 5614907
feat: update mosaic to working version
averissimo ff1519f
docs: update docs and remove import
averissimo 2fd5f4c
[skip style] [skip vbump] Restyle files
github-actions[bot] 9300a7a
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
github-actions[bot] 3dae9e1
docs: clean up
averissimo c06f9f9
chore: lint code
averissimo eacc78d
chore: cleanup and adds disclaimer on top
averissimo f005229
Merge branch 'main' into 948-minimal_mosaic
averissimo 296ab15
fix: update tests with bivariate geom plot call
averissimo f0917e5
Update R/custom_mosaic.R
averissimo a05f6dd
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
github-actions[bot] dcb1507
fix: examples
averissimo 07e3ff4
feat: use simpler expressions
averissimo 60ee4e2
chore: rename of file to better describe contents
averissimo 3eb774c
chore: update ggplot2 and dplyr versions
averissimo e88600b
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
github-actions[bot] 5ca3ffd
fix: .data pronoun and bug with breaks/labels
averissimo 5db4df3
Merge branch 'main' into 948-minimal_mosaic
llrs-roche File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # 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 | ||
| #' 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) + | ||
| #' geom_mosaic(aes(x = RACE, fill = SEX)) | ||
| #' @export | ||
| geom_mosaic <- function(mapping = NULL, data = NULL, | ||
| stat = "mosaic", position = "identity", | ||
| ..., | ||
| na.rm = FALSE, # nolint: object_name_linter. | ||
| show.legend = TRUE, # nolint: object_name_linter. | ||
| inherit.aes = TRUE) { # nolint: object_name_linter. | ||
|
|
||
| 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( | ||
| 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()) | ||
| } | ||
|
|
||
| #' @keywords internal | ||
| 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" | ||
| ), | ||
| 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") | ||
| ) | ||
|
|
||
| #' @keywords internal | ||
| StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter. | ||
| "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) | ||
|
|
||
| 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 | ||
| result$PANEL <- unique(data$PANEL) | ||
| result | ||
| } | ||
| ) | ||
|
|
||
| #' Determining scales for mosaics | ||
| #' | ||
| #' @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()`. | ||
| #' @keywords internal | ||
| .scale_x_mosaic <- function(breaks = unique, | ||
| minor_breaks = NULL, | ||
| labels = unique, | ||
| na.value = NA_real_, # nolint: object_name_linter. | ||
| 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 = ScaleContinuousMosaic, , | ||
| guide = ggplot2::waiver(), | ||
| ... | ||
| ) | ||
| } | ||
|
|
||
| #' @keywords internal | ||
| ScaleContinuousMosaic <- ggplot2::ggproto( # nolint: object_name_linter. | ||
| "ScaleContinuousMosaic", ggplot2::ScaleContinuousPosition, | ||
| train = function(self, x) { | ||
| if (length(x) == 0) { | ||
| return() | ||
| } | ||
| if (is.list(x)) { | ||
| 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 (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)) { | ||
| 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) | ||
| ) | ||
|
|
||
| #' @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. | ||
| #' | ||
| #' ### Value | ||
| #' | ||
| #' 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) { | ||
| # Example: compute rectangles from x and y | ||
| result <- data |> | ||
| # Count combinations of X and Y | ||
| dplyr::count(.data$x, .data$fill, .drop = FALSE) |> | ||
| # Compute total for each X group | ||
| dplyr::mutate( | ||
| .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(.data$x_total), .data$x, .data$fill) |> | ||
| # Compute total sample size to turn counts into widths | ||
| dplyr::mutate( | ||
| N_total = dplyr::n(), | ||
| x_width = .data$x_total / .data$N_total | ||
| ) |> | ||
| # Convert counts to x widths | ||
| dplyr::mutate( | ||
| .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(.data$x_width_last, default = 0)), | ||
| xmax = .data$xmin + .data$x_width | ||
| ) |> | ||
| # Compute y-min/y-max for stacked proportions | ||
| dplyr::mutate( | ||
| .by = .data$x, | ||
| ymin = c(0, utils::head(cumsum(.data$prop), -1)), | ||
| ymax = cumsum(.data$prop) | ||
| ) |> | ||
| dplyr::mutate( | ||
| 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(.data$x, .data$fill, .data$xmin, .data$xmax, .data$ymin, .data$ymax, .n = .data$n) | ||
| result | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.