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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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