From 723ae19b676a0712dbf5727d2b7c9565ee6fd2aa Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Mon, 15 Dec 2025 03:51:39 +0100 Subject: [PATCH 1/9] tinyframe() can now handle formulas without variables (e.g., ~ 1 or ~ 0) --- R/tinyformula.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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] } From e4b53d5db54a58fe520dbcecf84f8ccbbc30de9b Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Mon, 15 Dec 2025 03:52:36 +0100 Subject: [PATCH 2/9] support formulas without x-variable such as ~ 0 or ~ 1 or y ~ 1 etc. --- R/tinyplot.R | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index 1073a4c9..2cd5ec81 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1429,13 +1429,15 @@ 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]] + } ## extract y (if any) y = tinyframe(tf$y, mf) From 636e832781a9264cb72ab67788ea0a54fa941b5c Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Mon, 15 Dec 2025 04:17:39 +0100 Subject: [PATCH 3/9] handle names(x) only if non-NULL --- R/tinyplot.R | 1 - 1 file changed, 1 deletion(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index 2cd5ec81..050b2a61 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1431,7 +1431,6 @@ tinyplot.formula = function( ## extract x (if any) x = tinyframe(tf$x, mf) - xnam = names(x)[[1L]] 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), From 06775ae08976f571f85dc0bddb0ad101a66f06c8 Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 4 Feb 2026 03:20:45 +0100 Subject: [PATCH 4/9] handle defaults for cases where x or y is NULL - x = , y = NULL: barplot - x = NULL, y = : barplot - x = NULL, y = !: histogram - x = !, y = NULL: scatterplot against index (previous default) note that the cases with x = NULL necessitate switching settingsx and settings$y axis labels still need fixing --- R/sanitize_type.R | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 { From c8aa24db95caa68b3fb792f0614b3dc942175deb Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 4 Feb 2026 03:24:08 +0100 Subject: [PATCH 5/9] set xnam/ynam to NULL if x/y are missing --- R/tinyplot.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/R/tinyplot.R b/R/tinyplot.R index 050b2a61..22bc1135 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1436,6 +1436,8 @@ tinyplot.formula = function( 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]] + } else { + xnam = NULL } ## extract y (if any) @@ -1445,6 +1447,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) From f45de30c1c6059306fce230c9644b7a0c27f064c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 22 Mar 2026 21:31:26 -0700 Subject: [PATCH 6/9] better control flow for univariate names - also fix potential 'Count' vs 'Frequency' bug for barplot univariate cases --- R/sanitize_xylab.R | 2 +- R/tinyplot.R | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) 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/tinyplot.R b/R/tinyplot.R index 40d2e7e3..b34ff255 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1374,19 +1374,26 @@ 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)) { + # Both x and y NULL (e.g., ~ 0 with type = "segments"): let + # sanitize_xylab() determine labels from xmin/xmax/ymin/ymax deps. + } else if (is.null(x) && !is.null(y)) { + # 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 From 6a6175bb05ed79163f4f58148acecf4962eed9c5 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 22 Mar 2026 21:43:34 -0700 Subject: [PATCH 7/9] handle segements exception labeling --- R/tinyplot.R | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index b34ff255..71e125d1 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1375,12 +1375,19 @@ tinyplot.formula = function( 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 (is.null(x) && is.null(y)) { - # Both x and y NULL (e.g., ~ 0 with type = "segments"): let - # sanitize_xylab() determine labels from xmin/xmax/ymin/ymax deps. + # 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)) { - # 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. + # 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 From 0d2e2b49c4513bc14564c0b375fff093e02ee837 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 22 Mar 2026 21:53:03 -0700 Subject: [PATCH 8/9] tests and document --- .../barplot_formula_univariate.svg | 59 +++++ .../_tinysnapshot/barplot_formula_y1.svg | 59 +++++ .../_tinysnapshot/formula_univariate_num.svg | 217 ++++++++++++++++++ inst/tinytest/_tinysnapshot/formula_y1.svg | 94 ++++++++ .../_tinysnapshot/formula_y1_cust_lab.svg | 75 ++++++ .../_tinysnapshot/hist_formula_y1.svg | 75 ++++++ .../_tinysnapshot/segments_formula_null.svg | 69 ++++++ inst/tinytest/test-misc.R | 19 ++ inst/tinytest/test-type_barplot.R | 15 +- inst/tinytest/test-type_histogram.R | 9 +- inst/tinytest/test-type_segments.R | 8 + man/tinyplot-package.Rd | 2 +- 12 files changed, 698 insertions(+), 3 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_formula_y1.svg create mode 100644 inst/tinytest/_tinysnapshot/formula_univariate_num.svg create mode 100644 inst/tinytest/_tinysnapshot/formula_y1.svg create mode 100644 inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg create mode 100644 inst/tinytest/_tinysnapshot/hist_formula_y1.svg create mode 100644 inst/tinytest/_tinysnapshot/segments_formula_null.svg 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{ From 9e2e7294270b138f7a294cd48949787f75eba0ae Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 22 Mar 2026 21:55:53 -0700 Subject: [PATCH 9/9] news --- NEWS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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