Skip to content

Commit

Permalink
draw argument (#245)
Browse files Browse the repository at this point in the history
* draw arg

* add draw examples to layering section of vignette

* fix chunk names

* switch to spaghetti plot

* wording tweak

* typo
  • Loading branch information
grantmcdermott authored Nov 12, 2024
1 parent 35ade48 commit 6df7187
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 47 deletions.
4 changes: 4 additions & 0 deletions R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 21 additions & 1 deletion R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -520,6 +536,7 @@ tinyplot.default = function(
ymin = NULL,
ymax = NULL,
add = FALSE,
draw = NULL,
file = NULL,
width = NULL,
height = NULL,
Expand All @@ -543,6 +560,7 @@ tinyplot.default = function(
dots = list(...)

if (isTRUE(add)) legend = FALSE
draw = substitute(draw)


# sanitize arguments
Expand Down Expand Up @@ -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())

Expand Down
18 changes: 18 additions & 0 deletions man/tinyplot.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

152 changes: 106 additions & 46 deletions vignettes/introduction.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down Expand Up @@ -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,
Expand All @@ -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 = <draw_function>)` 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
Expand Down

0 comments on commit 6df7187

Please sign in to comment.