Tutorial

The goal of this intro tutorial is to give you a sense of the main features and syntax of tinyplot, a lightweight extension of the base R graphics system. We don’t try to cover everything, but you should come away with a good understanding of how the package works and how it can integrate with your own projects.

Preliminaries

Start by loading the package. For the examples in this tutorial, we’ll be using a slightly modified version of the airquality dataset that comes bundled with base R. So let’s go ahead and create that now too.

library(tinyplot)

aq = airquality
aq$Month = factor(month.abb[aq$Month], levels = month.abb[5:9])

Similarity to 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’s 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

If you’d prefer to save on a few keystrokes, you can use the shorthand plt alias instead.

plt(Temp ~ Day, data = aq) # `plt` = shorthand alias for `tinyplot`

(Note that the plt shorthand would work for all of the remaining plotting calls below. But we’ll stick to tinyplot to keep things simple.)

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.

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

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
)

Colours

On the subject of group colours, the default palette should adjust automatically depending on the class and cardinality of the grouping variable. For example, a sequential (“viridis”) palette will be used if an ordered factor is detected.

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

However, this behaviour is easily customized via the palette argument. The default set of discrete colours are inherited from the user’s current global palette. (Most likely the “R4” set of colors; see ?palette). However, all of the various palettes listed by palette.pals() and hcl.pals() are supported as convenience strings.2 Note that case-insensitive, partial matching for these convenience string is allowed. For example:

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "l",
  palette = "tableau" # or "ggplot", "okabe", "set2", "harmonic", etc.
)

Beyond these convenience strings, users can also supply a valid palette-generating function for finer control over alpha transparency, colour order, and so forth. We’ll see a demonstration of this further below.

To underscore what we said earlier, colours are inherited from the user’s current palette. So these can also be set globally, just as they can for the base plot function. The next code chunk will set a new default palette for the remainder of the plots that follow.

# Set the default palette globally via the generic palette function
palette("tableau")

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”, “left”, 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!"
)

Beyond the convenience of these positional keywords, the legend argument also permits additional customization by passing an appropriate function (or, a list of arguments that 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’s a grouped density plot example, where we also add some shading by specifying that the background colour should vary by groups too.

with(
  aq,
  tinyplot(
    density(Temp),
    by = Month,
    fill = "by",                         # add fill by groups
    grid = TRUE,                         # add background grid
    legend = list("topright", bty = "o") # change legend features
  )
)

Finally, note that gradient legends are also supported for continuous grouping variables. Gradient legends can be customized in a similar vein to the discrete legends that we have seen so far (keyword positioning, palette choice, alpha transparency, etc.) But here is a simple example that demonstrates the default behaviour.

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

Interval plots

tinyplot adds supports for interval plots via the "pointrange", "errorbar", "ribbon" type arguments. An obvious use-case is for regression analysis and prediction.

mod = lm(Temp ~ 0 + Month / Day, data = aq)
aq = cbind(aq, predict(mod, interval = "confidence"))

with(
  aq,
  tinyplot(
    x = Day, y = fit,
    ymin = lwr, ymax = upr,
    by = Month,
    type = "ribbon",
    grid = TRUE,
    main = "Model predictions"
  )
)

Similarly, we can grab the model estimates to produce nice coefficient plots.

coeftab = data.frame(
  gsub("Month", "", names(coef(mod))),
  coef(mod),
  confint(mod)
) |>
  setNames(c("term", "estimate", "ci_low", "ci_high"))

with(
  subset(coeftab, !grepl("Day", term)),
  tinyplot(
    x = term, y = estimate,
    ymin = ci_low, ymax = ci_high,
    type = "pointrange", # or: "errobar", "ribbon"
    pch = 19, col = "dodgerblue",
    grid = TRUE,
    main = "Average Monthly Effect on Temperature"
  )
)

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.

with(
  aq,
  tinyplot(
    x = Day, y = fit,
    ymin = lwr, ymax = upr,
    type = "ribbon",
    facet = Month, ## <= facet, not by
    grid = TRUE,
    main = "Predicted air temperatures"
  )
)

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.

with(
  aq,
  tinyplot(
    x = Day, y = fit,
    ymin = lwr, ymax = upr,
    type = "ribbon",
    facet = Month,
    facet.args = list(nrow = 1),
    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 add back the original values to 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.

# Plot the original points 
with(
  aq,
  tinyplot(
    x = Day, y = Temp,
    by = Month,
    facet = "by", facet.args = list(bg = "grey90"),
    palette = "dark2",
    grid = TRUE, frame = FALSE, ylim = c(50, 100),
    main = "Actual and predicted air temperatures"
  )
)
# Add the model predictions to the same plot 
with(
  aq,
  tinyplot(
    x = Day, y = fit,
    ymin = lwr, ymax = upr,
    by = Month, facet = "by",
    type = "ribbon",
    palette = "dark2",
    add = TRUE
  )
)

Again, the facet argument also accepts a formula interface. One particular use case is for two-sided formulas, which arranges the facet layout in a fixed grid arrangement. Here’s a simple (if contrived) example.

aq$hot = ifelse(aq$Temp>=75, "hot", "cold")
aq$windy = ifelse(aq$Wind>=15, "windy", "calm")

tinyplot(
 Temp ~ Day, data = aq,
 facet = windy ~ hot,
 # the rest of these arguments are optional...
 facet.args = list(col = "white", bg = "black"),
 pch = 16, col = "dodgerblue",
 grid = TRUE, frame = FALSE, ylim = c(50, 100),
 main = "Daily temperatures vs. wind"
)

The facet.args customizations can also be set globally via the tpar() function, which provides a nice segue to our final section.

Customization

Customizing your plots further is straightforward, whether that is done directly by changing tinyplot arguments for a single plot, or by setting global parameters. For setting global parameters, users can invoke the standard par() arguments. But for improved convenience and integration with the rest of the package, we recommend that users instead go via tpar(), which is an extended version of par() that supports all of the latter’s parameters plus some tinyplot-specific ones. Here’s a quick penultimate example, where we impose several global changes (e.g., rotated axis labels, removed plot frame to get Tufte-style floating axes, etc.) before drawing the plot. change our point character, tick labels, and font family globally, before adding some transparency to our colour palette, and use Tufte-style floating axes with a background panel grid.

op = tpar(
  bty    = "n",           # No box (frame) around the plot 
  family = "HersheySans", # Use R's Hershey font instead of Arial default
  grid   = TRUE,          # Add a background grid
  las    = 1,             # Horizontal axis tick labels
  pch    = 16             # Filled points as default
)

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "b",
  palette = palette.colors(palette = "tableau", alpha = 0.5),
  main = "Daily temperatures by month"
)

(For access to a wider array of fonts, you might consider the showtext package (link).)

At the risk of repeating ourselves, the use of (t)par in the previous example again underscores the correspondence with the base graphics system. Because tinyplot is effectively a convenience wrapper around base plot, any global elements that you have set for the latter should carry over to the former. For nice out-of-the-box themes, we recommend the basetheme package (link).

tpar(op) # revert global changes from above

library(basetheme)
basetheme("royal") # or "clean", "dark", "ink", "brutal", etc.

tpar(pch = 15) # filled squares as first pch type

tinyplot(
  Temp ~ Day | Month, data = aq,
  type = "b",
  pch = "by",
  palette = "tropic",
  main = "Daily temperatures by month"
)


basetheme(NULL)  # back to default theme
dev.off()
#> null device 
#>           1

Conclusion

In summary, consider the tinyplot package if you are looking for base R plot functionality with some added convenience features. You can use pretty much the same syntax and all of your theme elements should carry over too. It has no dependencies other than base R itself and this makes it an attractive option for package developers or situations where dependency management is expensive (e.g., an R application running in a browser via WebAssembly).

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 the accompanying help pages of those two functions for more details on the available palettes, or read Zeileis & Murrell (2023, The R Journal, doi:10.32614/RJ-2023-071).↩︎