diff --git a/R/facet.R b/R/facet.R index 5617a3c1..744360d3 100644 --- a/R/facet.R +++ b/R/facet.R @@ -453,6 +453,10 @@ draw_facet_window = function(grid, ...) { grid } } + + # drawn elements + if (!is.null(draw)) eval(draw) + } # end of ii facet loop } # end of add check diff --git a/R/tinyplot.R b/R/tinyplot.R index c2fc2e6a..0b36ae49 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -253,6 +253,13 @@ #' added elements will be turned off. See also [tinyplot_add], which provides #' a convenient wrapper around this functionality for layering on top of an #' existing plot without having to repeat arguments. +#' @param draw a function that draws directly on the plot canvas (before `x` and +#' `y` are plotted). The `draw` argument is primarily useful for adding common +#' elements to each facet of a faceted plot, e.g. +#' \code{\link[graphics]{abline}} or \code{\link[graphics]{text}}. Note that +#' this argument is somewhat experimental and that _no_ internal checking is +#' done for correctness; the provided argument is simply captured and +#' evaluated as-is. See Examples. #' @param flip logical. Should the plot orientation be flipped, so that the #' y-axis is on the horizontal plane and the x-axis is on the vertical plane? #' Default is FALSE. @@ -433,6 +440,15 @@ #' data = aq #' ) #' +#' # To add common elements to each facet, use the `draw` argument +#' +#' tinyplot( +#' Temp ~ Day, +#' facet = windy ~ hot, +#' data = aq, +#' draw = abline(h = 75, lty = 2, col = "hotpink") +#' ) +#' #' # The (automatic) legend position and look can be customized using #' # appropriate arguments. Note the trailing "!" in the `legend` position #' # argument below. This tells `tinyplot` to place the legend _outside_ the plot @@ -520,6 +536,7 @@ tinyplot.default = function( ymin = NULL, ymax = NULL, add = FALSE, + draw = NULL, file = NULL, width = NULL, height = NULL, @@ -543,6 +560,7 @@ tinyplot.default = function( dots = list(...) if (isTRUE(add)) legend = FALSE + draw = substitute(draw) # sanitize arguments @@ -1001,7 +1019,9 @@ tinyplot.default = function( ymax = datapoints$ymax, ymin = datapoints$ymin, xaxt = xaxt, xlabs = xlabs, xlim = xlim, yaxt = yaxt, ylabs = ylabs, ylim = ylim, - flip = flip, xaxs = xaxs, yaxs = yaxs + flip = flip, + draw = draw, + xaxs = xaxs, yaxs = yaxs ) list2env(facet_window_args, environment()) diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 72e52c28..45f45e9e 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -46,6 +46,7 @@ tinyplot(x, ...) ymin = NULL, ymax = NULL, add = FALSE, + draw = NULL, file = NULL, width = NULL, height = NULL, @@ -394,6 +395,14 @@ added elements will be turned off. See also \link{tinyplot_add}, which provides a convenient wrapper around this functionality for layering on top of an existing plot without having to repeat arguments.} +\item{draw}{a function that draws directly on the plot canvas (before \code{x} and +\code{y} are plotted). The \code{draw} argument is primarily useful for adding common +elements to each facet of a faceted plot, e.g. +\code{\link[graphics]{abline}} or \code{\link[graphics]{text}}. Note that +this argument is somewhat experimental and that \emph{no} internal checking is +done for correctness; the provided argument is simply captured and +evaluated as-is. See Examples.} + \item{file}{character string giving the file path for writing a plot to disk. If specified, the plot will not be displayed interactively, but rather sent to the appropriate external graphics device (i.e., @@ -599,6 +608,15 @@ tinyplot( data = aq ) +# To add common elements to each facet, use the `draw` argument + +tinyplot( + Temp ~ Day, + facet = windy ~ hot, + data = aq, + draw = abline(h = 75, lty = 2, col = "hotpink") +) + # The (automatic) legend position and look can be customized using # appropriate arguments. Note the trailing "!" in the `legend` position # argument below. This tells `tinyplot` to place the legend _outside_ the plot diff --git a/vignettes/introduction.qmd b/vignettes/introduction.qmd index 65d7d81f..9e263550 100644 --- a/vignettes/introduction.qmd +++ b/vignettes/introduction.qmd @@ -32,8 +32,12 @@ of the `airquality` dataset that comes bundled with base R. ```{r aq} library(tinyplot) -aq = airquality -aq$Month = factor(month.abb[aq$Month], levels = month.abb[5:9]) +aq = transform( + airquality, + Month = factor(month.abb[Month], levels = month.abb[5:9]), + hot = ifelse(Temp>=75, "hot", "cold"), + windy = ifelse(Wind>=15, "windy", "calm") +) ``` ## Equivalence with `plot()` @@ -374,51 +378,46 @@ tinyplot( ) ``` -By default, facets will be arranged in a square configuration if more than -three facets are detected. Users can override this behaviour by supplying -`nrow` or `ncol` in the "facet.args" helper function. (The margin padding -between individual facets can also be adjusted via the `fmar` argument.) Note -that we can also reduce axis label redundancy by turning off the plot frame. +Facets are easily combined with grouping. This can either be done separately +(i.e., distinct arguments for `by` and `facet`), or along the same dimension. +For the latter case, we provide a special `facet = "by"` convenience shorthand. -```{r facet_nrow} +```{r facet_by} tinyplot( - Temp ~ Day, aq, - facet = ~Month, facet.args = list(nrow = 1), + Temp ~ Day | Month, aq, + facet = "by", # facet along same dimension as groups type = "lm", grid = TRUE, - frame = FALSE, main = "Predicted air temperatures" ) ``` -Here's a slightly fancier version where we combine facets with (by) colour -grouping, add a background fill to the facet text, and also overlay the -original values alongside our model predictions. For this particular example, -we'll use the `facet = "by"` convenience shorthand to facet along the same -month variable as the colour grouping. But you can easily specify different `by` -and `facet` variables if that's what your data support. +To customize facets, simply pass a list of named arguments through the companion +`facet.args` argument. For example, users can override the default "square" +facet window arrangement by supplying explicit `nrow` or `ncol` values. They can +also adjust the padding (margin) between individual facets, change the facet +title text and background, etc., etc. Here's a simple example where we (1) +arrange the facets in a single row, (2) add some background fill to the facet +text, and (3) and reduce axis redundancy by turning off the plot frame. -```{r facet_fancy} +```{r facet_nrow} tinyplot( - Temp ~ Day | Month, aq, - facet = "by", facet.args = list(bg = "grey90"), + Temp ~ Day, aq, + facet = ~Month, facet.args = list(nrow = 1, bg = "grey90"), type = "lm", - palette = "dark2", grid = TRUE, - axes = "l", - ylim = c(50, 100), - main = "Actual and predicted air temperatures" + frame = FALSE, # turning off the plot frame means only outer axes will be printed + main = "Predicted air temperatures" ) -tinyplot_add(type = "p") ``` -Note that the `facet` argument also accepts a _two-sided_ formula for arranging +The `facet.args` customizations can also be set globally via the `tpar()` +function. We will revisit this idea in the @themes section below. + +Finally, the `facet` argument also accepts a _two-sided_ formula for arranging facets in a fixed grid layout. Here's a simple (if contrived) example. ```{r facet_grid} -aq$hot = ifelse(aq$Temp>=75, "hot", "cold") -aq$windy = ifelse(aq$Wind>=15, "windy", "calm") - tinyplot( Temp ~ Day, data = aq, facet = windy ~ hot, @@ -430,28 +429,89 @@ tinyplot( ) ``` -The `facet.args` customizations can also be set globally via the `tpar()` -function, which provides a nice segue to our penultimate section. - ## Layers -In many contexts, it is convenient to build plots step-by-step, adding layers with different elements on top of a base. - -The `tinyplot()` function includes an `add=TRUE` argument that adds element to an existing plot, instead of drawing a new window. This argument is useful, but a bit verbose, as it requires users to make very similar successive calls, with many shared arguments. - -For convenience, the package also includes a `tinyplot_add()` function (alias `plt_add()`), which captures the last `tinyplot()` call, keeps all the same arguments, and modifies just the arguments that the user explicitly wants to change. - -In the following example, we first draw linear regression lines with facets and group coloring. Then, we add the original data points using `tinyplot_add()`. Notice that the same grouping and data options are carried over to points, without having to specify them in the `tinyplot_add()` function. +In many contexts, it is convenient to build plots step-by-step, adding layers +with different elements on top of a base. The **tinyplot** package offers a few +ways to achieve this layering effect. + +Similar to many base plotting functions, users can invoke the +`tinyplot(..., add=TRUE)` argument to draw a plot on top of an existing one, +rather than opening a new window. However, while this argument is useful, it can +become verbose since it requires users to make very similar successive calls, +with many shared arguments. + +For this reason, the **tinyplot** package also provides a special +`tinyplot_add()` (alias `plt_add()`) convenience function for adding layers to +an existing tinyplot. The idea is that users need simply pass the _specific_ +arguments that they want to add or modify relative to the base layer, and all +arguments will be inherited from the original. + +An example may help to demonstrate. Here we first draw some faceted points with +group colouring and various other aesthetic tweaks. Next, we add regression +lines with a simple `tinyplot_add(type = "lm")` call. Notice that the original +grouping, data and aesthetic options are all carried over correctly, without +having to repeat ourselves. + +```{r tinyplot_add} +tinyplot( + Temp ~ Day | Month, aq, + facet = "by", facet.args = list(bg = "grey90"), + palette = "dark2", + legend = FALSE, + grid = TRUE, + axes = "l", + ylim = c(50, 100), + main = "Actual and predicted air temperatures" +) +# Add regression lines +tinyplot_add(type = "lm") +``` -```{r} -library(tinyplot) +A related---but distinct---concept to adding plot layers is _drawing_ on a plot. +The canonical use case is annotating your plot with text or some other +function-based (rather than data-based) logic. For example, you may want to +demarcate some threshold values with horizontal or vertical lines, or simply +annotate your plot with text. The **tinyplot** way to do this is by passing the +`tinyplot(..., draw = )` argument. Here we demonstrate with a +simplified version of our facet grid example from earlier. -tinyplot(Sepal.Width ~ Sepal.Length | Species, - facet = ~Species, - data = iris, - type = "p") +```{r draw_simple} +tinyplot( + Temp ~ Day, data = aq, + facet = windy ~ hot, + # draw a horizontal (threshold) line in each facet using the abline function + draw = abline(h = 75, lty = 2, col = "hotpink") +) +``` -tinyplot_add(type = type_lm()) +Compared to "manually" drawing these elements on a plot _ex post_---e.g., via a +separate `abline()` call---there are several advantages to the idiomatic +**tinyplot** interface. First, the `draw` argument is facet-aware and will +ensure that each individual facet is correctly drawn upon. Second, you can +leverage the special `ii` internal counter to draw programmatically across +facets (see +[here](https://github.com/grantmcdermott/tinyplot/pull/245#issue-2642589951)). +Third, the `draw` argument is fully generic and accepts _any_ +drawing/annotating function. You can combine multiple drawing functions by +wrapping them with curly brackets`{}`, and even pass `tinyplot()` back towards +itself. + +Here's a slightly more complicated "spaghetti" plot example, where we pass +multiple functions through `draw = {...}`, including drawing all of the +lines in the background (of each facet) via a secondary `tinyplot()` call. + +```{r draw_spaghetti} +tinyplot( + Temp ~ Day | Month, aq, facet = "by", lwd = 3, type = "l", + frame = FALSE, legend = FALSE, ylim = c(50, 100), + draw = { + tinyplot(Temp ~ Day | Month, aq, col = "grey", type = "l", add = TRUE) + abline(h = 75, lty = 2) + text(5.5, 75, "Cold", pos = 1, offset = 0.4) + text(5.5, 75, "Hot", pos = 3, offset = 0.4) + } +) ``` ## Themes