Plot types

A key feature of tinyplot is the type argument, which allows you to specify different kinds of plots. This tutorial will guide you through the various plot types available in tinyplot, demonstrating how the type argument can be used to to create a wide range of visualizations.

We will consider three categories of plot types:

  1. Base types
  2. tinyplot types
  3. Custom types

We will use the built-in airquality dataset for our examples:

library(tinyplot)

aq = transform(
  airquality,
  Month = factor(Month, labels = month.abb[unique(Month)]),
  Hot = Temp > median(Temp)
)

Base types

The type argument in tinyplot supports all the standard plot types from base R’s plot function. These are specified using single-character strings.

  • "p" (Points): Produces a scatter plot of points. This is (usually) the default plot type.
  • "l" (Lines): Produces a line plot.
  • "b" (Points and Lines): Combines points and lines in same the plot.
  • "c" (Empty Points Joined by Lines): Plots empty points connected by lines.
  • "o" (Overplotted Points and Lines): Overlaps points and lines.
  • "s" and "S" (Stair Steps): Creates a step plot.
  • "h" (Histogram-like Vertical Lines): Plots vertical lines resembling a histogram.
  • "n" (Empty Plot): Creates an empty plot frame without data.

For example, we can use "b" to create a plot with combined points and lines.

tinyplot(Temp ~ Day | Month, data = aq, type = "b", main = "Points and lines")

tinyplot types

Beyond the base types, tinyplot introduces additional plot types for more advanced visualizations. Each of these additional types are available either as a convenience string (with default behaviour) or a companion type_*() function (with options for customized behaviour).

Shapes

string function description docs
"area" type_area() Plots the area under the curve from y = 0 to y = f(x). link
"errorbar" type_errorbar() Adds error bars to points; requires ymin and ymax. link
"l" / "b" / etc. type_lines() Draws lines and line-alike (same as base "l", "b", etc.) link
"pointrange" type_pointrange() Combines points with error bars. link
"p" type_points() Draws points (same as base "p"). link
"polygon" type_polygon() Draws polygons. link
"polypath" type_polypath() Draws a path whose vertices are given in x and y. link
"rect" type_rect() Draws rectangles; requires xmin, xmax, ymin, and ymax. link
"ribbon" type_ribbon() Creates a filled area between ymin and ymax. link
"segments" type_segments() Draws line segments between pairs of points. link
"text" type_text() Adds text annotations to a plot. link

Visualizations

string function description docs
"barplot" / "bar" type_barplot() Creates a bar plot. link
"boxplot" / "box" type_boxplot() Creates a box-and-whisker plot. link
"chull" type_chull() Draws convex hull(s) around grouped points. link
"density" type_density() Plots the density estimate of a variable. link
"ellipse" type_ellipse() Draws confidence ellipse(s) around grouped points. link
"histogram" / "hist" type_histogram() Creates a histogram of a single variable. link
"jitter" / "j" type_jitter() Jittered points. link
"qq" type_qq() Creates a quantile-quantile plot. link
"ridge" type_ridge() Creates a ridgeline (aka joy) plot. link
"rug" type_rug() Adds a rug to an existing plot. link
"spineplot" / "spine" type_spineplot() Creates a spine plot or spinogram. link
"violin" type_violin() Creates a violin plot. link

Models

string function description docs
"loess" type_loess() Local regression curve. link
"lm" type_lm() Linear regression line. link
"glm" type_glm() Generalized linear model fit. link
"spline" type_spline() Cubic (or Hermite) spline interpolation. link

Functions

string function description docs
"abline" type_abline() Line(s) with intercept and slope. link
"hline" type_hline() Horizontal line(s). link
"vline" type_vline() Vertical line(s). link
"function" type_function() Arbitrary function. link
"summary" type_summary() Summarizes y by unique values of x. link

To see the difference between the convenience strings and their respective type_*() functional equivalents, let’s quickly walk through two examples.

Example 1: jittering. We can add noise to data points using jittering. This allows us to avoid overplotting and can be useful to visualize discrete variables. On the left, we use the "jitter string shortcut with default settings. On the right, we use the type_jitter() function to reduce the amount of jittering.

tinyplot(
  Temp ~ Month, data = aq, main = "Default jittering", 
  type = "jitter"
)
tinyplot(
  Temp ~ Month, data = aq, main = "Modest jittering",
  type = type_jitter(amount = 0.05)
)

Example 2: Logit fit. In this example, we use type_glm() to fit a logistic regression model to the data, but with different confidence intervals.1

tinyplot(
  Hot ~ Wind, data = aq, main = "Logit with 95% interval",
  type = type_glm(family = "binomial")
)
tinyplot(
  Hot ~ Wind, data = aq, main = "Logit with 50% interval",
  type = type_glm(family = "binomial", level = 0.5)
)

To see what arguments are available for each type, simply consult the type-specific documentation.

?type_jitter
?type_glm
# etc

Finally, please note that all tinyplot types support grouping and faceting.

tinyplot(Temp ~ Wind | Month, data = aq, facet = "by", type = "lm")

Custom types

We want tinyplot to be as extensible as possible. Users should be able to create their own custom tinyplot types and, where appropriate, make them available to others.2 To create a custom type, you need only define a new type_<type>() function that adheres to a few basic principles.

Let’s walk through a minimal example to see how this works in practice.

A minimal example

Consider a custom type_log() that logs both x and y before plotting points.

type_log = function(base = exp(1)) {

  # data transformation function
  data_log = function() {
    fun = function(settings, ...) {
      # extract raw datapoints from settings environment
      datapoints = settings$datapoints

      # transform (log) the data
      datapoints$x = log(datapoints$x, base = base)
      datapoints$y = log(datapoints$y, base = base)
      datapoints = datapoints[order(datapoints$x), ]

      # re-assign modified datapoints back to settings
      settings$datapoints = datapoints
    }
    return(fun)
  }

  # drawing function
  draw_log = function() {
    fun = function(ix, iy, icol, ...) {
      points(
        x = ix,
        y = iy,
        col = icol
      )
    }
    return(fun)
  }

  # return object (list)
  out = list(
    data = data_log(),
    draw = draw_log(),
    name = "p" # fallback behaviour same as "p" type (e.g., legend defaults)
  )
  class(out) = "tinyplot_type"
  return(out)

}

To use, simply pass to type = type_log(<args>) as per as normal.

tinyplot(mpg ~ wt | factor(am), data = mtcars,
  type = type_log(), main = "Ln (natural logarithm)")
tinyplot(mpg ~ wt | factor(am), data = mtcars,
  type = type_log(base = 10), main = "Log (base 10)")

How it works

As illustrated by our minimal example, custom tinyplot types require three key functions:

  1. type_<type>(): The main wrapper function that users will call. It should accept any type-specific arguments that users will need to adjust the final plot (e.g., here: base). More importantly, it must return a list of class "tinyplot_type" and contain three elements:
    • data: A data transformation function (see below)
    • draw: A drawing function (see below)
    • name: The name of the underlying plot type. This can be anything, but re-using the name of a built-in type (e.g., "p") is a convenient way to inherit its default/fallback behaviour (e.g. legend defaults).
  2. data_<type>(): A function factory that returns a data transformation function:
    • Required arguments: settings and ...
    • settings is an environment containing plot data and parameters, including datapoints (a data frame with x, y, by, etc.), legend_args (a list of legend customizations), and many other internal parameters (see Appendix: Available settings below for a complete list).
    • Should modify settings in-place (e.g., settings$datapoints = ...)
    • Does not need to return anything
  3. draw_<type>(): A function factory that returns a drawing function:
    • Required arguments: ...
    • Available arguments: ix, iy, ixmin, ixmax, iymin, iymax, icol, ibg, ipch, ilty, ilwd, icex, and others
    • The i prefix indicates data for a single group (when using | grouping or facets)
    • Should call base R graphics functions (e.g., points(), lines(), polygon()) to render the plot

The key insight is that data_*() transforms the data once, while draw_*() is called repeatedly for each group or facet.

Pro tip: Re-use existing plot types

Rather than code up all of the internals for a custom type from scratch, users are strongly encouraged to re-use the scaffolding for existing types. One of the easiest ways to do this—which we use repeatedly in the main tinyplot codebase—is by “delegating” to an existing type at the end of the data_<type> function. Again, this is perhaps easier seen than explained, so consider an alternate version of our previous type_log() function.

type_log2 = function(base = exp(1)) {

  # data transformation function
  data_log = function() {
    fun = function(settings, ...) {
      datapoints = settings$datapoints

      datapoints$x = log(datapoints$x, base = base)
      datapoints$y = log(datapoints$y, base = base)
      datapoints = datapoints[order(datapoints$x), ]

      settings$datapoints = datapoints

      # re-assign/delegate to "p" type
      settings$type = "p"
    }
    return(fun)
  }

  # return object (list)
  out = list(
    data = data_log(),
    draw = NULL, # NULL => fall back to default type (now "p") drawing function
    name = "log"
  )
  class(out) = "tinyplot_type"
  return(out)

}

The key differences from our original type_log function are:

  • settings$type = "p" (re-assign the type at the end of data_log())
  • draw = NULL (drop the dedicated draw_log() function entirely; will fall back to draw_points() internally now)

This new version is obviously shorter than our original version, since we can skip the custom draw_log() section. But re-using existing tinyplot scaffolding brings added benefits beyond code concision. In particular, it guarantees that all of the functionality from an existing type will be made available to your custom type. (Which, trust us, is safer.) For example, it guarantees that grouping behaviour by other aesthetics (e.g, pch) passes through “for free”.

tinyplot(mpg ~ wt | factor(am), data = mtcars,
  pch = "by",
  type = type_log2(), main = "Better to re-use an existing drawing type")

Moral of the story: be lazy and re-use existing tinyplot scaffolding wherever possible, especially for drawing functions.

More examples

The tinyplot source code contains many examples of type constructor functions that should provide a helpful starting point for custom plot types. Each built-in tinyplot type is itself implemented as a custom type, so you can see real-world examples of varying complexity. For example, take a look at the type_points.R or type_area.R code. (The latter provides an example where we delegate to the type_ribbon drawing code.)

Beyond that, the tinyplot team are always happy to help guide users on how to create their own types. Just let us know by raising an issue on our GitHub repo.

Advanced customization: declaring special type behaviour

In most cases, simply declaring the data_<type>() transformation and passing to an appropriate drawing function should be enough for your custom type. Additional features like faceting, theming, and layering should flow seamlessly through the rest of the tinyplot machinery.

However, some plot types require advanced customization features that go beyond the basics. For example, type_spineplot() ships its own special (dual) axes logic that the surrounding scaffolding—margins, axes limits—needs to account for. For these special cases, we provide several escape hatches that your type can plug into to override default behaviour. The one you’re most likely to need is settings$type_hints; a named list that you can set inside your data_<type>() function. If a specific hint is declared, then tinyplot knows to use it for custom behaviour rather than reverting to the default logic.

Again, an example may help to illustrate. Say we are defining a custom type whose axes should span a fixed [0, 1] range (i.e., we don’t want its axis limits determined by the empirical data range). We can pass the special has_proportional_lim flag to ensure this:

data_mytype = function() {
  fun = function(settings, ...) {
    settings$type_hints = list(has_proportional_lim = TRUE)
    # ... other data transformations
  }
  return(fun)
}

Currently, type_hints accepts several known hints; all of which are booleans. Any hint left undefined is treated internally as NULL and is ignored. So you need only define the list of hints that you actually want:

Hint Declared by Effect when TRUE
draws_own_axes type_ridge(), type_spineplot() Your draw_*() function draws its own axes (having set xaxt/yaxt to "n"). Reserves the tick-row margin that would otherwise be dropped, so self-drawn labels aren’t clipped.
has_rhs_axis type_spineplot() Reserve right-hand margin for a secondary axis.
has_proportional_lim type_spineplot() Treat the limits as proportional (e.g. [0, 1]); don’t expand them to axis breaks.
framed type_spineplot() Report the user’s frame.plot choice when your type overrides the setting internally (e.g. because it draws its own box). Used to decide whether interior facets draw their own axes.
legend_fills_from_col type_hexbin(), type_spineplot() Fill the legend key with the group colour (col) rather than bg.
legend_fills_from_seq_palette type_ridge() Fill the legend key with a lighter step of the group colour’s sequential ramp.
legend_border_fg type_spineplot() Draw the legend key border in the foreground colour rather than the group colour.

To see type_hints in action, consult the source code of the types listed above.

Beyond type_hints, another customization escape hatch is provided by the sister type_info setting. Without going into too much detail, the key difference between these two is that type_hints specifies behaviour you want the plot machinery to respect, while type_info is used for passing bespoke rendering payload from data_<type>() through to draw_<type>() (e.g., coordinates, precomputed breaks, and so on). An example is the custom hex polygon offsets of type_hexbin() (see here).

We reiterate that type_hints and type_info are very much in the advanced corner of the tinyplot codebase. Most users can safely ignore these internal features when creating their own custom types. But feel free to reach out if you need help.

Appendix: Available settings

To see what objects and parameters are available in the settings environment, we can create a simple type_error() function that stops with the names of all available settings:

type_error = function() {
  data_error = function() {
    fun = function(settings, ...) {
      stop(paste(sort(names(settings)), collapse = ", "))
    }
    return(fun)
  }

  out = list(
    data = data_error()
  )
  class(out) = "tinyplot_type"
  return(out)
}

tinyplot(mpg ~ wt, data = mtcars, type = type_error())
Error in `settings$type_data()`:
! add, alpha, axes, bg, bubble, bubble_alpha, bubble_bg_alpha, bubble_pch, by, by_dep, call, cap, cex, cex_dep, col, datapoints, dodge, dots, draw, facet, facet_attr, facet_by, facet_dep, facet.args, file, fill, flip, frame.plot, group_offsets, height, labels, legend, legend_args, lighten, log, lty, lwd, null_by, null_facet, null_palette, null_xlim, null_ylim, offsets_axis, palette, pch, rev_x, rev_y, ribbon.alpha, sub, type, type_data, type_draw, type_hints, type_info, type_name, weights, weights_used, width, x, x_by, x_dep, xaxb, xaxl, xaxs, xaxt, xlab, xlabs, xlim, xmax, xmax_dep, xmin, xmin_dep, y, y_dep, yaxb, yaxl, yaxs, yaxt, ygroup, ylab, ylabs, ylim, ymax, ymax_dep, ymin, ymin_dep

As we can see, the settings environment contains many parameters that custom types can use as inputs or modify as needed.

Footnotes

  1. Note: If we simply specified the "glm" convenience string on its own, we’d get a linear fit since the default family is gaussian.↩︎

  2. Examples of downstream R packages shipping bespoke tinyplot types include descstat (see here and here) and trackframe (see here).↩︎