diff --git a/NAMESPACE b/NAMESPACE
index 2f9d179e..2d2e56a6 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand
+S3method(tinyplot,data.frame)
S3method(tinyplot,default)
S3method(tinyplot,density)
S3method(tinyplot,formula)
@@ -129,6 +130,7 @@ importFrom(stats,qchisq)
importFrom(stats,qnorm)
importFrom(stats,qt)
importFrom(stats,quantile)
+importFrom(stats,reformulate)
importFrom(stats,setNames)
importFrom(stats,spline)
importFrom(stats,terms)
diff --git a/NEWS.md b/NEWS.md
index 68da48f2..454bd69a 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -160,6 +160,19 @@ Theme fixes:
### Other new features
+- A dedicated `tinyplot.data.frame()` method now supports direct plotting of
+ data frames, with or without a formula. Combining with a formula is mostly
+ useful insofar as it facilitates piping, e.g.
+
+ ```r
+ iris |> plt(Sepal.Length ~ Petal.Width | Species)
+ ```
+
+ If no formula is provided, then the behaviour depends on the number of
+ variables (columns) in the data frame. For example, a dataset with 3 or more
+ variables will yield a `pairs()`-style grid of all variable combinations.
+ Thanks to @mthulin for the suggestion and original implementation idea.
+ (#613 @zeileis @grantmcdermott)
- The `grid` argument (and `tpar("grid")`) now accepts character strings to
control axis-specific grids at different resolutions. Uppercase letters
(`"X"`, `"Y"`, `"XY"`) draw grid lines at the standard tick positions, while
@@ -172,10 +185,9 @@ Theme fixes:
customizable via `tpar()` parameters: `adj.cap`, `cex.cap`, `col.cap`,
`font.cap`, and `line.cap`. (#592 @grantmcdermott)
- Facet formulas now support `1` as a convenience syntax for single row or
- column arrangements. For example, `plt(..., facet = z ~ 1)` is equivalent to
- `plt(..., facet = ~z, facet.args = list(ncol = 1))`. Analogously,
- `plt(..., facet = 1 ~ z)` can be used as a shortcut for
- `plt(..., facet = ~ z, facet.args = list(nrow = 1))`. (#562 @zeileis)
+ column arrangements. (#562 @zeileis)
+ - `plt(..., facet = z ~ 1)` <-> `plt(..., facet = ~z, facet.args = list(ncol = 1))`
+ - `plt(..., facet = 1 ~ z)` <-> `plt(..., facet = ~z, facet.args = list(nrow = 1))`.
- `type_barplot()` gains an `offset` argument for shifting bar baselines away
from zero. (#611, #615 @grantmcdermott @zeileis)
- If the offset is an unnamed scalar or numeric vector, it shifts the bars
diff --git a/R/tinyplot.data.frame.R b/R/tinyplot.data.frame.R
new file mode 100644
index 00000000..791ad753
--- /dev/null
+++ b/R/tinyplot.data.frame.R
@@ -0,0 +1,166 @@
+#' tinyplot Method for Plotting Data Frames
+#'
+#' @description Convenience interface for visualizing
+#' \code{\link[base]{data.frames}} with tinyplot.
+#'
+#' @details This is a convenience function for plotting data frames with
+#' or without a formula. The case with the formula mainly facilitates
+#' using `tinyplot()` in combination with pipes. The case without
+#' formula provides a quick way of plotting the variables in a data frame:
+#' a single variable, a pair of variables, or a
+#' \code{\link[graphics]{pairs}}-style grid of all variable combinations for
+#' 3 or more variables.
+#'
+#' @param x an object of class `"data.frame"`.
+#' @param formula a \code{\link[stats]{formula}} that is passed on to
+#' \code{\link{tinyplot.formula}}. If `formula` is `NULL` a formula of
+#' type `y ~ 1` or `y ~ x` is set up for 1- and 2-variable data frames,
+#' respectively. For data frames with 3+ variables, a
+#' \code{\link[graphics]{pairs}}-style grid of all variable combinations is
+#' drawn instead.
+#' @param by (3+ case only) optional grouping variable. Either 1) a character
+#' string giving a column name in `x`, or 2) a vector of equal length to the
+#' main data.frame (_caveat emptor_: this is not checked). The variable is
+#' spliced into each sub-plot's formula as `y ~ x | by` so groups are
+#' distinguished (e.g. by colour). Note that the legend is deliberately
+#' suppressed.
+#' @param labs (3+ case only) logical indicating whether the axes labels
+#' (titles) for each sub-plot in the pairs-style case should be shown. Default
+#' is `FALSE`.
+#' @param frames (3+ case only) logical indicating whether each sub-plot should
+#' be framed by a box. Default is `FALSE`. Note the trailing "s" plural case
+#' to disambiguate from `frame.plot`.
+#' @param ... further arguments passed to `tinyplot`.
+#'
+#' @examples
+#' tinytheme("clean2")
+#'
+#' ## using tinyplot() with data frames
+#' tinyplot(cars)
+#' tinyplot(iris, Sepal.Length ~ Petal.Width | Species)
+#'
+#' ## note that this also enables usage with pipes (in R >= 4.1.0) such as
+#' # cars |> tinyplot()
+#' # iris |> tinyplot(Sepal.Length ~ Petal.Width | Species)
+#'
+#' ## pairs-style display for data frames with 3 or more variables
+#' tinyplot(iris)
+#'
+#' ## pass `by` arg to group the pairs display (legend is suppressed)
+#' ## here, we also add optional frames around the individual sub-plots
+#' tinyplot(iris, by = "Species", frames = TRUE)
+#'
+#' ## another option (but assumes objects of equal length)
+#' tinyplot(iris[, 1:4], by = iris$Species, frames = TRUE)
+#'
+#' tinytheme() ## reset theme
+#'
+#' @importFrom stats reformulate
+#' @export
+tinyplot.data.frame = function (x, formula = NULL, by = NULL, labs = FALSE, frames = FALSE, ...) {
+ ## original call
+ cl = match.call()
+
+ ## update call for formula interface
+ names(cl)[names(cl) == "x"] = "data"
+ cl[[1L]] = quote(tinyplot::tinyplot)
+ if (is.null(formula)) cl$formula = . ~ .
+ id = which(names(cl) == "formula")
+ cl = as.call(as.list(cl)[c(1L, id, setdiff(2L:length(cl), id))])
+
+ ## variables
+ nm = names(x)
+ n = length(nm)
+
+ ## backtick-protect names so reformulate() parses non-syntactic names (e.g.
+ ## "GDP (2020)") as single symbols rather than as code, matching pairs().
+ bt = function(x) paste0("`", x, "`")
+
+ if (is.null(formula) && n > 2L) {
+
+ ## 3d: pairs-esque
+ op = par(mfrow = c(n, n))
+ on.exit(par(op))
+
+ assert_logical(labs)
+ assert_logical(frames)
+ if (!is.null(by)) {
+ if (inherits(by, "character")) {
+ ## a column name already in `x`
+ assert_choice(by, nm)
+ } else {
+ ## a standalone vector: splice it into `x` under a unique name so the
+ ## formula method can reference it. make.unique() guards against an
+ ## existing "__by__" column. The name is added after `n` is computed,
+ ## so it is used for grouping only and not drawn as its own row/column.
+ by_nm = make.unique(c(nm, "__by__"))[n + 1L]
+ x[[by_nm]] = by
+ by = by_nm
+ cl[["data"]] = x
+ }
+ ## `by` is spliced directly into each cell formula below, so drop it from
+ ## the call. Suppress the legend to avoid repeating it across every cell.
+ if ("by" %in% names(cl)) cl[["by"]] = NULL
+ cl[["legend"]] = FALSE
+ }
+
+ ## To scale cex like faceted plots, we capture the existing theme and apply
+ ## temporary cex overrides (restoring on exit). The theme is built as a
+ ## language object because `cl` is a matched call, so `cl[["theme"]]` is
+ ## unevaluated (e.g. the call `list("dark")`, not a list).
+ cex_fct_adj = 0.66 # use same scaling as with faceted plots.
+ active_theme = get_tpar("tinytheme", default = "default")
+ theme_arg = cl[["theme"]]
+ if (is.null(theme_arg)) {
+ theme_ij = bquote(list(.(active_theme), cex = .(cex_fct_adj)))
+ } else if (is.call(theme_arg) && identical(theme_arg[[1L]], as.name("list"))) {
+ theme_arg[["cex"]] = cex_fct_adj
+ theme_ij = theme_arg
+ } else {
+ theme_ij = bquote(list(.(theme_arg), cex = .(cex_fct_adj)))
+ }
+ ## drop any `theme` from the cell call so it isn't re-applied per cell (see
+ ## above). NULL-assignment on a call errors if the element is absent, so
+ ## guard with a membership check.
+ if ("theme" %in% names(cl)) cl[["theme"]] = NULL
+
+ ## apply temp theme overrides and restore orig theme on exit
+ do.call(tinytheme, eval(theme_ij))
+ on.exit(if (identical(active_theme, "default")) tinytheme() else tinytheme(active_theme), add = TRUE)
+
+ for (j in 1L:n) {
+ for (i in 1L:n) {
+ cl_ij = cl
+ grp = if (is.null(by)) "" else paste(" |", bt(by))
+ if (i == j) {
+ cl_ij$formula = reformulate(paste0("1", grp), bt(nm[i]))
+ cl_ij$main = nm[i]
+ } else {
+ cl_ij$formula = reformulate(paste0(bt(nm[i]), grp), bt(nm[j]))
+ }
+ if (!labs) {
+ cl_ij$ylab = NA
+ cl_ij$xlab = NA
+ }
+ eval.parent(cl_ij)
+ if (frames) box("figure", lwd = 0.3)
+ }
+ }
+
+ } else {
+
+ ## `by` only applies to the >=3 (pairs-style) case; drop it so it isn't
+ ## passed through to tinyplot() here.
+ if ("by" %in% names(cl)) cl[["by"]] = NULL
+
+ ## default formula
+ ## 1d: y ~ 1
+ ## 2d: y ~ x
+ if (is.null(formula)) {
+ cl$formula = if (length(nm) < 2L) reformulate("1", bt(nm)) else reformulate(bt(nm[1L]), bt(nm[2L]))
+ }
+
+ ## evaluate updated call
+ eval.parent(cl)
+ }
+}
diff --git a/inst/tinytest/_tinysnapshot/df_2var.svg b/inst/tinytest/_tinysnapshot/df_2var.svg
new file mode 100644
index 00000000..8eecd7d4
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/df_2var.svg
@@ -0,0 +1,114 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/df_pairs.svg b/inst/tinytest/_tinysnapshot/df_pairs.svg
new file mode 100644
index 00000000..6e689cde
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/df_pairs.svg
@@ -0,0 +1,3069 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/df_pairs_by.svg b/inst/tinytest/_tinysnapshot/df_pairs_by.svg
new file mode 100644
index 00000000..f085e2be
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/df_pairs_by.svg
@@ -0,0 +1,3236 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/df_pairs_by_vector.svg b/inst/tinytest/_tinysnapshot/df_pairs_by_vector.svg
new file mode 100644
index 00000000..5496d9a1
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/df_pairs_by_vector.svg
@@ -0,0 +1,2553 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg b/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg
new file mode 100644
index 00000000..49b7a7d2
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg
@@ -0,0 +1,3486 @@
+
+
diff --git a/inst/tinytest/test-dataframe.R b/inst/tinytest/test-dataframe.R
new file mode 100644
index 00000000..8e6a32f2
--- /dev/null
+++ b/inst/tinytest/test-dataframe.R
@@ -0,0 +1,38 @@
+source("helpers.R")
+using("tinysnapshot")
+
+# 2-variable data frame -> y ~ x
+f = function() tinyplot(cars, theme = "clean2")
+expect_snapshot_plot(f, label = "df_2var")
+
+# 3+ variables -> pairs-style grid
+f = function() tinyplot(iris, theme = "clean2")
+expect_snapshot_plot(f, label = "df_pairs")
+
+# pairs grid with a `by` grouping variable (column name)
+f = function() tinyplot(iris, by = "Species", theme = "clean2")
+expect_snapshot_plot(f, label = "df_pairs_by")
+
+# `by` as a standalone vector (grouping var excluded from the grid)
+f = function() tinyplot(iris[, 1:4], by = iris$Species, theme = "clean2")
+expect_snapshot_plot(f, label = "df_pairs_by_vector")
+
+# axis labels and per-panel frames toggled on
+f = function() tinyplot(iris, by = "Species", labs = TRUE, frames = TRUE, theme = "clean2")
+expect_snapshot_plot(f, label = "df_pairs_labs_frames")
+
+
+# Non-snapshot logical / error checks (run on any platform) -----
+
+# `by` column name must exist in the data frame
+expect_error(tinyplot(iris, by = "nope"))
+
+# non-syntactic column names work (matching pairs())
+df = iris[, 1:3]
+names(df) = c("a b", "c d", "e f")
+expect_silent(tinyplot(df))
+
+# a pre-existing "__by__" column does not collide with the spliced vector
+df = iris[, 1:4]
+df[["__by__"]] = rnorm(nrow(df))
+expect_silent(tinyplot(df, by = iris$Species))
diff --git a/man/tinyplot.data.frame.Rd b/man/tinyplot.data.frame.Rd
new file mode 100644
index 00000000..cdae00eb
--- /dev/null
+++ b/man/tinyplot.data.frame.Rd
@@ -0,0 +1,72 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tinyplot.data.frame.R
+\name{tinyplot.data.frame}
+\alias{tinyplot.data.frame}
+\title{tinyplot Method for Plotting Data Frames}
+\usage{
+\method{tinyplot}{data.frame}(x, formula = NULL, by = NULL, labs = FALSE, frames = FALSE, ...)
+}
+\arguments{
+\item{x}{an object of class \code{"data.frame"}.}
+
+\item{formula}{a \code{\link[stats]{formula}} that is passed on to
+\code{\link{tinyplot.formula}}. If \code{formula} is \code{NULL} a formula of
+type \code{y ~ 1} or \code{y ~ x} is set up for 1- and 2-variable data frames,
+respectively. For data frames with 3+ variables, a
+\code{\link[graphics]{pairs}}-style grid of all variable combinations is
+drawn instead.}
+
+\item{by}{(3+ case only) optional grouping variable. Either 1) a character
+string giving a column name in \code{x}, or 2) a vector of equal length to the
+main data.frame (\emph{caveat emptor}: this is not checked). The variable is
+spliced into each sub-plot's formula as \code{y ~ x | by} so groups are
+distinguished (e.g. by colour). Note that the legend is deliberately
+suppressed.}
+
+\item{labs}{(3+ case only) logical indicating whether the axes labels
+(titles) for each sub-plot in the pairs-style case should be shown. Default
+is \code{FALSE}.}
+
+\item{frames}{(3+ case only) logical indicating whether each sub-plot should
+be framed by a box. Default is \code{FALSE}. Note the trailing "s" plural case
+to disambiguate from \code{frame.plot}.}
+
+\item{...}{further arguments passed to \code{tinyplot}.}
+}
+\description{
+Convenience interface for visualizing
+\code{\link[base]{data.frames}} with tinyplot.
+}
+\details{
+This is a convenience function for plotting data frames with
+or without a formula. The case with the formula mainly facilitates
+using \code{tinyplot()} in combination with pipes. The case without
+formula provides a quick way of plotting the variables in a data frame:
+a single variable, a pair of variables, or a
+\code{\link[graphics]{pairs}}-style grid of all variable combinations for
+3 or more variables.
+}
+\examples{
+tinytheme("clean2")
+
+## using tinyplot() with data frames
+tinyplot(cars)
+tinyplot(iris, Sepal.Length ~ Petal.Width | Species)
+
+## note that this also enables usage with pipes (in R >= 4.1.0) such as
+# cars |> tinyplot()
+# iris |> tinyplot(Sepal.Length ~ Petal.Width | Species)
+
+## pairs-style display for data frames with 3 or more variables
+tinyplot(iris)
+
+## pass `by` arg to group the pairs display (legend is suppressed)
+## here, we also add optional frames around the individual sub-plots
+tinyplot(iris, by = "Species", frames = TRUE)
+
+## another option (but assumes objects of equal length)
+tinyplot(iris[, 1:4], by = iris$Species, frames = TRUE)
+
+tinytheme() ## reset theme
+
+}