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