From 929b53fa4b1937f26e6592b8db8fbc5a66a1a713 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 12:53:02 -0700 Subject: [PATCH 1/9] drop singletons --- R/type_density.R | 2 +- R/type_ridge.R | 3 ++- R/type_violin.R | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/R/type_density.R b/R/type_density.R index fdfbfe8a..30216b3e 100644 --- a/R/type_density.R +++ b/R/type_density.R @@ -182,7 +182,7 @@ 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 if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/R/type_ridge.R b/R/type_ridge.R index 6274fbfd..1601a97c 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -284,15 +284,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 = Filter(function(k) nrow(k) > 1, datapoints) # drop 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..3b3193cb 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -147,7 +147,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 = Filter(function(k) nrow(k) > 1, datapoints) # drop singletons if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw From b7b40554de3de4b5a841e8f11a5e2662e1b72101 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 13:07:42 -0700 Subject: [PATCH 2/9] facets are always factors --- R/sanitize_facet.R | 48 ++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 21 deletions(-) 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( From 226bba7deb55617f64a172ace948066b3dc97fe1 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 13:15:55 -0700 Subject: [PATCH 3/9] update test snapshot --- .../ridge_gradient_facet_theme_ridge2.svg | 4210 ++++++++--------- 1 file changed, 2105 insertions(+), 2105 deletions(-) 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 - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - + + - + From 65b50e321f56af1433e7ab24855dec9b9713864a Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 16:46:08 -0700 Subject: [PATCH 4/9] formal 'singleton' argument - fancier version with warnings etc. --- R/singletons.R | 111 ++++++++++++++++++++++++++++++++++++++++++++ R/type_density.R | 20 ++++++-- R/type_ridge.R | 22 +++++++-- R/type_violin.R | 23 +++++++-- man/type_density.Rd | 13 +++++- man/type_ridge.Rd | 13 +++++- man/type_violin.Rd | 13 +++++- 7 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 R/singletons.R diff --git a/R/singletons.R b/R/singletons.R new file mode 100644 index 00000000..014a68cf --- /dev/null +++ b/R/singletons.R @@ -0,0 +1,111 @@ +## 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. The +## discard is what these helpers implement, along with the optional warning that +## names the groups that went missing. + + +## The levels split() will group a vector on, without coercing it: factors keep +## their own (unused ones included), anything else gets the sorted unique values +## that factor() would derive anyway. Same pattern as facet.R's panel list. +group_levels = function(x) { + if (is.factor(x)) levels(x) else sort(unique(x)) +} + + +## The variable name to print in front of a split key's value. Each key prefers +## the label the reader actually sees on the plot -- the legend title for `by`, +## the axis label for the primary axis keys ("x" for violin, "y" for ridge) -- +## and falls back to the deparsed input recorded by tinyplot(). The axis label +## is the better source of the two here: under the formula method `x_dep` and +## `y_dep` are just the forwarded placeholders "x" and "y". `facet` never comes +## through here; it has its own titles, built by facet_titles(). +key_title = function(key, settings) { + if (key == "by") { + return(settings[["legend_args"]][["title"]] %||% settings[["by_dep"]]) + } + axlab = settings[[paste0(key, "lab")]] + axlab %||% settings[[paste0(key, "_dep")]] %||% key +} + + +## Human-readable labels for a set of split cells. +## +## `keys` is the named list of level vectors that `datapoints` was split on, in +## split order; `idx` indexes the resulting cells. Those cells are the cross +## product of the levels with the *first* key varying fastest, which is exactly +## what expand.grid() produces -- so the group behind each cell is recovered +## positionally, rather than by splitting the "."-pasted cell names back apart +## (which breaks on any level containing a "."). +singleton_labels = function(idx, keys, settings) { + grid = expand.grid(keys, stringsAsFactors = FALSE) + labs = NULL + for (key in names(keys)) { + ## A lone "" is the placeholder that datapoints carries for an absent + ## grouping variable: no `by`, no `facet`, or a `by` that type_ridge() + ## folded into the primary axis. Nothing to report for those. + if (identical(keys[[key]], "")) next + vals = grid[[key]][idx] + if (key == "facet") { + lab = facet_titles( + vals, + prefix = TRUE, + facet_vars = settings[["facet_vars"]], + facet_grid = isTRUE(attr(settings[["facet"]], "facet_grid")) + ) + ## grids join the two strip titles with "~"; reads better as a list + lab = gsub("~", ", ", lab, fixed = TRUE) + } else { + lab = paste0(key_title(key, settings), " = ", vals) + } + labs = if (is.null(labs)) lab else paste(labs, lab, sep = ", ") + } + labs +} + + +## Drop the split cells that are too small to estimate a density from. +## +## `datapoints` is the list returned by split(); `keys` is the named list of +## level vectors it was split on (only consulted when warning, so callers may +## pass NULL otherwise). `singletons` is one of "drop" (silent), "warn" (drop, +## but say which groups went), or "none" (keep them, and let the bandwidth rules +## complain if they cannot cope). +drop_singletons = function(datapoints, keys, singletons, settings) { + ## 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)) + singl = which(nobs == 1L) + if (singletons == "warn" && length(singl) > 0L) { + labs = singleton_labels(singl, keys, settings) + msg = paste0("Dropped ", length(singl), " singleton group(s)") + if (length(labs) > 0L) { + msg = paste0(msg, ":\n", paste0(" ", labs, collapse = "\n")) + } + warning(msg, "\n", call. = FALSE) + } + datapoints[nobs > 1L] +} + + +## Level vectors for the keys `datapoints` is about to be split on, in split +## order. Only needed to name the offending groups, so it returns NULL unless +## we are actually going to warn -- and it has to be called *before* the split, +## while `datapoints` is still the data frame. +## +## `levels` supplies a key's levels directly, for the case where the column no +## longer carries them: type_violin() recodes `x` to integer plotting positions +## before splitting, so its labels have to come from the levels it saved first. +singleton_keys = function(datapoints, keys, singletons, levels = list()) { + if (singletons != "warn") return(NULL) + out = lapply(keys, function(key) { + levels[[key]] %||% group_levels(datapoints[[key]]) + }) + names(out) = keys + out +} diff --git a/R/type_density.R b/R/type_density.R index 30216b3e..45546cfa 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 `"drop"` option silently removes any singleton cases, although +#' they may still be represented as empty facets in your plot. `"warn"` also +#' drops singletons and further emits a warning with the offending cases. +#' 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("drop", "warn", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) + singletons = match.arg(singletons, c("drop", "warn", "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 = "drop") { 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) @@ -181,8 +192,9 @@ data_density = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, if (is.null(ylab)) ylab = "Density" + skeys = singleton_keys(datapoints, c("by", "facet"), singletons) datapoints = split(datapoints, list(datapoints$by, datapoints$facet)) - datapoints = Filter(function(k) nrow(k) > 1, datapoints) # drop singletons + datapoints = drop_singletons(datapoints, skeys, singletons, settings) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/R/type_ridge.R b/R/type_ridge.R index 1601a97c..bc372c2d 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -75,6 +75,15 @@ #' transparency of the density fills. In most cases, will default to a value of #' 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 `"drop"` option silently removes any singleton cases, +#' although they may still be represented as empty ridge lines or facets in +#' your plot. `"warn"` also drops singletons and further emits a warning with +#' the offending cases. 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: #' @@ -215,10 +224,12 @@ type_ridge = function( gradient = FALSE, raster = FALSE, col = NULL, - alpha = NULL + alpha = NULL, + singletons = c("drop", "warn", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) + singletons = match.arg(singletons, c("drop", "warn", "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 = "drop" ) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "yaxt", "xaxt", "null_by")) @@ -292,8 +305,9 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, } ## + skeys = singleton_keys(datapoints, c("y", "by", "facet"), singletons) datapoints = split(datapoints, list(datapoints$y, datapoints$by, datapoints$facet)) - datapoints = Filter(function(k) nrow(k) > 1, datapoints) # drop singletons + datapoints = drop_singletons(datapoints, skeys, singletons, settings) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/R/type_violin.R b/R/type_violin.R index 3b3193cb..e8e0fabe 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 `"drop"` option silently removes any singleton cases, +#' although they may still be represented as empty violins or facets in your +#' plot. `"warn"` also drops singletons and further emits a warning with the +#' offending cases. 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("drop", "warn", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) + singletons = match.arg(singletons, c("drop", "warn", "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 = "drop") { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "null_palette", "facet", "ylab", "col", "bg", "log", "null_by", "null_facet")) settings[["lighten"]] = lighten @@ -146,8 +157,12 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, datapoints = datapoints[xord,] + skeys = singleton_keys( + datapoints, c("x", "by", "facet"), singletons, + levels = list(x = xlvls) # x is integer positions by now + ) datapoints = split(datapoints, list(datapoints$x, datapoints$by, datapoints$facet)) - datapoints = Filter(function(k) nrow(k) > 1, datapoints) # drop singletons + datapoints = drop_singletons(datapoints, skeys, singletons, settings) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/man/type_density.Rd b/man/type_density.Rd index 2b2f4c81..5a86c1ff 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("drop", "warn", "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{"drop"} option silently removes any singleton cases, although +they may still be represented as empty facets in your plot. \code{"warn"} also +drops singletons and further emits a warning with the offending cases. +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..80859c19 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("drop", "warn", "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{"drop"} option silently removes any singleton cases, +although they may still be represented as empty ridge lines or facets in +your plot. \code{"warn"} also drops singletons and further emits a warning with +the offending cases. 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..04fb1df2 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("drop", "warn", "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{"drop"} option silently removes any singleton cases, +although they may still be represented as empty violins or facets in your +plot. \code{"warn"} also drops singletons and further emits a warning with the +offending cases. 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 From 5f1561c8a3e191d97eecedb6ae3845e3dd223797 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 16:46:57 -0700 Subject: [PATCH 5/9] tests --- .../_tinysnapshot/density_singletons_drop.svg | 264 +++++++++++++++++ .../_tinysnapshot/density_singletons_none.svg | 271 ++++++++++++++++++ .../_tinysnapshot/ridge_singletons_drop.svg | 120 ++++++++ .../_tinysnapshot/violin_singletons_drop.svg | 112 ++++++++ inst/tinytest/test-type_density.R | 29 ++ inst/tinytest/test-type_ridge.R | 14 + inst/tinytest/test-type_violin.R | 14 + 7 files changed, 824 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/density_singletons_drop.svg create mode 100644 inst/tinytest/_tinysnapshot/density_singletons_none.svg create mode 100644 inst/tinytest/_tinysnapshot/ridge_singletons_drop.svg create mode 100644 inst/tinytest/_tinysnapshot/violin_singletons_drop.svg 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_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..8e2671b6 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 +f = function() { + plt(~mpg, facet = cyl ~ vs, data = mtcars, type = "density") +} +expect_snapshot_plot(f, label = "density_singletons_drop") + +# "warn" drops the same groups, but names them +expect_warning( + plt(~mpg, facet = cyl ~ vs, data = mtcars, type = type_density(singletons = "warn")), + pattern = "vs = 0, cyl = 4" +) + +# "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..72993ef6 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -170,3 +170,17 @@ 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 +f = function() { + plt(cyl ~ mpg, facet = ~vs, data = mtcars, type = "ridge") +} +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..bf2cc040 100644 --- a/inst/tinytest/test-type_violin.R +++ b/inst/tinytest/test-type_violin.R @@ -89,3 +89,17 @@ 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 +f = function() { + plt(mpg ~ cyl, facet = ~vs, data = mtcars, type = "violin") +} +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")) From 5e7674f0c98456f167b9e83a97bda7ca84dacbc0 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 16:47:29 -0700 Subject: [PATCH 6/9] news --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index a3852e96..d95925cf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -107,6 +107,12 @@ 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 + (silently) dropped by default. The affected types---`type_density()`, + `type_violin()`, and `type_ridge()`---also gain a `singletons` argument for + controlling this behaviour, e.g. option `"warn"` also drops singletons but + reports which groups were removed. (#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. From 5a8711e42a5947f8829d5a647d9e46f300b4c8c2 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 18:37:02 -0700 Subject: [PATCH 7/9] simplify --- NEWS.md | 2 +- R/singletons.R | 111 +++++------------------------- R/type_density.R | 9 ++- R/type_ridge.R | 11 ++- R/type_violin.R | 16 ++--- inst/tinytest/test-type_density.R | 4 +- man/type_density.Rd | 4 +- man/type_ridge.Rd | 6 +- man/type_violin.Rd | 10 +-- 9 files changed, 45 insertions(+), 128 deletions(-) diff --git a/NEWS.md b/NEWS.md index d95925cf..f5bdd383 100644 --- a/NEWS.md +++ b/NEWS.md @@ -112,7 +112,7 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. (silently) dropped by default. The affected types---`type_density()`, `type_violin()`, and `type_ridge()`---also gain a `singletons` argument for controlling this behaviour, e.g. option `"warn"` also drops singletons but - reports which groups were removed. (#300 @grantmcdermott) + reports how many groups were removed. (#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/singletons.R b/R/singletons.R index 014a68cf..b92d23c5 100644 --- a/R/singletons.R +++ b/R/singletons.R @@ -2,110 +2,33 @@ ## ## 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. The -## discard is what these helpers implement, along with the optional warning that -## names the groups that went missing. - - -## The levels split() will group a vector on, without coercing it: factors keep -## their own (unused ones included), anything else gets the sorted unique values -## that factor() would derive anyway. Same pattern as facet.R's panel list. -group_levels = function(x) { - if (is.factor(x)) levels(x) else sort(unique(x)) -} - - -## The variable name to print in front of a split key's value. Each key prefers -## the label the reader actually sees on the plot -- the legend title for `by`, -## the axis label for the primary axis keys ("x" for violin, "y" for ridge) -- -## and falls back to the deparsed input recorded by tinyplot(). The axis label -## is the better source of the two here: under the formula method `x_dep` and -## `y_dep` are just the forwarded placeholders "x" and "y". `facet` never comes -## through here; it has its own titles, built by facet_titles(). -key_title = function(key, settings) { - if (key == "by") { - return(settings[["legend_args"]][["title"]] %||% settings[["by_dep"]]) - } - axlab = settings[[paste0(key, "lab")]] - axlab %||% settings[[paste0(key, "_dep")]] %||% key -} - - -## Human-readable labels for a set of split cells. -## -## `keys` is the named list of level vectors that `datapoints` was split on, in -## split order; `idx` indexes the resulting cells. Those cells are the cross -## product of the levels with the *first* key varying fastest, which is exactly -## what expand.grid() produces -- so the group behind each cell is recovered -## positionally, rather than by splitting the "."-pasted cell names back apart -## (which breaks on any level containing a "."). -singleton_labels = function(idx, keys, settings) { - grid = expand.grid(keys, stringsAsFactors = FALSE) - labs = NULL - for (key in names(keys)) { - ## A lone "" is the placeholder that datapoints carries for an absent - ## grouping variable: no `by`, no `facet`, or a `by` that type_ridge() - ## folded into the primary axis. Nothing to report for those. - if (identical(keys[[key]], "")) next - vals = grid[[key]][idx] - if (key == "facet") { - lab = facet_titles( - vals, - prefix = TRUE, - facet_vars = settings[["facet_vars"]], - facet_grid = isTRUE(attr(settings[["facet"]], "facet_grid")) - ) - ## grids join the two strip titles with "~"; reads better as a list - lab = gsub("~", ", ", lab, fixed = TRUE) - } else { - lab = paste0(key_title(key, settings), " = ", vals) - } - labs = if (is.null(labs)) lab else paste(labs, lab, sep = ", ") - } - labs -} +## 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(); `keys` is the named list of -## level vectors it was split on (only consulted when warning, so callers may -## pass NULL otherwise). `singletons` is one of "drop" (silent), "warn" (drop, -## but say which groups went), or "none" (keep them, and let the bandwidth rules -## complain if they cannot cope). -drop_singletons = function(datapoints, keys, singletons, settings) { +## `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)) - singl = which(nobs == 1L) - if (singletons == "warn" && length(singl) > 0L) { - labs = singleton_labels(singl, keys, settings) - msg = paste0("Dropped ", length(singl), " singleton group(s)") - if (length(labs) > 0L) { - msg = paste0(msg, ":\n", paste0(" ", labs, collapse = "\n")) - } - warning(msg, "\n", call. = FALSE) + 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] } - - -## Level vectors for the keys `datapoints` is about to be split on, in split -## order. Only needed to name the offending groups, so it returns NULL unless -## we are actually going to warn -- and it has to be called *before* the split, -## while `datapoints` is still the data frame. -## -## `levels` supplies a key's levels directly, for the case where the column no -## longer carries them: type_violin() recodes `x` to integer plotting positions -## before splitting, so its labels have to come from the levels it saved first. -singleton_keys = function(datapoints, keys, singletons, levels = list()) { - if (singletons != "warn") return(NULL) - out = lapply(keys, function(key) { - levels[[key]] %||% group_levels(datapoints[[key]]) - }) - names(out) = keys - out -} diff --git a/R/type_density.R b/R/type_density.R index 45546cfa..200ebf2f 100644 --- a/R/type_density.R +++ b/R/type_density.R @@ -34,10 +34,10 @@ #' groups, i.e. combinations of `by` and `facet` that consist of only 1 row. #' The default `"drop"` option silently removes any singleton cases, although #' they may still be represented as empty facets in your plot. `"warn"` also -#' drops singletons and further emits a warning with the offending cases. -#' Finally, `"none"` skips all singleton checks and retains the affected +#' drops singletons and further emits a warning reporting how many there +#' were. 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 +#' numeric `bw`, since the data-driven bandwidth rules need at least 2 #' observations. #' #' @section Bandwidth selection: While the choice of smoothing bandwidth will @@ -192,9 +192,8 @@ data_density = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, if (is.null(ylab)) ylab = "Density" - skeys = singleton_keys(datapoints, c("by", "facet"), singletons) datapoints = split(datapoints, list(datapoints$by, datapoints$facet)) - datapoints = drop_singletons(datapoints, skeys, singletons, settings) + 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 bc372c2d..af682f01 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -75,16 +75,16 @@ #' transparency of the density fills. In most cases, will default to a value of #' 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 `"drop"` option silently removes any singleton cases, #' although they may still be represented as empty ridge lines or facets in -#' your plot. `"warn"` also drops singletons and further emits a warning with -#' the offending cases. Finally, `"none"` skips all singleton checks and -#' retains the affected groups; possibly leading to an error. Note that +#' your plot. `"warn"` also drops singletons and further emits a warning +#' reporting how many there were. 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 @@ -305,9 +305,8 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, } ## - skeys = singleton_keys(datapoints, c("y", "by", "facet"), singletons) datapoints = split(datapoints, list(datapoints$y, datapoints$by, datapoints$facet)) - datapoints = drop_singletons(datapoints, skeys, singletons, settings) + 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 e8e0fabe..3e8a0962 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -17,11 +17,11 @@ #' groups, i.e. combinations of `x`, `by`, and `facet` that consist of only 1 #' row. The default `"drop"` option silently removes any singleton cases, #' although they may still be represented as empty violins or facets in your -#' plot. `"warn"` also drops singletons and further emits a warning with the -#' offending cases. 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. +#' plot. `"warn"` also drops singletons and further emits a warning reporting +#' how many there were. 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. @@ -157,12 +157,8 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, datapoints = datapoints[xord,] - skeys = singleton_keys( - datapoints, c("x", "by", "facet"), singletons, - levels = list(x = xlvls) # x is integer positions by now - ) datapoints = split(datapoints, list(datapoints$x, datapoints$by, datapoints$facet)) - datapoints = drop_singletons(datapoints, skeys, singletons, settings) + datapoints = drop_singletons(datapoints, singletons) if (joint.bw == "none" || is.numeric(bw)) { dens_bw = bw diff --git a/inst/tinytest/test-type_density.R b/inst/tinytest/test-type_density.R index 8e2671b6..614b92e3 100644 --- a/inst/tinytest/test-type_density.R +++ b/inst/tinytest/test-type_density.R @@ -124,10 +124,10 @@ f = function() { } expect_snapshot_plot(f, label = "density_singletons_drop") -# "warn" drops the same groups, but names them +# "warn" drops the same groups, but reports how many went expect_warning( plt(~mpg, facet = cyl ~ vs, data = mtcars, type = type_density(singletons = "warn")), - pattern = "vs = 0, cyl = 4" + pattern = "Dropped 1 singleton" ) # "none" keeps them, which the data-driven bandwidth rules cannot cope with diff --git a/man/type_density.Rd b/man/type_density.Rd index 5a86c1ff..f6252848 100644 --- a/man/type_density.Rd +++ b/man/type_density.Rd @@ -66,8 +66,8 @@ settings.)} groups, i.e. combinations of \code{by} and \code{facet} that consist of only 1 row. The default \code{"drop"} option silently removes any singleton cases, although they may still be represented as empty facets in your plot. \code{"warn"} also -drops singletons and further emits a warning with the offending cases. -Finally, \code{"none"} skips all singleton checks and retains the affected +drops singletons and further emits a warning reporting how many there +were. 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.} diff --git a/man/type_ridge.Rd b/man/type_ridge.Rd index 80859c19..c80b9818 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -117,9 +117,9 @@ cases where \code{by==y} or \code{by==x}), will default to 0.6.} groups, i.e. combinations of \code{y}, \code{by}, and \code{facet} that consist of only 1 row. The default \code{"drop"} option silently removes any singleton cases, although they may still be represented as empty ridge lines or facets in -your plot. \code{"warn"} also drops singletons and further emits a warning with -the offending cases. Finally, \code{"none"} skips all singleton checks and -retains the affected groups; possibly leading to an error. Note that +your plot. \code{"warn"} also drops singletons and further emits a warning +reporting how many there were. 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.} } diff --git a/man/type_violin.Rd b/man/type_violin.Rd index 04fb1df2..ab384a32 100644 --- a/man/type_violin.Rd +++ b/man/type_violin.Rd @@ -63,11 +63,11 @@ displays consistent and lets the fill read cleanly over grid lines. Set to groups, i.e. combinations of \code{x}, \code{by}, and \code{facet} that consist of only 1 row. The default \code{"drop"} option silently removes any singleton cases, although they may still be represented as empty violins or facets in your -plot. \code{"warn"} also drops singletons and further emits a warning with the -offending cases. 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.} +plot. \code{"warn"} also drops singletons and further emits a warning reporting +how many there were. 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 From 4826487188afedca19b629851d43d08f0de914d5 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 18:48:22 -0700 Subject: [PATCH 8/9] default to warn Singleton groups are dropped either way, but staying silent about it risks the missing panel (or violin, or ridge line) reading as "no data here" rather than "only one observation here". A survey of the standard datasets suggests this fires rarely -- only on small, unbalanced data like mtcars -- so the warning should be high-signal rather than noise. It is also emitted once per call, not on replay, so window resizes do not repeat it. Pass singletons = "drop" for the old quiet behaviour. Claude-Session: https://claude.ai/code/session_014YKk45eRcv5MkVuu9LkDeW --- NEWS.md | 9 +++++---- R/type_density.R | 20 ++++++++++---------- R/type_ridge.R | 20 ++++++++++---------- R/type_violin.R | 20 ++++++++++---------- inst/tinytest/test-type_density.R | 16 ++++++++-------- inst/tinytest/test-type_ridge.R | 10 ++++++++-- inst/tinytest/test-type_violin.R | 10 ++++++++-- man/type_density.Rd | 16 ++++++++-------- man/type_ridge.Rd | 16 ++++++++-------- man/type_violin.Rd | 16 ++++++++-------- 10 files changed, 83 insertions(+), 70 deletions(-) diff --git a/NEWS.md b/NEWS.md index f5bdd383..11f6de6f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -109,10 +109,11 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. - Density-based plots no longer error out on singleton groups, i.e. `by` and `facet` combinations containing only one observation. Such groups are now - (silently) dropped by default. The affected types---`type_density()`, - `type_violin()`, and `type_ridge()`---also gain a `singletons` argument for - controlling this behaviour, e.g. option `"warn"` also drops singletons but - reports how many groups were removed. (#300 @grantmcdermott) + 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/type_density.R b/R/type_density.R index 200ebf2f..16fa59a2 100644 --- a/R/type_density.R +++ b/R/type_density.R @@ -32,13 +32,13 @@ #' 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 `"drop"` option silently removes any singleton cases, although -#' they may still be represented as empty facets in your plot. `"warn"` also -#' drops singletons and further emits a warning reporting how many there -#' were. 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. +#' 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 @@ -126,10 +126,10 @@ type_density = function( # more args from density here? echo.bw = FALSE, alpha = NULL, - singletons = c("drop", "warn", "none") + singletons = c("warn", "drop", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) - singletons = match.arg(singletons, c("drop", "warn", "none")) + singletons = match.arg(singletons, c("warn", "drop", "none")) if (is.logical(joint.bw)) { joint.bw = ifelse(joint.bw, "mean", "none") } @@ -183,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, singletons = "drop") { + 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) diff --git a/R/type_ridge.R b/R/type_ridge.R index af682f01..915d12ae 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -78,13 +78,13 @@ #' #' @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 `"drop"` option silently removes any singleton cases, -#' although they may still be represented as empty ridge lines or facets in -#' your plot. `"warn"` also drops singletons and further emits a warning -#' reporting how many there were. 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. +#' 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 @@ -225,11 +225,11 @@ type_ridge = function( raster = FALSE, col = NULL, alpha = NULL, - singletons = c("drop", "warn", "none") + singletons = c("warn", "drop", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) - singletons = match.arg(singletons, c("drop", "warn", "none")) + singletons = match.arg(singletons, c("warn", "drop", "none")) if (is.logical(joint.bw)) { joint.bw = ifelse(joint.bw, "mean", "none") } @@ -269,7 +269,7 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, raster = FALSE, col = NULL, alpha = NULL, - singletons = "drop" + singletons = "warn" ) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "yaxt", "xaxt", "null_by")) diff --git a/R/type_violin.R b/R/type_violin.R index 3e8a0962..1fd9f227 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -15,13 +15,13 @@ #' `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 `"drop"` option silently removes any singleton cases, -#' although they may still be represented as empty violins or facets in your -#' plot. `"warn"` also drops singletons and further emits a warning reporting -#' how many there were. 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. +#' 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. @@ -74,10 +74,10 @@ type_violin = function( trim = FALSE, width = 0.9, lighten = TRUE, - singletons = c("drop", "warn", "none") + singletons = c("warn", "drop", "none") ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) - singletons = match.arg(singletons, c("drop", "warn", "none")) + singletons = match.arg(singletons, c("warn", "drop", "none")) if (is.logical(joint.bw)) { joint.bw = ifelse(joint.bw, "mean", "none") } @@ -97,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, singletons = "drop") { + 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 diff --git a/inst/tinytest/test-type_density.R b/inst/tinytest/test-type_density.R index 614b92e3..de465abf 100644 --- a/inst/tinytest/test-type_density.R +++ b/inst/tinytest/test-type_density.R @@ -118,18 +118,18 @@ 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 -f = function() { - plt(~mpg, facet = cyl ~ vs, data = mtcars, type = "density") -} -expect_snapshot_plot(f, label = "density_singletons_drop") - -# "warn" drops the same groups, but reports how many went +# 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 = type_density(singletons = "warn")), + 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")), diff --git a/inst/tinytest/test-type_ridge.R b/inst/tinytest/test-type_ridge.R index 72993ef6..bce0c812 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -174,9 +174,15 @@ 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 +# 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 = "ridge") + plt(cyl ~ mpg, facet = ~vs, data = mtcars, type = type_ridge(singletons = "drop")) } expect_snapshot_plot(f, label = "ridge_singletons_drop") expect_error( diff --git a/inst/tinytest/test-type_violin.R b/inst/tinytest/test-type_violin.R index bf2cc040..de32b106 100644 --- a/inst/tinytest/test-type_violin.R +++ b/inst/tinytest/test-type_violin.R @@ -93,9 +93,15 @@ 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 +# 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 = "violin") + plt(mpg ~ cyl, facet = ~vs, data = mtcars, type = type_violin(singletons = "drop")) } expect_snapshot_plot(f, label = "violin_singletons_drop") expect_error( diff --git a/man/type_density.Rd b/man/type_density.Rd index f6252848..60422c2d 100644 --- a/man/type_density.Rd +++ b/man/type_density.Rd @@ -13,7 +13,7 @@ type_density( n = 512, echo.bw = FALSE, alpha = NULL, - singletons = c("drop", "warn", "none") + singletons = c("warn", "drop", "none") ) } \arguments{ @@ -64,13 +64,13 @@ 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{"drop"} option silently removes any singleton cases, although -they may still be represented as empty facets in your plot. \code{"warn"} also -drops singletons and further emits a warning reporting how many there -were. 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.} +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 c80b9818..a6207864 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -20,7 +20,7 @@ type_ridge( raster = FALSE, col = NULL, alpha = NULL, - singletons = c("drop", "warn", "none") + singletons = c("warn", "drop", "none") ) } \arguments{ @@ -115,13 +115,13 @@ 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{"drop"} option silently removes any singleton cases, -although they may still be represented as empty ridge lines or facets in -your plot. \code{"warn"} also drops singletons and further emits a warning -reporting how many there were. 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.} +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 ab384a32..c42fca4d 100644 --- a/man/type_violin.Rd +++ b/man/type_violin.Rd @@ -14,7 +14,7 @@ type_violin( trim = FALSE, width = 0.9, lighten = TRUE, - singletons = c("drop", "warn", "none") + singletons = c("warn", "drop", "none") ) } \arguments{ @@ -61,13 +61,13 @@ displays consistent and lets the fill read cleanly over grid lines. Set to \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{"drop"} option silently removes any singleton cases, -although they may still be represented as empty violins or facets in your -plot. \code{"warn"} also drops singletons and further emits a warning reporting -how many there were. 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.} +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 From a65e17b62e52feab971996b3936111435b9795eb Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 21 Aug 2026 19:39:49 -0700 Subject: [PATCH 9/9] posterity --- R/type_density.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/type_density.R b/R/type_density.R index 16fa59a2..e2cbf24c 100644 --- a/R/type_density.R +++ b/R/type_density.R @@ -193,6 +193,7 @@ 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) > 1, datapoints) # drop singletons (rather use dedicated function below) datapoints = drop_singletons(datapoints, singletons) if (joint.bw == "none" || is.numeric(bw)) {