From 49a0f377137ad875fb998f6c2faee0697968ab19 Mon Sep 17 00:00:00 2001 From: Matthijs Berends Date: Mon, 9 Nov 2026 22:10:01 +0100 Subject: [PATCH] (v3.1.0.9021) Show the number of tested isolates on susceptible-only bars, and split ggplot_sir() by antimicrobial if it is not shown in the plot --- DESCRIPTION | 2 +- NEWS.md | 5 +++-- R/ggplot_sir.R | 35 ++++++++++++++++++++++++++------ man/ggplot_sir.Rd | 2 +- man/plot.Rd | 2 +- tests/testthat/test-ggplot_sir.R | 17 ++++++++++++++++ 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 1ec15635c..a81294019 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: AMR -Version: 3.1.0.9020 +Version: 3.1.0.9021 Date: 2026-11-09 Title: Antimicrobial Resistance Data Analysis Description: Functions to simplify and standardise antimicrobial resistance (AMR) diff --git a/NEWS.md b/NEWS.md index d8458b504..5680e3972 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# AMR 3.1.0.9020 +# AMR 3.1.0.9021 This version fixes a critical bug in WISCA that biased all coverage estimates of `wisca()` and `antibiogram(..., wisca = TRUE)` downwards since AMR 3.0.0. Users are strongly advised to re-run their WISCA analyses. @@ -21,7 +21,8 @@ This version fixes a critical bug in WISCA that biased all coverage estimates of * Taxonomic names are now looked up once per session, making the function considerably faster for long vectors * `ggplot_sir()`, `geom_sir()` and `labels_sir_count()` * Fixed the data labels being placed on the wrong bars with `position = "dodge"` or `position = "stack"`. They are now grouped like the bars, using the new `fill` argument of `labels_sir_count()`, which `ggplot_sir()` sets automatically - * If the interpretation is not shown in the plot, e.g., with `fill = "species"`, only the susceptible proportion is now plotted, following the guideline: S+SDD+I for EUCAST, S+SDD for CLSI, and S with `guideline = NULL`. Previously, the bars of all interpretations were drawn on top of each other. The y axis title of `ggplot_sir()` then states the category, e.g., "Proportion SI" + * If the interpretation is not shown in the plot, e.g., with `fill = "species"`, only the susceptible proportion is now plotted, following the guideline: S+SDD+I for EUCAST, S+SDD for CLSI, and S with `guideline = NULL`. Previously, the bars of all interpretations were drawn on top of each other. The y axis title of `ggplot_sir()` then states the category, e.g., "Proportion SI", and the data labels show the number of tested isolates + * `ggplot_sir()` now splits the plot by antimicrobial if there is more than one and the antimicrobial is not shown in the plot, e.g., with `x = "species"`, as their bars were previously stacked on top of each other. This uses `facet = "antibiotic"` if `facet` is not set, and otherwise a grid of `facet` by antimicrobial * `ggplot_pca()` * Fixed an error when setting `groups` together with `labels`, and setting `groups` without `labels` now colours the points by these groups instead of using them as labels, as documented * WISCA (`wisca()` and `antibiogram(..., wisca = TRUE)`) diff --git a/R/ggplot_sir.R b/R/ggplot_sir.R index 3bfa9598a..b05ba539e 100755 --- a/R/ggplot_sir.R +++ b/R/ggplot_sir.R @@ -36,7 +36,7 @@ #' @param fill Variable to categorise using the plots legend, either `"interpretation"` (default) or `"antibiotic"` or a grouping variable. For [labels_sir_count()], this must be the same as the `fill` of [geom_sir()], so that the labels are placed on the right bars. If the interpretation is not shown in the plot (i.e., neither in `x`, `fill`, nor `facet`), only the susceptible proportion is plotted, which follows the guideline (see `guideline`): S+SDD+I for EUCAST, S+SDD for CLSI, and S if `guideline = NULL`. #' @param breaks A [numeric] vector of positions. #' @param limits A [numeric] vector of length two providing limits of the scale, use `NA` to refer to the existing minimum or maximum. -#' @param facet Variable to split plots by, either `"interpretation"` (default) or `"antibiotic"` or a grouping variable. +#' @param facet Variable to split plots by, either `"interpretation"` or `"antibiotic"` or a grouping variable. If there is more than one antimicrobial and the antimicrobial is not shown in the plot (i.e., neither in `x`, `fill`, nor `facet`), [ggplot_sir()] splits the plot by antimicrobial, as their bars would otherwise be stacked: using `facet = "antibiotic"` if `facet` is not set, and otherwise using a grid of `facet` by antimicrobial. Note that [geom_sir()] cannot do this, so add [facet_sir()] in that case. #' @inheritParams proportion #' @param nrow (when using `facet`) number of rows. #' @param colours A named vactor with colour to be used for filling. The default colours are colour-blind friendly. @@ -240,6 +240,21 @@ ggplot_sir <- function(data, position <- "fill" } + plot_df <- tryCatch( + sir_df(data = data, translate_ab = translate_ab, language = language, minimum = minimum, guideline = guideline), + error = function(e) NULL + ) + shown <- c(normalise_sir_plot_var(x), normalise_sir_plot_var(fill), normalise_sir_plot_var(facet)) + facet_grid_ab <- FALSE + if (!is.null(plot_df) && length(unique(plot_df$antibiotic)) > 1 && !"antibiotic" %in% shown) { + # the antibiotic is not shown, so the bars of all antibiotics would be stacked; split the plot by antibiotic + if (is.null(facet)) { + facet <- "antibiotic" + } else { + facet_grid_ab <- TRUE + } + } + p <- ggplot2::ggplot(data = data) + create_geom_sir( position = position, x = x, fill = fill, facet = facet, translate_ab = translate_ab, @@ -280,17 +295,21 @@ ggplot_sir <- function(data, p <- p + ggplot2::scale_fill_discrete(labels = labels_taxonomy) } - if (!is.null(facet)) { + if (isTRUE(facet_grid_ab)) { + # a grid of the set facet by antibiotic + p <- p + ggplot2::facet_grid( + stats::as.formula(paste0("`", normalise_sir_plot_var(facet), "` ~ antibiotic")), + scales = "free_x", + labeller = labeller_taxonomy + ) + } else if (!is.null(facet)) { # pass the value, as facet_sir() takes an unquoted name as column name p <- p + do.call(facet_sir, list(facet = facet, nrow = nrow)) } if (missing(y.title) && !"interpretation" %in% c(normalise_sir_plot_var(x), normalise_sir_plot_var(fill), normalise_sir_plot_var(facet))) { # only the susceptible proportion is shown, e.g. "Proportion SI" for EUCAST - susceptible <- tryCatch( - levels(sir_df(data = data, translate_ab = translate_ab, language = language, minimum = minimum, guideline = guideline)$interpretation)[1], - error = function(e) NULL - ) + susceptible <- if (is.null(plot_df)) NULL else levels(plot_df$interpretation)[1] if (!is.null(susceptible)) { y.title <- paste(y.title, susceptible) } @@ -389,6 +408,10 @@ sir_plot_data <- function(data, x, fill, facet, translate_ab, minimum, language, # the interpretation is not shown, so the bars of all interpretations would overlap; # keep only the susceptible proportion, which is the first level and follows the guideline: # S+SDD+I ("SI") for EUCAST, S+SDD ("S") for CLSI, and S for guideline = NULL + # the number of isolates of the data labels then becomes the number of tested isolates + keys <- do.call(paste, c(out[, setdiff(colnames(out), c("interpretation", "value", "ci_min", "ci_max", "isolates")), drop = FALSE], sep = "\r")) + totals <- tapply(out$isolates, keys, sum) + out$isolates <- as.integer(totals[keys]) out <- out[which(as.character(out$interpretation) == levels(out$interpretation)[1]), , drop = FALSE] } out diff --git a/man/ggplot_sir.Rd b/man/ggplot_sir.Rd index 1968afe46..c6bb5e150 100644 --- a/man/ggplot_sir.Rd +++ b/man/ggplot_sir.Rd @@ -67,7 +67,7 @@ labels_sir_count( \item{fill}{Variable to categorise using the plots legend, either \code{"interpretation"} (default) or \code{"antibiotic"} or a grouping variable. For \code{\link[=labels_sir_count]{labels_sir_count()}}, this must be the same as the \code{fill} of \code{\link[=geom_sir]{geom_sir()}}, so that the labels are placed on the right bars. If the interpretation is not shown in the plot (i.e., neither in \code{x}, \code{fill}, nor \code{facet}), only the susceptible proportion is plotted, which follows the guideline (see \code{guideline}): S+SDD+I for EUCAST, S+SDD for CLSI, and S if \code{guideline = NULL}.} -\item{facet}{Variable to split plots by, either \code{"interpretation"} (default) or \code{"antibiotic"} or a grouping variable.} +\item{facet}{Variable to split plots by, either \code{"interpretation"} or \code{"antibiotic"} or a grouping variable. If there is more than one antimicrobial and the antimicrobial is not shown in the plot (i.e., neither in \code{x}, \code{fill}, nor \code{facet}), \code{\link[=ggplot_sir]{ggplot_sir()}} splits the plot by antimicrobial, as their bars would otherwise be stacked: using \code{facet = "antibiotic"} if \code{facet} is not set, and otherwise using a grid of \code{facet} by antimicrobial. Note that \code{\link[=geom_sir]{geom_sir()}} cannot do this, so add \code{\link[=facet_sir]{facet_sir()}} in that case.} \item{breaks}{A \link{numeric} vector of positions.} diff --git a/man/plot.Rd b/man/plot.Rd index aff08c050..0378afbbb 100644 --- a/man/plot.Rd +++ b/man/plot.Rd @@ -185,7 +185,7 @@ labels_taxonomy(x, ...) \item{breakpoint_type}{The type of breakpoints to use, either \code{"ECOFF"}, \code{"animal"}, or \code{"human"}. ECOFF stands for Epidemiological Cut-Off values. The default is \code{"human"}, which can also be set with the package option \code{\link[=AMR-options]{AMR_breakpoint_type}}. If \code{host} is set to values of veterinary species, this will automatically be set to \code{"animal"}.} -\item{facet}{Variable to split plots by, either \code{"interpretation"} (default) or \code{"antibiotic"} or a grouping variable.} +\item{facet}{Variable to split plots by, either \code{"interpretation"} or \code{"antibiotic"} or a grouping variable. If there is more than one antimicrobial and the antimicrobial is not shown in the plot (i.e., neither in \code{x}, \code{fill}, nor \code{facet}), \code{\link[=ggplot_sir]{ggplot_sir()}} splits the plot by antimicrobial, as their bars would otherwise be stacked: using \code{facet = "antibiotic"} if \code{facet} is not set, and otherwise using a grid of \code{facet} by antimicrobial. Note that \code{\link[=geom_sir]{geom_sir()}} cannot do this, so add \code{\link[=facet_sir]{facet_sir()}} in that case.} \item{nrow}{(when using \code{facet}) number of rows.} diff --git a/tests/testthat/test-ggplot_sir.R b/tests/testthat/test-ggplot_sir.R index 5c6f2297a..8e682dd05 100644 --- a/tests/testthat/test-ggplot_sir.R +++ b/tests/testthat/test-ggplot_sir.R @@ -186,10 +186,27 @@ test_that("test-ggplot_sir.R", { built <- ggplot_build(p) expect_equal(sort(built$data[[1]]$y), sort(expected$value)) expect_equal(nrow(built$data[[2]]), nrow(expected)) + # data labels show the number of tested isolates + expected_n <- tapply(sir_df(grouped, guideline = gl)$isolates, sir_df(grouped, guideline = gl)$gender, sum) + expect_identical(sort(built$data[[2]]$label), sort(paste0("n=", expected_n))) expect_identical(p$labels$y, paste("Proportion", levels(expected$interpretation)[1])) } # all interpretations when they are shown in the facets built <- ggplot_build(ggplot_sir(grouped, x = "antibiotic", fill = "gender", facet = "interpretation", position = "dodge", guideline = "EUCAST")) expect_equal(nrow(built$data[[1]]), nrow(sir_df(grouped, guideline = "EUCAST"))) + + # split by antibiotic if it is not shown in the plot and there are several + grouped2 <- example_isolates[, c("gender", "AMX", "CIP")] + grouped2 <- structure(grouped2, class = c("grouped_df", "tbl_df", "tbl", "data.frame"), groups = data.frame(gender = c("F", "M"), .rows = I(list(which(grouped2$gender == "F"), which(grouped2$gender == "M"))))) + p <- ggplot_sir(grouped2, x = "gender", guideline = "EUCAST") + expect_true(inherits(p$facet, "FacetWrap")) + expect_identical(names(p$facet$params$facets), "antibiotic") + p <- ggplot_sir(grouped2, x = "interpretation", fill = "interpretation", facet = "gender", guideline = "EUCAST") + expect_true(inherits(p$facet, "FacetGrid")) + expect_identical(names(p$facet$params$cols), "antibiotic") + expect_identical(names(p$facet$params$rows), "gender") + # not if the antibiotic is shown, or if there is only one + expect_false(inherits(ggplot_sir(grouped2, x = "antibiotic", guideline = "EUCAST")$facet, "FacetWrap")) + expect_false(inherits(ggplot_sir(grouped, x = "gender", guideline = "EUCAST")$facet, "FacetWrap")) } })