Introduction to tinyplot

tinyplot is a lightweight extension of the base R graphics system, designed to simplify and enhance data visualization. This tutorial provides a gentle introduction to the package’s core features and syntax. We won’t cover everything, but you should come away with a solid understanding of how tinyplot works and how it can integrate with your own projects.

We start this tutorial by loading the package and a slightly modified version of the airquality dataset that comes bundled with base R.

library(tinyplot)

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()

As far as possible, tinyplot tries to be a drop-in replacement for regular plot calls.

par(mfrow = c(1, 2))

plot(0:10, main = "plot")
tinyplot(0:10, main = "tinyplot")

par(mfrow = c(1, 1)) # reset layout

Similarly, we can plot elements from a data frame using either the atomic or formula methods. Here is a simple example using the aq dataset that we created earlier.

# with(aq,  tinyplot(Day, Temp)) # atomic method (same as below)
tinyplot(Temp ~ Day, data = aq)  # formula method

TipTip: plt() shorthand alias

Use the plt() alias instead of tinyplot() to save yourself a few keystrokes. For example:

plt(Temp ~ Day, data = aq)

Feel free to try this shorthand with any of the plots that follow; tinyplot(<args>) and plt(<args>) should produce identical results.

Grouped data

Where tinyplot starts to diverge from its base counterpart is with respect to grouped data. In particular, tinyplot allows you to characterize groups using the by argument.1

# tinyplot(aq$Day, aq$Temp, by = aq$Month) # same as below
with(aq, tinyplot(Day, Temp, by = Month))

An arguably more convenient approach is to use the equivalent formula syntax. Just place the “by” grouping variable after a vertical bar (i.e., |).

tinyplot(Temp ~ Day | Month, data = aq)

You can use standard base plotting arguments to adjust features of your plot. For example, change pch (plot character) to get filled points and cex (character expansion) to change their size.

tinyplot(
  Temp ~ Day | Month, data = aq,
  pch = 16,
  cex = 2
)

Similarly, converting to a grouped line plot is a simple matter of adjusting the type argument.

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "l"
)

The default behaviour of tinyplot is to represent groups through colour. However, note that we can automatically adjust pch and lty by groups too by passing the "by" convenience keyword. This can be used in conjunction with the default group colouring. Or, as a replacement for group colouring—an option that may be particularly useful for contexts where colour is expensive or prohibited (e.g., certain academic journals).

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "l",
  col = "black", # override automatic group colours
  lty = "by"     # change line type by group instead
)

The "by" convenience argument is also available for mapping group colours to background fill bg (alias fill). One use case is to override the grouped border colours for filled plot characters and instead pass them through the background fill.

tinyplot(
  Temp ~ Day | Month, data = aq,
  pch = 21,      # use filled circles
  col = "black", # override automatic group (border) colours of points
  fill = "by"    # use background fill by group instead
)

Aesthetic and themes

The previous examples illustrate a core pillar of the tinyplot philosophy: customizing your plot aesthetic should be easy. But we can go further still. Take colours, for instance. The default set of discrete colours for grouped plots are inherited from the user’s current global palette; most likely the “R4” palette (see ?palette). But users can switch to another any of R’s (many) other built-in colour palettes simply by passing the name as a convenience string to the palette argument.2 For example:

tinyplot(
  Temp ~ Day | Month, data = aq,
  pch = 19,
  palette = "tableau" # or "ggplot2", "okabe", "set2", "harmonic", etc.
)

You can also use the alpha argument to adjust the (alpha) transparency of your colours.

tinyplot(
  Temp ~ Day | Month, data = aq,
  pch = 19,
  palette = "tableau",
  alpha = 0.3
)

The tinyplot function accepts many such arguments for tweaking your plot aesthetic. However, rather than overwhelm readers with all of the options, we would rather highlight a simpler way to customize your plots: themes. For example:

tinyplot(
  Temp ~ Day | Month, data = aq,
  pch = 19,
  theme = "dark"
)

Themes do a number of things at once and are the subject of a dedicated Themes vignette that goes into details. For the moment, just accept them as a convenient way to adjust your plot aesthetic without having to finagle all of various parameters that base R graphics often requires. Beyond convenience, themes also have the added benefit of enabling dynamic adjustment of the plot regions, so that excess whitespace is reduced.3

In the previous plot, we used theme = "dark" to invoke an ephemeral theme. If we drew a new plot, then it would revert back to the default aesthetic. But we can also set a persistent theme via the tinytheme() function. Let’s do that now, under the knowledge that all of the remaining plots in this vignette will adopt the same aesthetic (until we reset at the end).

# Set persistent theme for the rest of this vignette
tinytheme("clean2")

Try it out:

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "l",
  main = "Our new persistent theme",
  sub = "Notice that the subtitle is above the plot",
  cap = "Source: A helpful caption"
)

Legend

In all of the preceding plots, you will have noticed that we get an automatic legend. The legend position and look can be customized with the legend argument. At a minimum, you can pass the familiar legend position keywords as a convenience string ("topright", "bottom", etc.). Moreover, a key feature of tinyplot is that we can easily and elegantly place the legend outside the plot area by adding a trailing “!” to these keywords. (As you may have realised, the default legend position is "right!".) Let’s demonstrate by moving the legend to the left of the plot.

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "l",
  legend = "left!"
)

Another option is legend = "direct", which places text labels at the end of each group’s data instead of a separate legend box. This works best for line-based plots with x-sorted data, and pairs well with dynamic themes (like the "clean2" theme that we already enable abovesd) for tighter margins.

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "l",
  legend = "direct"
)

Beyond the convenience of positional keywords, the legend argument also permits additional customization in the form of a list of arguments, which will be passed on to the standard legend() function internally. So you can change or turn off the legend title, remove the bounding box, switch the direction of the legend text to horizontal, etc. Here is a grouped density plot example, where we also add some shading by specifying that the background colour should vary by groups too.

tinyplot(
  ~ Temp | Month,
  data = aq,
  type = "density",
  fill = "by",                                    # add fill by groups
  legend = list("top!", title = FALSE, bty = "o") # change legend features
)

All of the legend examples that we have seen thus far are representations of discrete groups. However, please note that tinyplot also supports grouping by continuous variables, which automatically yield gradient legends.

tinyplot(Ozone ~ Temp | Wind, data = aq)

Gradient legends (and plots) can be customized in an identical manner to discrete legends by adjusting the keyword positioning, palette choice, alpha transparency, etc. Here is a quick adaptation of the previous plot to demonstrate.

tinyplot(
  Ozone ~ Temp | Wind, data = aq,
  pch  = 21,      # use filled plot character
  cex  = 2,       # bigger points
  col  = "black", # override automatic (grouped) border colour of points
  fill = 0.5,     # use background fill instead with added alpha transparency 
)

As an aside, note that we passed a numeric convenience argument to fill (alias bg) above. Specifically, when fill is given as a numeric in the range of [0,1] then it automatically inherits the grouped colour mappings, but with corresponding alpha transparency.

More plot types

We’ve already seen several plot types, such as "p" (points), "l" (lines), and "density". In general, tinyplot supports all of the standard plot types and elements available in base R, along with a number of additional types that can be tedious to code manually. You can find the full list of available types in the dedicated Plot Types vignette. For the moment, we’ll content ourselves with only one or two illustrative examples.

Earlier, we saw a grouped density plot. But these can be hard to reason about if there is a high degree of overlap between densities. A good alternative in such cases is a ridge(line) plot, which tinyplot supports out of the box. Note that ridge plots are best viewed with their own dedicated theme.

tinyplot(
  Month ~ Temp, aq,
  type = "ridge",
  theme = "ridge2", # or "ridge" (adds a plot frame)
  # optional customizations
  gradient = TRUE, col = "white"
)

Similarly, tinyplot also supports special types to fit models and display their predictions, along with confidence intervals. Here is a somewhat silly example where we fit a linear model to predict temperature by day of month.4

tinyplot(
  Temp ~ Day | Month, aq,
  type = "lm",
  legend = list("direct", repel = TRUE),
  main = "Linear model"
)

The default behaviour of these model types can be adjusted by passing appropriate arguments to the equivalent functional version of the type in question. These functional types all follow a type_<typename> syntax, so that "lm" is paired with type_lm(), "ridge" with type_ridge(), etc. Below we illustrate with an adapted generalised linear model, where we explicitly pass a family argument to fit a logistic regression.

tinyplot(
  I(hot == "hot") ~ Wind, aq,
  # type = "glm", ## default is gaussian
  type = type_glm(family = binomial),
  main = "Logit model: Temps above 75 °F",
  ylab = "Probability", yaxl = "%"
)

Again, these are just a few examples of the many different plot types that tinyplot supports. Please also take a look at the dedicated Plot Types vignette for the full list, as well a guide for creating your own custom types. You can also request support for additional plot types, or see what’s on our roadmap, by heading over to this pinned issue on our GitHub repo.

Facets

Alongside the standard “by” grouping approach that we have seen thus far, tinyplot also supports faceted plots. Mirroring the main tinyplot function, the facet argument accepts both atomic and formula methods. In general, however, we recommend the formula version as being safer since it does a better job of handling missing values.

tinyplot(
  Temp ~ Day, aq,
  facet = ~Month, ## <= facet, not by
  type = "lm",
  main = "Predicted air temperatures"
)

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.

tinyplot(
  Temp ~ Day | Month, aq,
  facet = "by", # facet along same dimension as groups
  type = "lm",
  main = "Predicted air temperatures"
)

To customize facets, simply pass a list of named arguments through the companion facet.args argument. Customization options include: override the default “square” facet window arrangement; allow free-scaled axes so that the limits of each individual facet are drawn independently; adjust the padding (margin) between individual facets; change the facet title text and background; etc. Here is a simple example where we (1) arrange the facets in a single row and (2) change the facet strip text and background fill.

tinyplot(
  Temp ~ Day, aq,
  facet = ~Month,
  facet.args = list(nrow = 1, col = "white", bg = "black"),
  type = "lm",
  main = "Predicted air temperatures"
)

The facet.args customizations can also be set globally via the tpar() function, or as part of a your tinytheme() configuration.

Finally, the facet argument also accepts a two-sided formula for arranging facets in a fixed grid layout. Here is a simple, if contrived, example.

tinyplot(
  Temp ~ Day, data = aq,
  facet = windy ~ hot
)

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 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 be verbose since it requires users to make very similar successive calls, with many shared arguments.

For this reason, tinyplot provides a special tinyplot_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; all others arguments will be inherited from the original call.

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 fits 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.

tinyplot(
  Temp ~ Day | Month, aq,
  facet = "by",
  legend = FALSE,
  main = "Actual and predicted air temperatures",
  cap = "Note: Shaded regions give OLS fit"
)
# Add regression fits
tinyplot_add(type = "lm")

TipTip: plt_add() shorthand alias

Much like plt() is an alias for tinyplot(), you can save yourself a few keystrokes by typing plt_add() instead of tinyplot_add():

plt_add(type = "lm")

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 using abline(), or simply annotate your plot using text(). This works as usual with a regular plots, but not with facetted plots because text(), abline(), rect(), etc. are not facet aware.

The tinyplot way of adding common elements to facetted plots is by passing the tinyplot(..., draw = <draw_function>) argument. Here we demonstrate with a simplified version of our facet grid example from earlier.

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)
)

This works because the draw argument is facet-aware and will ensure that each individual facet is correctly drawn upon. Moreover, with draw you can leverage the special ii internal counter to draw programmatically across facets (see here). 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 is 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. (We’ll also override with a different theme, to avoid overplotting with the grid lines, etc.)

tinyplot(
  Temp ~ Day | Month, aq, facet = "by", lwd = 3, type = "l",
  legend = FALSE,
  theme = "float", ylim = c(50, 100), frame = FALSE,
  draw = {
    tinyplot_add(col = "grey", lwd = 1, facet = NULL)
    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)
  }
)

Saving plots

A final point to note is that tinyplot offers convenience features for exporting plots to disk. Simply invoke the file argument to specify the relevant file path (including the extension type). You can customize the output dimensions (in inches) via the accompanying width and height arguments.5

tinyplot(
  Temp ~ Day | Month, data = aq,
  file = "aq.png", width = 8, height = 5
)

# optional: delete the saved plot
unlink("aq.png")

Alongside convenience, the benefit of this native tinyplot approach (versus the traditional approach of manually opening an external graphics device, e.g. png()) is that all of your current graphic settings are automatically carried over to the exported file. Feel free to try yourself by setting some global graphics parameters via tpar() and then using file to save a plot.

Reset theme

Don’t forget to reset the plot theme.

# reset theme to default
tinytheme()

Conclusion

The goal of this tutorial has been to give you a clear sense of how tinyplot works and what it offers. The sales pitch is simple: you get to use the familiar syntax from base R plot(), but with many additional plot types and user-friendly features.

Believe it or not, there’s loads more to tinyplot that we didn’t cover here. We recommend continuing with the Plot types and Themes vignettes if you’d like to keep exploring. Or, take a look at our Gallery page if you’re looking for quick inspiration with code examples.

We’ll close by emphasizing that tinyplot has no dependencies other than base R itself. We hope that this makes it an attractive and lightweight option for package developers—and regular R users!— who want to create convenient, sophisticated plots with minimal overhead.

Happy tinyplotting!

Footnotes

  1. At this point, experienced base plot users might protest that you can colour by groups using the col argument, e.g. with(aq, plot(Day, Temp, col = Month)). This is true, but there are several limitations. First, you don’t get an automatic legend. Second, the base plot.formula method doesn’t specify the grouping within the formula itself (not a deal-breaker, but not particularly consistent either). Third, and perhaps most importantly, this grouping doesn’t carry over to line plots (i.e., type=“l”). Instead, you have to transpose your data and use matplot. See this old StackOverflow thread for a longer discussion.↩︎

  2. See palette.pals() and hcl.pals() for the full set of available palettes. Or, read Zeileis & Murrell (2023, The R Journal, doi:10.32614/RJ-2023-071). Beyond these convenience strings, users can also supply a valid palette-generating function for finer control and additional options. If you have installed the ggsci package (link), for example, then you could use palette = pal_npg() to generate a palette consistent with those used by the Nature Publishing Group.↩︎

  3. You can already see in the “dark” theme example above: Notice that the plot region takes up more vertical space, since it isn’t trying to reserve space for the missing title and subtitle.↩︎

  4. The grouped setting here makes this visualization equivalent to predict(lm(Temp ~ 0 + Month / Day, data = aq), interval = "confidence").↩︎

  5. The default dimensions are 7x7, with a resolution of 300 DPI. However, these too can be customized via the file.width, file.height, and file.res parameters in tpar().↩︎