diff --git a/NEWS.md b/NEWS.md index 49a7d229..39cde365 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,16 @@ where the formatting is also better._ ## Development version +### New features + +- Support for univariate formulas, e.g., `y ~ 1`, `~ x`, and `~ 0`. These are + translated to `x = NULL` or `y = NULL` in the default method call, with + automatic type inference: `y ~ 1` (numeric) produces a histogram, `y ~ 1` + (factor) produces a barplot, `~ x` (factor) produces a barplot, and `~ x` + (numeric) produces a scatterplot against the index. The `~ 0` form is useful + for types that don't require x/y, such as `segments` and `rect`. Thanks to + @brock for suggestion and discussion. (#534 @zeileis, @grantmcdermott) + ### Aesthetic changes - The legend plot characters for the `"pointrange"` and `"errorbar"` types now diff --git a/R/sanitize_type.R b/R/sanitize_type.R index 6a13516c..dc50fd57 100644 --- a/R/sanitize_type.R +++ b/R/sanitize_type.R @@ -43,10 +43,23 @@ sanitize_type = function(settings) { assert_choice(type, known_types, null.ok = TRUE) if (is.null(type)) { - if (!is.null(x) && (is.factor(x) || is.character(x)) && !(is.factor(y) || is.character(y))) { + if (is.null(x) && !(is.factor(y) || is.character(y))) { + # enforce histogram type for y ~ 1 + settings$x = y + settings$y = NULL + type = type_hist + } else if (is.null(x) && (is.factor(y) || is.character(y))) { + # enforce barplot type for factor(y) ~ 1 + settings$x = y + settings$y = NULL + type = type_barplot + } else if ((is.factor(x) || is.character(x)) && is.null(y)) { + # enforce barplot type for ~ factor(y) + type = type_barplot + } else if (!is.null(x) && (is.factor(x) || is.character(x)) && !(is.factor(y) || is.character(y))) { # enforce boxplot type for y ~ factor(x) type = type_boxplot - } else if (is.factor(y) || is.character(y)) { + } else if (!is.null(x) && (is.factor(y) || is.character(y))) { # enforce spineplot type for factor(y) ~ x type = type_spineplot } else { diff --git a/R/sanitize_xylab.R b/R/sanitize_xylab.R index 6ea3a3af..e367edd1 100644 --- a/R/sanitize_xylab.R +++ b/R/sanitize_xylab.R @@ -37,7 +37,7 @@ sanitize_xylab = function(settings) { if (!is.null(ylab)) { out_ylab = ylab } else if (is_frequency && is.null(y) && !is.null(x)) { - out_ylab = "Frequency" + out_ylab = if (type == "barplot") "Count" else "Frequency" } else if (is_density && is.null(y) && !is.null(x)) { out_ylab = "Density" } else if (is_ribbon) { diff --git a/R/tinyformula.R b/R/tinyformula.R index 56fd546e..29189061 100644 --- a/R/tinyformula.R +++ b/R/tinyformula.R @@ -74,6 +74,8 @@ tinyframe = function(formula, data, drop = FALSE) { ## - formula: (sub-)formula ## - data: model.frame from full formula if (is.null(formula)) return(NULL) - names = sapply(attr(terms(formula), "variables")[-1L], deparse, width.cutoff = 500L) + vars = attr(terms(formula), "variables")[-1L] + if (is.null(vars)) return(NULL) + names = sapply(vars, deparse, width.cutoff = 500L) data[, names, drop = drop] } diff --git a/R/tinyplot.R b/R/tinyplot.R index a1304b2c..71e125d1 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1326,13 +1326,16 @@ tinyplot.formula = function( m[[1L]] = quote(stats::model.frame) mf = eval.parent(m) - ## extract x + ## extract x (if any) x = tinyframe(tf$x, mf) - xnam = names(x)[[1L]] - if (length(names(x)) != 1L) warning( - paste("formula should specify exactly one x-variable, using:", xnam), + if (!is.null(x)) { + xnam = names(x)[[1L]] + if (length(names(x)) > 1L) warning(paste("formula should specify at most one x-variable, using:", xnam), "\nif you want to use arithmetic operators, make sure to wrap them inside I()") - x = x[[xnam]] + x = x[[xnam]] + } else { + xnam = NULL + } ## extract y (if any) y = tinyframe(tf$y, mf) @@ -1341,6 +1344,8 @@ tinyplot.formula = function( if (length(names(y)) > 1L) warning(paste("formula should specify at most one y-variable, using:", ynam), "\nif you want to use arithmetic operators, make sure to wrap them inside I()") y = y[[ynam]] + } else { + ynam = NULL } ## extract by (if any) @@ -1369,19 +1374,33 @@ tinyplot.formula = function( dens_type = !is.null(type) && (is.atomic(type) && identical(type, "density")) || (!is.atomic(type) && identical(type$name, "density")) hist_type = !is.null(type) && (is.atomic(type) && type %in% c("hist", "histogram")) || (!is.atomic(type) && identical(type$name, "histogram")) barp_type = !is.null(type) && (is.atomic(type) && identical(type, "barplot")) || (!is.atomic(type) && identical(type$name, "barplot")) - if (dens_type) { + if (is.null(x) && is.null(y)) { + # Exception: both x and y NULL (e.g., ~ 0 with type = "segments"). + # Build labels from xmin/xmax/ymin/ymax names in the original call (m), + # since deparse(substitute()) in the default method would see mf[["..."]]. + if (is.null(xlab) && !is.null(m[["xmin"]]) && !is.null(m[["xmax"]])) { + xlab = sprintf("[%s, %s]", deparse1(m[["xmin"]]), deparse1(m[["xmax"]])) + } + if (is.null(ylab) && !is.null(m[["ymin"]]) && !is.null(m[["ymax"]])) { + ylab = sprintf("[%s, %s]", deparse1(m[["ymin"]]), deparse1(m[["ymax"]])) + } + } else if (is.null(x) && !is.null(y)) { + # Exception: univariate y ~ 1 formulas. sanitize_type() will swap x/y and + # infer the type (histogram or barplot). Set xlab from the variable name + # and let sanitize_xylab() determine ylab after the type is known. + if (is.null(xlab)) xlab = ynam + } else if (dens_type) { # if (is.null(ylab)) ylab = "Density" ## rather assign ylab as part of internal type_density() logic if (is.null(xlab)) xlab = xnam } else if (hist_type) { # if (is.null(ylab)) ylab = "Frequency" ## rather assign ylab as part of internal type_histogram() logic if (is.null(xlab)) xlab = xnam } else if (is.null(y)) { - if (!barp_type) { + if (is.factor(x) || is.character(x) || barp_type) { + if (is.null(xlab)) xlab = xnam + } else { if (is.null(ylab)) ylab = xnam if (is.null(xlab)) xlab = "Index" - } else { - if (is.null(ylab)) ylab = "Count" - if (is.null(xlab)) xlab = xnam } } else { if (is.null(ylab)) ylab = ynam diff --git a/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg b/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg new file mode 100644 index 00000000..ecd38adc --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + +Species +Count +setosa +versicolor +virginica + + + + + + + +0 +10 +20 +30 +40 +50 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg b/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg new file mode 100644 index 00000000..ecd38adc --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + +Species +Count +setosa +versicolor +virginica + + + + + + + +0 +10 +20 +30 +40 +50 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/formula_univariate_num.svg b/inst/tinytest/_tinysnapshot/formula_univariate_num.svg new file mode 100644 index 00000000..f03555bc --- /dev/null +++ b/inst/tinytest/_tinysnapshot/formula_univariate_num.svg @@ -0,0 +1,217 @@ + + + + + + + + + + + + + +Index +Sepal.Length + + + + + +0 +50 +100 +150 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/formula_y1.svg b/inst/tinytest/_tinysnapshot/formula_y1.svg new file mode 100644 index 00000000..774a436c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/formula_y1.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica + + + + + + + +Sepal.Length +Frequency + + + + + + + + +4 +5 +6 +7 +8 + + + + + + +0 +5 +10 +15 +20 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg b/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg new file mode 100644 index 00000000..27aaa9bb --- /dev/null +++ b/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + +My X +My Y + + + + + + +4 +5 +6 +7 +8 + + + + + + + + +0 +5 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_formula_y1.svg b/inst/tinytest/_tinysnapshot/hist_formula_y1.svg new file mode 100644 index 00000000..599d843d --- /dev/null +++ b/inst/tinytest/_tinysnapshot/hist_formula_y1.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + +Sepal.Length +Frequency + + + + + + +4 +5 +6 +7 +8 + + + + + + + + +0 +5 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/segments_formula_null.svg b/inst/tinytest/_tinysnapshot/segments_formula_null.svg new file mode 100644 index 00000000..0f02e63e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/segments_formula_null.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + +[x0, x1] +[y0, y1] + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + + + + + + + + + + diff --git a/inst/tinytest/test-misc.R b/inst/tinytest/test-misc.R index 05e60ac5..4433c3d0 100644 --- a/inst/tinytest/test-misc.R +++ b/inst/tinytest/test-misc.R @@ -129,3 +129,22 @@ f = function() { tinyplot(1:10, pch = 19, cex = 2, main = "dots not cut off") } expect_snapshot_plot(f, label = "issue_545_xaxs_yaxs_restoration") + + +# univariate formula: ~ x (numeric) gives scatterplot against index +f = function() { + tinyplot(~ Sepal.Length, data = iris) +} +expect_snapshot_plot(f, label = "formula_univariate_num") + +# univariate formula: y ~ 1 with by grouping +f = function() { + tinyplot(Sepal.Length ~ 1 | Species, data = iris) +} +expect_snapshot_plot(f, label = "formula_y1") + +# univariate formula: user-supplied labels override defaults +f = function() { + tinyplot(Sepal.Length ~ 1, data = iris, xlab = "My X", ylab = "My Y") +} +expect_snapshot_plot(f, label = "formula_y1_cust_lab") diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index 4a973b7e..712fdf18 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -73,4 +73,17 @@ f = function() { x = rpois(n, 5) plt(~ x | grp, type = "barplot", beside = TRUE, ylab = "Custom y title") } -expect_snapshot_plot(f, label = "barplot_custom_ytitle") \ No newline at end of file +expect_snapshot_plot(f, label = "barplot_custom_ytitle") + + +# univariate formula: factor(y) ~ 1 infers barplot +f = function() { + tinyplot(Species ~ 1, data = iris) +} +expect_snapshot_plot(f, label = "barplot_formula_y1") + +# univariate formula: ~ factor(x) infers barplot +f = function() { + tinyplot(~ Species, data = iris) +} +expect_snapshot_plot(f, label = "barplot_formula_univariate") diff --git a/inst/tinytest/test-type_histogram.R b/inst/tinytest/test-type_histogram.R index cb0c0ce8..2c10d874 100644 --- a/inst/tinytest/test-type_histogram.R +++ b/inst/tinytest/test-type_histogram.R @@ -84,4 +84,11 @@ f = function() { data = iris ) } -expect_snapshot_plot(f, label = "hist_facet_free_breaks_free") \ No newline at end of file +expect_snapshot_plot(f, label = "hist_facet_free_breaks_free") + + +# univariate formula: y ~ 1 infers histogram +f = function() { + tinyplot(Sepal.Length ~ 1, data = iris) +} +expect_snapshot_plot(f, label = "hist_formula_y1") diff --git a/inst/tinytest/test-type_segments.R b/inst/tinytest/test-type_segments.R index d502d03c..d45ceb1f 100644 --- a/inst/tinytest/test-type_segments.R +++ b/inst/tinytest/test-type_segments.R @@ -52,3 +52,11 @@ f = function() { expect_snapshot_plot(f, label = "segments_by_yequal") rm(x, y, i, s, grp) + + +# univariate formula: ~ 0 with xmin/xmax/ymin/ymax +f = function() { + df = data.frame(x0 = c(0, .1), y0 = c(.2, 1), x1 = c(1, .9), y1 = c(.75, 0)) + tinyplot(~ 0, xmin = x0, ymin = y0, xmax = x1, ymax = y1, data = df, type = "segments") +} +expect_snapshot_plot(f, label = "segments_formula_null") diff --git a/man/tinyplot-package.Rd b/man/tinyplot-package.Rd index 757963d2..13b5d091 100644 --- a/man/tinyplot-package.Rd +++ b/man/tinyplot-package.Rd @@ -16,7 +16,7 @@ Useful links: } \author{ -\strong{Maintainer}: Grant McDermott \email{gmcd@amazon.com} (\href{https://orcid.org/0000-0001-7883-8573}{ORCID}) +\strong{Maintainer}: Grant McDermott \email{contact@grantmcdermott.com} (\href{https://orcid.org/0000-0001-7883-8573}{ORCID}) Authors: \itemize{