diff --git a/NEWS.md b/NEWS.md index a3852e96..11f6de6f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -107,6 +107,13 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. ### Bug fixes +- Density-based plots no longer error out on singleton groups, i.e. `by` and + `facet` combinations containing only one observation. Such groups are now + dropped, together with a warning reporting how many were removed. The + affected types---`type_density()`, `type_violin()`, and `type_ridge()`---also + gain a `singletons` argument for controlling this behaviour: option `"drop"` + removes them quietly, while `"none"` retains them (and so requires a numeric + `bw`). (#300 @grantmcdermott) - `flip = TRUE` now flips the drawn geometry of the single-letter line types, not just the axes: `type = "h"` draws horizontal segments to the baseline, and the step types `"s"` and `"S"` swap which coordinate moves first. diff --git a/R/sanitize_facet.R b/R/sanitize_facet.R index d540efbc..4a51cf71 100644 --- a/R/sanitize_facet.R +++ b/R/sanitize_facet.R @@ -15,28 +15,35 @@ sanitize_facet = function(settings) { # prefixes, and the levels let facet_titles() restore a value's type after # splitting a composite title apart. See facet_var_list(). facet_vars = NULL - if (!is.null(facet) && length(facet) == 1 && facet == "by") { - by = as.factor(by) ## if by==facet, then both need to be factors - facet = by - facet_by = TRUE - # facet titles inherit the "by" variable name (same as the legend title) - facet_vars = list(x = facet_var_list(by, legend_args[["title"]] %||% by_dep)) - } else if (!is.null(facet) && inherits(facet, "formula")) { - facet = get_facet_fml(facet, data = data) - if (isTRUE(attr(facet, "facet_grid"))) { - facet.args[["nrow"]] = attr(facet, "facet_nrow") - } - facet_vars = attr(facet, "facet_vars") - } else if (!is.null(facet)) { - # recorded by tinyplot.formula(), else fall back to the deparsed input of - # the default method, e.g. facet = dat$fvar. (When called via - # tinyplot.formula(), facet_dep is just the forwarded "facet" placeholder, - # but that method has already recorded the real name.) - facet_vars = attr(facet, "facet_vars") - if (is.null(facet_vars) && !is.null(facet_dep) && !facet_dep %in% c("facet", "NULL")) { - facet_vars = list(x = facet_var_list(facet, facet_dep)) + + null_facet = TRUE + if (!is.null(facet)) { + null_facet = FALSE + if (length(facet) == 1 && facet == "by") { + by = as.factor(by) ## if by==facet, then both need to be factors + facet = by + facet_by = TRUE + # facet titles inherit the "by" variable name (same as the legend title) + facet_vars = list(x = facet_var_list(by, legend_args[["title"]] %||% by_dep)) + } else if (inherits(facet, "formula")) { + facet = get_facet_fml(facet, data = data) + if (isTRUE(attr(facet, "facet_grid"))) { + facet.args[["nrow"]] = attr(facet, "facet_nrow") + } + facet_vars = attr(facet, "facet_vars") + } else { + # recorded by tinyplot.formula(), else fall back to the deparsed input of + # the default method, e.g. facet = dat$fvar. (When called via + # tinyplot.formula(), facet_dep is just the forwarded "facet" placeholder, + # but that method has already recorded the real name.) + facet_vars = attr(facet, "facet_vars") + if (is.null(facet_vars) && !is.null(facet_dep) && !facet_dep %in% c("facet", "NULL")) { + facet_vars = list(x = facet_var_list(facet, facet_dep)) + } } + facet = as.factor(facet) # facets are *always* factors } + # The variables travel as an attribute so that they survive the handover from # tinyplot.formula(), but they get stripped here: `facet` flows on into # `datapoints`, where a stray attribute would break identity checks against @@ -44,7 +51,6 @@ sanitize_facet = function(settings) { if (!is.null(facet)) attr(facet, "facet_vars") = NULL facet_attr = attributes(facet) # TODO: better way to restore facet attributes? - null_facet = is.null(facet) # update settings env2env( diff --git a/R/singletons.R b/R/singletons.R new file mode 100644 index 00000000..b92d23c5 --- /dev/null +++ b/R/singletons.R @@ -0,0 +1,34 @@ +## Shared singleton handling for the density-family types. +## +## type_density(), type_violin() and type_ridge() all estimate a density per +## group, which means splitting `datapoints` on some combination of the primary +## axis, `by` and `facet`, then discarding any cell too small to smooth. + + +## Drop the split cells that are too small to estimate a density from. +## +## `datapoints` is the list returned by split(). `singletons` is one of "drop" +## (silent), "warn" (drop, but say how many went), or "none" (keep them, and let +## the bandwidth rules complain if they cannot cope). +## +## Note that this counts rows rather than filtering on them directly: split() +## emits a cell for every level *combination*, so the 0-row cells it invents +## have to be told apart from the 1-row cells the user actually supplied. Only +## the latter are worth warning about; a plain Filter(nrow > 1) conflates the +## two and reports groups that never existed. +drop_singletons = function(datapoints, singletons) { + ## empty cells are an artefact of the split, never a user group, so they go + ## in every case + if (singletons == "none") { + return(Filter(function(k) nrow(k) > 0, datapoints)) + } + nobs = vapply(datapoints, nrow, integer(1)) + if (singletons == "warn" && any(nobs == 1L)) { + warning( + "Dropped ", sum(nobs == 1L), " singleton group(s). Densities ", + "require at least 2 observations.\n", + call. = FALSE + ) + } + datapoints[nobs > 1L] +} diff --git a/R/type_density.R b/R/type_density.R index fdfbfe8a..e2cbf24c 100644 --- a/R/type_density.R +++ b/R/type_density.R @@ -30,6 +30,15 @@ #' left alone. Bandwidths shared across subgroups are reported once and named #' as joint; individual bandwidths are reported per group, truncated after #' three so the label stays legible. +#' @param singletons character string indicating what to do with singleton +#' groups, i.e. combinations of `by` and `facet` that consist of only 1 row. +#' The default `"warn"` option removes any singleton cases and emits a +#' warning reporting how many there were. `"drop"` does the same thing, but +#' quietly. In either case the dropped groups may still be represented as +#' empty facets in your plot. Finally, `"none"` skips all singleton checks and +#' retains the affected groups; possibly leading to an error. Note that +#' singletons require a numeric `bw`, since the data-driven bandwidth rules +#' need at least 2 observations. #' #' @section Bandwidth selection: While the choice of smoothing bandwidth will #' always stand to affect a density visualization, it gains an added @@ -116,9 +125,11 @@ type_density = function( n = 512, # more args from density here? echo.bw = FALSE, - alpha = NULL + alpha = NULL, + singletons = c("warn", "drop", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) + singletons = match.arg(singletons, c("warn", "drop", "none")) if (is.logical(joint.bw)) { joint.bw = ifelse(joint.bw, "mean", "none") } @@ -127,7 +138,7 @@ type_density = function( out = list( data = data_density(bw = bw, adjust = adjust, kernel = kernel, n = n, joint.bw = joint.bw, echo.bw = echo.bw, - alpha = alpha), + alpha = alpha, singletons = singletons), draw = NULL, name = "density" ) @@ -172,7 +183,7 @@ format_echo_vec = function(x, numeric = TRUE) { data_density = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, joint.bw = "none", echo.bw = character(0), - alpha = NULL) { + alpha = NULL, singletons = "warn") { fun = function(settings, ...) { env2env(settings, environment(), c("by", "bg", "facet", "ylab", "col", "ribbon.alpha", "datapoints")) ribbon.alpha = if (is.null(alpha)) .tpar[["ribbon.alpha"]] else (alpha) @@ -182,7 +193,8 @@ data_density = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, if (is.null(ylab)) ylab = "Density" datapoints = split(datapoints, list(datapoints$by, datapoints$facet)) - datapoints = Filter(function(k) nrow(k) > 0, datapoints) + # datapoints = Filter(function(k) nrow(k) > 1, datapoints) # drop singletons (rather use dedicated function below) + datapoints = drop_singletons(datapoints, singletons) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/R/type_ridge.R b/R/type_ridge.R index 6274fbfd..915d12ae 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -76,6 +76,15 @@ #' 1, i.e. fully opaque. But for some `by` grouped plots (excepting the special #' cases where `by==y` or `by==x`), will default to 0.6. #' +#' @param singletons character string indicating what to do with singleton +#' groups, i.e. combinations of `y`, `by`, and `facet` that consist of only 1 +#' row. The default `"warn"` option removes any singleton cases and emits a +#' warning reporting how many there were. `"drop"` does the same thing, but +#' quietly. In either case the dropped groups may still be represented as empty +#' ridge lines or facets in your plot. Finally, `"none"` skips all singleton +#' checks and retains the affected groups; possibly leading to an error. Note +#' that singletons require a numeric `bw`, since the data-driven bandwidth +#' rules need at least 2 observations. #' @section Technical note on gradient fills: #' #' `tinyplot` uses two basic approaches for drawing gradient fills in ridge line @@ -215,10 +224,12 @@ type_ridge = function( gradient = FALSE, raster = FALSE, col = NULL, - alpha = NULL + alpha = NULL, + singletons = c("warn", "drop", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) + singletons = match.arg(singletons, c("warn", "drop", "none")) if (is.logical(joint.bw)) { joint.bw = ifelse(joint.bw, "mean", "none") } @@ -236,7 +247,8 @@ type_ridge = function( ylevels = ylevels, raster = raster, col = col, - alpha = alpha + alpha = alpha, + singletons = singletons ), name = "ridge" ) @@ -256,7 +268,8 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, ylevels = NULL, raster = FALSE, col = NULL, - alpha = NULL + alpha = NULL, + singletons = "warn" ) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "yaxt", "xaxt", "null_by")) @@ -284,15 +297,16 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, if (isTRUE(x_by)) fill_by = FALSE # if (isTRUE(anyby) && is.null(alpha)) alpha = 0.6 + if (!is.factor(datapoints$y)) datapoints$y = factor(datapoints$y) ## reorder levels of y-variable if requested if (!is.null(ylevels)) { - if (!is.factor(datapoints$y)) datapoints$y = factor(datapoints$y) datapoints$y = sanitize_xlevels(datapoints$y, ylevels, arg = "ylevels") if (y_by) datapoints$by = datapoints$y } ## datapoints = split(datapoints, list(datapoints$y, datapoints$by, datapoints$facet)) + datapoints = drop_singletons(datapoints, singletons) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/R/type_violin.R b/R/type_violin.R index 04cf2593..1fd9f227 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -13,6 +13,15 @@ #' series colour(s)? Default is `TRUE`, which keeps single- and multi-group #' displays consistent and lets the fill read cleanly over grid lines. Set to #' `FALSE` to use the fully-saturated palette colour(s) instead. +#' @param singletons character string indicating what to do with singleton +#' groups, i.e. combinations of `x`, `by`, and `facet` that consist of only 1 +#' row. The default `"warn"` option removes any singleton cases and emits a +#' warning reporting how many there were. `"drop"` does the same thing, but +#' quietly. In either case the dropped groups may still be represented as +#' empty violins or facets in your plot. Finally, `"none"` skips all singleton +#' checks and retains the affected groups; possibly leading to an error. Note +#' that singletons require a numeric `bw`, since the data-driven bandwidth +#' rules need at least 2 observations. #' @inherit stats::density details #' @details See [`type_density`] for more details and considerations related to #' bandwidth selection and kernel types. @@ -64,9 +73,11 @@ type_violin = function( # more args from density here? trim = FALSE, width = 0.9, - lighten = TRUE + lighten = TRUE, + singletons = c("warn", "drop", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) + singletons = match.arg(singletons, c("warn", "drop", "none")) if (is.logical(joint.bw)) { joint.bw = ifelse(joint.bw, "mean", "none") } @@ -74,7 +85,7 @@ type_violin = function( out = list( data = data_violin(bw = bw, adjust = adjust, kernel = kernel, n = n, joint.bw = joint.bw, trim = trim, width = width, - lighten = lighten), + lighten = lighten, singletons = singletons), # draw = NULL, # name = "polygon" draw = draw_polygon(density = NULL), @@ -86,7 +97,7 @@ type_violin = function( data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, joint.bw = "none", trim = FALSE, width = 0.9, - lighten = TRUE) { + lighten = TRUE, singletons = "warn") { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "null_palette", "facet", "ylab", "col", "bg", "log", "null_by", "null_facet")) settings[["lighten"]] = lighten @@ -147,7 +158,7 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, datapoints = split(datapoints, list(datapoints$x, datapoints$by, datapoints$facet)) - datapoints = Filter(function(k) nrow(k) > 0, datapoints) + datapoints = drop_singletons(datapoints, singletons) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/inst/tinytest/_tinysnapshot/density_singletons_drop.svg b/inst/tinytest/_tinysnapshot/density_singletons_drop.svg new file mode 100644 index 00000000..2ca49bd8 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/density_singletons_drop.svg @@ -0,0 +1,264 @@ + + + + + + + + + + + + + +mpg +Density + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + + +0.00 +0.10 +0.20 + +0 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + + +0.00 +0.10 +0.20 + +1 + +4 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + + +0.00 +0.10 +0.20 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + + +0.00 +0.10 +0.20 + +6 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + + +0.00 +0.10 +0.20 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + + +0.00 +0.10 +0.20 + +8 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/density_singletons_none.svg b/inst/tinytest/_tinysnapshot/density_singletons_none.svg new file mode 100644 index 00000000..9aa32149 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/density_singletons_none.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + +mpg +Density + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + +0.0 +0.1 +0.2 +0.3 +0.4 + +0 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + +0.0 +0.1 +0.2 +0.3 +0.4 + +1 + +4 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + +0.0 +0.1 +0.2 +0.3 +0.4 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + +0.0 +0.1 +0.2 +0.3 +0.4 + +6 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + +0.0 +0.1 +0.2 +0.3 +0.4 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + + + + + + +0.0 +0.1 +0.2 +0.3 +0.4 + +8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/ridge_gradient_facet_theme_ridge2.svg b/inst/tinytest/_tinysnapshot/ridge_gradient_facet_theme_ridge2.svg index 326348fc..b4f7c776 100644 --- a/inst/tinytest/_tinysnapshot/ridge_gradient_facet_theme_ridge2.svg +++ b/inst/tinytest/_tinysnapshot/ridge_gradient_facet_theme_ridge2.svg @@ -30,2133 +30,2133 @@ am - - + + - + -5 -10 -15 -20 -25 -30 -35 -40 - -0 +5 +10 +15 +20 +25 +30 +35 +40 + +0 - - + + - + -5 -10 -15 -20 -25 -30 -35 -40 - -1 +5 +10 +15 +20 +25 +30 +35 +40 + +1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 1 - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - + + - + diff --git a/inst/tinytest/_tinysnapshot/ridge_singletons_drop.svg b/inst/tinytest/_tinysnapshot/ridge_singletons_drop.svg new file mode 100644 index 00000000..8ea6e1dd --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ridge_singletons_drop.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + +mpg +cyl + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + +0 + + + + + + + + + + + + + + + + + +10 +15 +20 +25 +30 +35 + +1 + + + + + + + + + + + + + +4 +6 +8 + + + + + + + + + + + + +4 +6 +8 + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_singletons_drop.svg b/inst/tinytest/_tinysnapshot/violin_singletons_drop.svg new file mode 100644 index 00000000..ae2b8dde --- /dev/null +++ b/inst/tinytest/_tinysnapshot/violin_singletons_drop.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + +cyl +mpg + + + + + + + + + + + + + +4 +6 +8 + + + + + + + +10 +15 +20 +25 +30 +35 + +0 + + + + + + + + + + + + + + +4 +6 +8 + + + + + + + +10 +15 +20 +25 +30 +35 + +1 + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_density.R b/inst/tinytest/test-type_density.R index e5a087a1..de465abf 100644 --- a/inst/tinytest/test-type_density.R +++ b/inst/tinytest/test-type_density.R @@ -114,3 +114,32 @@ f = function() { ) } expect_snapshot_plot(f, label = "density_echo_bw_cap_clean") + +# +## singleton groups (#300) + +# cyl == 4 & vs == 0 is a single car, so no density can be estimated for it. +# The default reports the loss; "drop" does the same thing quietly. +expect_warning( + plt(~mpg, facet = cyl ~ vs, data = mtcars, type = "density"), + pattern = "Dropped 1 singleton" +) + +f = function() { + plt(~mpg, facet = cyl ~ vs, data = mtcars, type = type_density(singletons = "drop")) +} +expect_snapshot_plot(f, label = "density_singletons_drop") + +# "none" keeps them, which the data-driven bandwidth rules cannot cope with +expect_error( + plt(~mpg, facet = cyl ~ vs, data = mtcars, type = type_density(singletons = "none")), + pattern = "at least 2 data points" +) +# ... but a numeric bandwidth can +f = function() { + plt(~mpg, facet = cyl ~ vs, data = mtcars, + type = type_density(singletons = "none", bw = 1)) +} +expect_snapshot_plot(f, label = "density_singletons_none") + +expect_error(type_density(singletons = "nope")) diff --git a/inst/tinytest/test-type_ridge.R b/inst/tinytest/test-type_ridge.R index 36c24e1b..bce0c812 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -170,3 +170,23 @@ f = function() { } expect_snapshot_plot(f, label = "ridge_ylab_na_issue650") + +# +## singleton groups (#300) + +# cyl == 4 & vs == 0 is a single car, so no density can be estimated for it. +# The default reports the loss; "drop" does the same thing quietly. +expect_warning( + plt(cyl ~ mpg, facet = ~vs, data = mtcars, type = "ridge"), + pattern = "Dropped 1 singleton" +) + +f = function() { + plt(cyl ~ mpg, facet = ~vs, data = mtcars, type = type_ridge(singletons = "drop")) +} +expect_snapshot_plot(f, label = "ridge_singletons_drop") +expect_error( + plt(cyl ~ mpg, facet = ~vs, data = mtcars, type = type_ridge(singletons = "none")), + pattern = "at least 2 data points" +) +expect_error(type_ridge(singletons = "nope")) diff --git a/inst/tinytest/test-type_violin.R b/inst/tinytest/test-type_violin.R index 3bfb6676..de32b106 100644 --- a/inst/tinytest/test-type_violin.R +++ b/inst/tinytest/test-type_violin.R @@ -89,3 +89,23 @@ f = function() { type = type_violin(lighten = FALSE), theme = "clean2") } expect_snapshot_plot(f, label = "violin_groups_lighten_false") + +# +## singleton groups (#300) + +# cyl == 4 & vs == 0 is a single car, so no density can be estimated for it. +# The default reports the loss; "drop" does the same thing quietly. +expect_warning( + plt(mpg ~ cyl, facet = ~vs, data = mtcars, type = "violin"), + pattern = "Dropped 1 singleton" +) + +f = function() { + plt(mpg ~ cyl, facet = ~vs, data = mtcars, type = type_violin(singletons = "drop")) +} +expect_snapshot_plot(f, label = "violin_singletons_drop") +expect_error( + plt(mpg ~ cyl, facet = ~vs, data = mtcars, type = type_violin(singletons = "none")), + pattern = "at least 2 data points" +) +expect_error(type_violin(singletons = "nope")) diff --git a/man/type_density.Rd b/man/type_density.Rd index 2b2f4c81..60422c2d 100644 --- a/man/type_density.Rd +++ b/man/type_density.Rd @@ -12,7 +12,8 @@ type_density( "cosine", "optcosine"), n = 512, echo.bw = FALSE, - alpha = NULL + alpha = NULL, + singletons = c("warn", "drop", "none") ) } \arguments{ @@ -60,6 +61,16 @@ three so the label stays legible.} If no \code{alpha} value is provided, then will default to \code{tpar("ribbon.alpha")} (i.e., probably \code{0.2} unless this has been overridden by the user in their global settings.)} + +\item{singletons}{character string indicating what to do with singleton +groups, i.e. combinations of \code{by} and \code{facet} that consist of only 1 row. +The default \code{"warn"} option removes any singleton cases and emits a +warning reporting how many there were. \code{"drop"} does the same thing, but +quietly. In either case the dropped groups may still be represented as +empty facets in your plot. Finally, \code{"none"} skips all singleton checks and +retains the affected groups; possibly leading to an error. Note that +singletons require a numeric \code{bw}, since the data-driven bandwidth rules +need at least 2 observations.} } \description{ Type function for density plots. diff --git a/man/type_ridge.Rd b/man/type_ridge.Rd index bd059b38..a6207864 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -19,7 +19,8 @@ type_ridge( gradient = FALSE, raster = FALSE, col = NULL, - alpha = NULL + alpha = NULL, + singletons = c("warn", "drop", "none") ) } \arguments{ @@ -111,6 +112,16 @@ qualitative palette, or black if no theme is set.} transparency of the density fills. In most cases, will default to a value of 1, i.e. fully opaque. But for some \code{by} grouped plots (excepting the special cases where \code{by==y} or \code{by==x}), will default to 0.6.} + +\item{singletons}{character string indicating what to do with singleton +groups, i.e. combinations of \code{y}, \code{by}, and \code{facet} that consist of only 1 +row. The default \code{"warn"} option removes any singleton cases and emits a +warning reporting how many there were. \code{"drop"} does the same thing, but +quietly. In either case the dropped groups may still be represented as empty +ridge lines or facets in your plot. Finally, \code{"none"} skips all singleton +checks and retains the affected groups; possibly leading to an error. Note +that singletons require a numeric \code{bw}, since the data-driven bandwidth +rules need at least 2 observations.} } \description{ Type function for producing ridge plots (also known as joy plots), diff --git a/man/type_violin.Rd b/man/type_violin.Rd index b63c578b..c42fca4d 100644 --- a/man/type_violin.Rd +++ b/man/type_violin.Rd @@ -13,7 +13,8 @@ type_violin( n = 512, trim = FALSE, width = 0.9, - lighten = TRUE + lighten = TRUE, + singletons = c("warn", "drop", "none") ) } \arguments{ @@ -57,6 +58,16 @@ enforced) giving the normalized width of the individual violins.} series colour(s)? Default is \code{TRUE}, which keeps single- and multi-group displays consistent and lets the fill read cleanly over grid lines. Set to \code{FALSE} to use the fully-saturated palette colour(s) instead.} + +\item{singletons}{character string indicating what to do with singleton +groups, i.e. combinations of \code{x}, \code{by}, and \code{facet} that consist of only 1 +row. The default \code{"warn"} option removes any singleton cases and emits a +warning reporting how many there were. \code{"drop"} does the same thing, but +quietly. In either case the dropped groups may still be represented as +empty violins or facets in your plot. Finally, \code{"none"} skips all singleton +checks and retains the affected groups; possibly leading to an error. Note +that singletons require a numeric \code{bw}, since the data-driven bandwidth +rules need at least 2 observations.} } \description{ Type function for violin plots, which are an alternative to box